diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/mailnews/imap | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
145 files changed, 57683 insertions, 0 deletions
diff --git a/comm/mailnews/imap/public/moz.build b/comm/mailnews/imap/public/moz.build new file mode 100644 index 0000000000..d6e40a3e80 --- /dev/null +++ b/comm/mailnews/imap/public/moz.build @@ -0,0 +1,29 @@ +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +XPIDL_SOURCES += [ + "nsIAutoSyncFolderStrategy.idl", + "nsIAutoSyncManager.idl", + "nsIAutoSyncMsgStrategy.idl", + "nsIAutoSyncState.idl", + "nsIImapFlagAndUidState.idl", + "nsIImapHeaderXferInfo.idl", + "nsIImapIncomingServer.idl", + "nsIImapMailFolderSink.idl", + "nsIImapMessageSink.idl", + "nsIImapMockChannel.idl", + "nsIImapOfflineSync.idl", + "nsIImapProtocol.idl", + "nsIImapProtocolSink.idl", + "nsIImapServerSink.idl", + "nsIImapService.idl", + "nsIImapUrl.idl", + "nsIMailboxSpec.idl", + "nsIMsgImapMailFolder.idl", +] + +XPIDL_MODULE = "msgimap" + +EXPORTS += [] diff --git a/comm/mailnews/imap/public/nsIAutoSyncFolderStrategy.idl b/comm/mailnews/imap/public/nsIAutoSyncFolderStrategy.idl new file mode 100644 index 0000000000..23ada6d468 --- /dev/null +++ b/comm/mailnews/imap/public/nsIAutoSyncFolderStrategy.idl @@ -0,0 +1,23 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +#include "nsIAutoSyncMsgStrategy.idl" + +interface nsIMsgFolder; + +[scriptable, uuid(d3bf91cc-37bb-4752-9994-1a8473e46a90)] +interface nsIAutoSyncFolderStrategy : nsISupports { + + /** + * Returns a relative-priority for the second folder by comparing with the first one. + */ + nsAutoSyncStrategyDecisionType sort(in nsIMsgFolder aFolder1, in nsIMsgFolder aFolder2); + + /** + * Tests whether the given folder should be excluded or not. + */ + boolean isExcluded(in nsIMsgFolder aFolder); + +}; diff --git a/comm/mailnews/imap/public/nsIAutoSyncManager.idl b/comm/mailnews/imap/public/nsIAutoSyncManager.idl new file mode 100644 index 0000000000..531b9e702b --- /dev/null +++ b/comm/mailnews/imap/public/nsIAutoSyncManager.idl @@ -0,0 +1,192 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIAutoSyncMsgStrategy; +interface nsIAutoSyncFolderStrategy; +interface nsIMsgDBHdr; +interface nsIAutoSyncState; +interface nsIAutoSyncMgrListener; +interface nsIMsgFolder; + +[scriptable, uuid(41ec36a7-1a53-4ca3-b698-dca6452a8761)] +interface nsIAutoSyncMgrListener : nsISupports { + + /** + * Queue types + */ + const long PriorityQueue = 1; + const long UpdateQueue = 2; + const long DiscoveryQueue = 3; + + /** + * It is called on the listener when a new folder is added into + * the queue + * + * @param aQType type of the queue + * @param aFolder folder that is added into the queue + */ + void onFolderAddedIntoQ(in long aQType, in nsIMsgFolder aFolder); + + /** + * It is called on the listener when a folder is removed from + * the queue + * + * @param aQType type of the queue + * @param aFolder folder that is removed from the queue + */ + void onFolderRemovedFromQ(in long aQType, in nsIMsgFolder aFolder); + + /** + * It is called on the listener when a message download is successfully started + * + * @param aFolder folder in which the download is started + * @param aNumberOfMessages number of the messages that will be downloaded + * @param aTotalPending total number of messages waiting to be downloaded + */ + void onDownloadStarted(in nsIMsgFolder aFolder, in unsigned long aNumberOfMessages, + in unsigned long aTotalPending); + /** + * It is called on the listener when a message download on the given folder + * is completed + */ + void onDownloadCompleted(in nsIMsgFolder aFolder); + + /** + * It is called on the listener when an error occurs during the message download + */ + void onDownloadError(in nsIMsgFolder aFolder); + + /* + * Auto-Sync manager is running or waiting for idle + */ + void onStateChanged(in boolean aRunning); + + /** + * It is called on the listener after the auto-sync manager starts to process + * existing headers of the given folder to find missing message bodies + * (mostly for debugging purposes) + */ + void onDiscoveryQProcessed(in nsIMsgFolder aFolder, in unsigned long aNumberOfHdrsProcessed, + in unsigned long aLeftToProcess); + + /** + * It is called on the listener after the auto-sync manager updates the given folder + * (mostly for debugging purposes) + */ + void onAutoSyncInitiated(in nsIMsgFolder aFolder); +}; + + +[scriptable, uuid(7fe0b48e-f5d8-4747-beb7-888c9cced3a5)] +interface nsIAutoSyncManager : nsISupports { + + /** + * Download models + */ + const long dmParallel = 0; + const long dmChained = 1; + + /** + * Suggested minimum grouping size in bytes for message downloads. + * Setting this attribute to 0 resets its value to the + * hardcoded default. + */ + attribute unsigned long groupSize; + + /** + * Active strategy function to prioritize + * messages in the download queue + */ + attribute nsIAutoSyncMsgStrategy msgStrategy; + + /** + * Active strategy function to prioritize + * folders in the download queue + */ + attribute nsIAutoSyncFolderStrategy folderStrategy; + + /** + * Adds a listener to notify about auto-sync events + */ + void addListener(in nsIAutoSyncMgrListener aListener); + + /** + * Removes the listener from notification list + */ + void removeListener(in nsIAutoSyncMgrListener aListener); + + /** + * Tests the given message to make sure that whether + * it fits the download criteria or not + */ + boolean doesMsgFitDownloadCriteria(in nsIMsgDBHdr aMsgHdr); + + /** + * Called by the nsAutoSyncState object when the download + * queue is changed. Given interface is already addref'd. + */ + void onDownloadQChanged(in nsIAutoSyncState aAutoSyncStateObj); + + /** + * Called by the nsAutoSyncState object when the download + * is started. Given interface is already addref'd. + */ + void onDownloadStarted(in nsIAutoSyncState aAutoSyncStateObj, in nsresult aStartCode); + + /** + * Called by the nsAutoSyncState object when the download + * completed. Given interface is already addref'd. + */ + void onDownloadCompleted(in nsIAutoSyncState aAutoSyncStateObj, in nsresult aExitCode); + + /** + * Number of elements in the discovery queue. + * @see nsAutoSyncManager.h for details + */ + readonly attribute unsigned long discoveryQLength; + + /** + * Number of elements in the update queue. + * @see nsAutoSyncManager.h for details + */ + readonly attribute unsigned long updateQLength; + + /** + * Number of elements in the download queue (a.k.a priority queue). + * @see nsAutoSyncManager.h for details + */ + readonly attribute unsigned long downloadQLength; + + /** + * Active download model; Chained (serial), or Parallel + */ + attribute long downloadModel; + + /** + * The imap folder corresponding to aAutoSyncState has had a message + * added to it. Autosync may want to add this folder to the update q. + * + * @param aAutoSyncState state obj for folder needing updating + */ + void onFolderHasPendingMsgs(in nsIAutoSyncState aAutoSyncState); + + /// Pause autosync (e.g., we're downloading for offline). + void pause(); + + /// Resume normal autosync activities (e.g., we've come back online). + void resume(); +}; + +%{C++ +#define NS_AUTOSYNCMANAGER_CID \ +{ /* C358C568-47B2-42b2-8146-3C0F8D1FAD6E */ \ + 0xc358c568, 0x47b2, 0x42b2, \ + { 0x81, 0x46, 0x3c, 0xf, 0x8d, 0x1f, 0xad, 0x6e }} +#define NS_AUTOSYNCMANAGER_CLASSNAME \ + "Auto-Sync Manager" +#define NS_AUTOSYNCMANAGER_CONTRACTID \ + "@mozilla.org/imap/autosyncmgr;1" +%} diff --git a/comm/mailnews/imap/public/nsIAutoSyncMsgStrategy.idl b/comm/mailnews/imap/public/nsIAutoSyncMsgStrategy.idl new file mode 100644 index 0000000000..773c62426d --- /dev/null +++ b/comm/mailnews/imap/public/nsIAutoSyncMsgStrategy.idl @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIMsgFolder; +interface nsIMsgDBHdr; + +typedef long nsAutoSyncStrategyDecisionType; + +[scriptable,uuid(0365bec5-3753-43c2-b13e-441747815f37)] +interface nsAutoSyncStrategyDecisions : nsISupports +{ + /// same priority + const nsAutoSyncStrategyDecisionType Same = 0x00000001; + /// higher priority + const nsAutoSyncStrategyDecisionType Higher = 0x00000002; + /// lower priority + const nsAutoSyncStrategyDecisionType Lower = 0x00000004; +}; + +[scriptable, uuid(9cb4baff-3112-4cf8-8463-f81b0aa78f93)] +interface nsIAutoSyncMsgStrategy : nsISupports { + + /** + * Returns a relative-priority for the second message by comparing with the first message. + */ + nsAutoSyncStrategyDecisionType sort(in nsIMsgFolder aFolder, in nsIMsgDBHdr aMsgHdr1, in nsIMsgDBHdr aMsgHdr2); + + /** + * Tests whether the given message should be excluded or not. + */ + boolean isExcluded(in nsIMsgFolder aFolder, in nsIMsgDBHdr aMsgHdr); +}; diff --git a/comm/mailnews/imap/public/nsIAutoSyncState.idl b/comm/mailnews/imap/public/nsIAutoSyncState.idl new file mode 100644 index 0000000000..7d2c155435 --- /dev/null +++ b/comm/mailnews/imap/public/nsIAutoSyncState.idl @@ -0,0 +1,157 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIMsgFolder; +interface nsIMsgDBHdr; + +[scriptable, uuid(7512f927-b8f0-48c4-b101-03e859e61281)] +interface nsIAutoSyncState : nsISupports { + + /** + * Auto-Sync states. + * + * ***WARNING***: If you change these, be sure to update stateStrings in + * nsAutoSyncState.cpp. If you do not, out-of-bounds memory accesses may + * happen. + */ + + /** + * Initial state. Returned to after new message bodies downloaded or if + * it is determined that no body fetches are actually needed. The next state + * is typically stStatusIssued but a transition directly to stUpdateNeeded + * may also occur. + */ + const long stCompletedIdle = 0; + + /** + * STATUS issued (URL "folderstatus"). Will check to see if new messages + * are present. If new messages are detected, the next state is + * stUpdateIssued; otherwise, a return to stCompleteIdle occurs. The previous + * state is always stCompleteIdle. + */ + const long stStatusIssued = 1; + + /** + * Occurs when imap APPEND and sometimes COPY or MOVE imap commands add + * messages that may not be detected in autosync stStatusIssued state. These + * are the "pending" messages referred to in the autosync code. + * stUpdateIssued state (URL "select") will occur next. + * The previous state is always stCompleteIdle, skipping stStatusIssued. + */ + const long stUpdateNeeded = 2; + + /** + * Update issued (URL "select"). Will figure out if there are any bodies to + * download after new headers are fetched and, if so, move to state + * stReadyToDownload. Otherwise, returns to stCompleteIdle. + */ + const long stUpdateIssued = 3; + + /** + * Preparing to download the next group of message bodies then move to + * stDownloadInProgress + */ + const long stReadyToDownload = 4; + + /** + * Fetch bodies issued (URL "fetch"). Group of message bodies download in + * progress. If more are needed, next state is stReadyToDownload; otherwise, + * when all are received, the next state is stCompleteIdle. + */ + const long stDownloadInProgress = 5; + + /** + * Puts the download queue offset to its previous position. + */ + void rollback(); + + /** + * Clears the download queue. Resets the offsets. + */ + void resetDownloadQ(); + + /** + * Rollbacks the offset to the previous position and + * changes the state to ready-to-download. + */ + void tryCurrentGroupAgain(in unsigned long aRetryCount); + + /** + * Resets the retry counter. + */ + void resetRetryCounter(); + + /** + * Tests whether the given folder has the same imap server. + */ + boolean isSibling(in nsIAutoSyncState aAnotherStateObj); + + /** + * Update the folder to find new message headers to download + */ + void updateFolder(); + + /** + * Downloads the bodies of the given messages from the server. + */ + void downloadMessagesForOffline(in Array<nsIMsgDBHdr> aMessageList); + + /** + * Returns an array containing the nsIMsgDBHdrs of the messages that will + * be downloaded next. + * + * @param aSuggestedGroupSizeLimit suggested size per group in bytes + * @param aActualGroupSize total size of the messages in bytes in the group + */ + Array<nsIMsgDBHdr> getNextGroupOfMessages(in unsigned long aSuggestedGroupSizeLimit, + out unsigned long aActualGroupSize); + + /** + * Iterates through the existing headers of the folder to find + * the messages not downloaded yet. + * + * @param aNumberOfHeadersToProcess number of headers to be processed + * at this pass + * + * @return the number of headers left to process + */ + unsigned long processExistingHeaders(in unsigned long aNumberOfHeadersToProcess); + + /** + * Tests whether the download queue is empty. + */ + boolean isDownloadQEmpty(); + + /** + * Last time the existing headers are completely processed. + */ + [noscript]readonly attribute PRTime lastSyncTime; + + /** + * Last time the owner folder is updated. + */ + [noscript]attribute PRTime lastUpdateTime; + + /** + * Download operation state. + */ + attribute long state; + + /** + * Number of messages waiting to be downloaded. + */ + readonly attribute long pendingMessageCount; + + /** + * Total number of messages in the download queue. + */ + readonly attribute long totalMessageCount; + + /** + * The folder this auto-sync object is related to. + */ + readonly attribute nsIMsgFolder ownerFolder; +}; diff --git a/comm/mailnews/imap/public/nsIImapFlagAndUidState.idl b/comm/mailnews/imap/public/nsIImapFlagAndUidState.idl new file mode 100644 index 0000000000..a65d1a81c2 --- /dev/null +++ b/comm/mailnews/imap/public/nsIImapFlagAndUidState.idl @@ -0,0 +1,86 @@ +/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +[scriptable, uuid(360848be-f694-40db-80ef-793a2c43ddcb)] +interface nsIImapFlagAndUidState : nsISupports +{ + readonly attribute long numberOfMessages; + readonly attribute long numberOfRecentMessages; + + /** + * If a full update, the total number of deleted messages + * in the folder; if a partial update, the number of deleted + * messages in the partial update + **/ + readonly attribute long numberOfDeletedMessages; + + /** + * If this is true, instead of fetching 1:* (FLAGS), and putting all + * UIDs and flags in the array, we only fetched the uids and flags + * that changed since the last time we were selected on this folder. + * This means we have a sparse array, and should not assume missing + * UIDs have been deleted. + **/ + readonly attribute boolean partialUIDFetch; + + /** + * Set of flags the server supports storing per message. See nsImapCore.h + * for the set of flags. + */ + readonly attribute unsigned short supportedUserFlags; + + /** + * Check if a UID is in the state. + * @param uid - The message UID. + * @returns True if UID is in the state. + */ + boolean hasMessage(in unsigned long uid); + + /** + * OR's the passed in flags with the previous flags because we want to + * accumulate the FLAGS and PERMANENTFLAGS response. + * + * @param aFlags - flags to OR with current flags. + */ + void orSupportedUserFlags(in unsigned short aFlags); + + unsigned long getUidOfMessage(in long zeroBasedIndex); + unsigned short getMessageFlags(in long zeroBasedIndex); + void setMessageFlags(in long zeroBasedIndex, in unsigned short flags); + void expungeByIndex(in unsigned long zeroBasedIndex); + void addUidFlagPair(in unsigned long uid, in unsigned short flags, in unsigned long zeroBasedIndex); + void addUidCustomFlagPair(in unsigned long uid, in string customFlag); + /** + * Get the message flags by the message UID. + * @param uid - The message UID. + * @returns The message flags. + */ + unsigned short getMessageFlagsByUid(in unsigned long uid); + string getCustomFlags(in unsigned long uid); // returns space-separated keywords + void reset(); + void clearCustomFlags(in unsigned long uid); + /** + * Adds custom attributes to a hash table for the purpose of storing them + * them. + * @param aUid UID of the associated msg + * @param aCustomAttributeName Name of the custom attribute value + * @param aCustomAttributeValue Value of the attribute, + */ + void setCustomAttribute(in unsigned long aUid, + in ACString aCustomAttributeName, + in ACString aCustomAttributeValue); + + /** + * Gets the custom attributes from the hash table where they were stored earlier + * them. + * @param aUid UID of the associated msg + * @param aCustomAttributeName Name of the custom attribute value + * @param aCustomAttributeValue Value of the attribute, + */ + ACString getCustomAttribute(in unsigned long aUid, + in ACString aCustomAttributeName); +}; diff --git a/comm/mailnews/imap/public/nsIImapHeaderXferInfo.idl b/comm/mailnews/imap/public/nsIImapHeaderXferInfo.idl new file mode 100644 index 0000000000..74718e056e --- /dev/null +++ b/comm/mailnews/imap/public/nsIImapHeaderXferInfo.idl @@ -0,0 +1,22 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +#include "MailNewsTypes2.idl" + +[scriptable, uuid(38f8f784-b092-11d6-ba4b-00108335942a)] +interface nsIImapHeaderInfo : nsISupports { + attribute nsMsgKey msgUid; + attribute long msgSize; + readonly attribute ACString msgHdrs; + void cacheLine(in string line, in unsigned long uid); + void resetCache(); +}; + +[scriptable, uuid(f0842eda-af29-4ecd-82e1-fba91bd65d66)] +interface nsIImapHeaderXferInfo : nsISupports { + readonly attribute long numHeaders; + nsIImapHeaderInfo getHeader(in long hdrIndex); +}; diff --git a/comm/mailnews/imap/public/nsIImapHostSessionList.h b/comm/mailnews/imap/public/nsIImapHostSessionList.h new file mode 100644 index 0000000000..456bbdf7dc --- /dev/null +++ b/comm/mailnews/imap/public/nsIImapHostSessionList.h @@ -0,0 +1,129 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef _nsIImapHostSessionList_H_ +#define _nsIImapHostSessionList_H_ + +#include "nsISupports.h" +#include "nsImapCore.h" +#include "../src/nsImapNamespace.h" + +class nsIImapIncomingServer; + +// f4d89e3e-77da-492c-962b-7835f0742c22 +#define NS_IIMAPHOSTSESSIONLIST_IID \ + { \ + 0xf4d89e3e, 0x77da, 0x492c, { \ + 0x96, 0x2b, 0x78, 0x35, 0xf0, 0x74, 0x2c, 0x22 \ + } \ + } + +// this is an interface to a linked list of host info's +class nsIImapHostSessionList : public nsISupports { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_IIMAPHOSTSESSIONLIST_IID) + + // Host List + NS_IMETHOD AddHostToList(const char* serverKey, + nsIImapIncomingServer* server) = 0; + NS_IMETHOD ResetAll() = 0; + + // Capabilities + NS_IMETHOD GetHostHasAdminURL(const char* serverKey, bool& result) = 0; + NS_IMETHOD SetHostHasAdminURL(const char* serverKey, bool hasAdminUrl) = 0; + // Subscription + NS_IMETHOD GetHostIsUsingSubscription(const char* serverKey, + bool& result) = 0; + NS_IMETHOD SetHostIsUsingSubscription(const char* serverKey, + bool usingSubscription) = 0; + + // Passwords + NS_IMETHOD GetPasswordForHost(const char* serverKey, nsString& result) = 0; + NS_IMETHOD SetPasswordForHost(const char* serverKey, + const nsAString& password) = 0; + NS_IMETHOD GetPasswordVerifiedOnline(const char* serverKey, bool& result) = 0; + NS_IMETHOD SetPasswordVerifiedOnline(const char* serverKey) = 0; + + // OnlineDir + NS_IMETHOD GetOnlineDirForHost(const char* serverKey, nsString& result) = 0; + NS_IMETHOD SetOnlineDirForHost(const char* serverKey, + const char* onlineDir) = 0; + + // Delete is move to trash folder + NS_IMETHOD GetDeleteIsMoveToTrashForHost(const char* serverKey, + bool& result) = 0; + NS_IMETHOD SetDeleteIsMoveToTrashForHost(const char* serverKey, + bool isMoveToTrash) = 0; + NS_IMETHOD GetShowDeletedMessagesForHost(const char* serverKey, + bool& result) = 0; + + NS_IMETHOD SetShowDeletedMessagesForHost(const char* serverKey, + bool showDeletedMessages) = 0; + + // Get namespaces + NS_IMETHOD GetGotNamespacesForHost(const char* serverKey, bool& result) = 0; + NS_IMETHOD SetGotNamespacesForHost(const char* serverKey, + bool gotNamespaces) = 0; + + // Folders + NS_IMETHOD SetHaveWeEverDiscoveredFoldersForHost(const char* serverKey, + bool discovered) = 0; + NS_IMETHOD GetHaveWeEverDiscoveredFoldersForHost(const char* serverKey, + bool& result) = 0; + NS_IMETHOD SetDiscoveryForHostInProgress(const char* serverKey, + bool inProgress) = 0; + NS_IMETHOD GetDiscoveryForHostInProgress(const char* serverKey, + bool& result) = 0; + + // Trash Folder + NS_IMETHOD SetOnlineTrashFolderExistsForHost(const char* serverKey, + bool exists) = 0; + NS_IMETHOD GetOnlineTrashFolderExistsForHost(const char* serverKey, + bool& result) = 0; + + // INBOX + NS_IMETHOD GetOnlineInboxPathForHost(const char* serverKey, + nsString& result) = 0; + NS_IMETHOD GetShouldAlwaysListInboxForHost(const char* serverKey, + bool& result) = 0; + NS_IMETHOD SetShouldAlwaysListInboxForHost(const char* serverKey, + bool shouldList) = 0; + + // Namespaces + NS_IMETHOD GetNamespaceForMailboxForHost(const char* serverKey, + const char* mailbox_name, + nsImapNamespace*& result) = 0; + NS_IMETHOD SetNamespaceFromPrefForHost(const char* serverKey, + const char* namespacePref, + EIMAPNamespaceType type) = 0; + NS_IMETHOD AddNewNamespaceForHost(const char* serverKey, + nsImapNamespace* ns) = 0; + NS_IMETHOD ClearServerAdvertisedNamespacesForHost(const char* serverKey) = 0; + NS_IMETHOD ClearPrefsNamespacesForHost(const char* serverKey) = 0; + NS_IMETHOD GetDefaultNamespaceOfTypeForHost(const char* serverKey, + EIMAPNamespaceType type, + nsImapNamespace*& result) = 0; + NS_IMETHOD SetNamespacesOverridableForHost(const char* serverKey, + bool overridable) = 0; + NS_IMETHOD GetNamespacesOverridableForHost(const char* serverKey, + bool& result) = 0; + NS_IMETHOD GetNumberOfNamespacesForHost(const char* serverKey, + uint32_t& result) = 0; + NS_IMETHOD GetNamespaceNumberForHost(const char* serverKey, int32_t n, + nsImapNamespace*& result) = 0; + // ### dmb hoo boy, how are we going to do this? + NS_IMETHOD CommitNamespacesForHost(nsIImapIncomingServer* server) = 0; + NS_IMETHOD FlushUncommittedNamespacesForHost(const char* serverKey, + bool& result) = 0; + + // Hierarchy Delimiters + NS_IMETHOD SetNamespaceHierarchyDelimiterFromMailboxForHost( + const char* serverKey, const char* boxName, char delimiter) = 0; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(nsIImapHostSessionList, + NS_IIMAPHOSTSESSIONLIST_IID) + +#endif diff --git a/comm/mailnews/imap/public/nsIImapIncomingServer.idl b/comm/mailnews/imap/public/nsIImapIncomingServer.idl new file mode 100644 index 0000000000..25ff1c59c0 --- /dev/null +++ b/comm/mailnews/imap/public/nsIImapIncomingServer.idl @@ -0,0 +1,114 @@ +/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIURI; +interface nsIImapUrl; +interface nsIImapProtocol; +interface nsIMsgFolder; +interface nsIMsgWindow; + +typedef long nsMsgImapDeleteModel; + +[scriptable, uuid(bbfc33de-fe89-11d3-a564-0060b0fc04b7)] +interface nsMsgImapDeleteModels : nsISupports +{ + const long IMAPDelete = 0; /* delete with a big red x */ + const long MoveToTrash = 1; /* delete moves message to the trash */ + const long DeleteNoTrash = 2; /* delete is shift delete - don't create or use trash */ +}; + +[scriptable, uuid(ea6a0765-07b8-40df-924c-9004ed707251)] +interface nsIImapIncomingServer : nsISupports { + + attribute long maximumConnectionsNumber; + attribute boolean forceSelect; + attribute long timeOutLimits; + attribute AUTF8String adminUrl; + attribute ACString serverDirectory; + /// RFC 2971 ID response stored as a pref + attribute AUTF8String serverIDPref; + attribute boolean cleanupInboxOnExit; + attribute nsMsgImapDeleteModel deleteModel; + attribute boolean dualUseFolders; + attribute ACString personalNamespace; + attribute ACString publicNamespace; + attribute ACString otherUsersNamespace; + attribute boolean offlineDownload; + attribute boolean overrideNamespaces; + attribute boolean usingSubscription; + attribute AUTF8String manageMailAccountUrl; + attribute boolean fetchByChunks; + attribute boolean sendID; + attribute boolean isAOLServer; + attribute boolean useIdle; + attribute boolean checkAllFoldersForNew; + + /// Is this a GMail Server? + attribute boolean isGMailServer; + + /** + * See IMAP RFC 4551 + **/ + attribute boolean useCondStore; + + /** + * See IMAP RFC 4978 + */ + attribute boolean useCompressDeflate; + + /** + * This contains a folder path, for example INBOX/Trash. Note that the + * account manager sets this attribute to the path of the trash folder the + * user has chosen. + */ + attribute AString trashFolderName; + + attribute boolean downloadBodiesOnGetNewMail; + attribute boolean autoSyncOfflineStores; + + /// Max age of messages we will autosync to, or keep in offline store. + attribute long autoSyncMaxAgeDays; + + /** + * See IMAP RFC 6855 + */ + attribute boolean allowUTF8Accept; + + void GetImapConnectionAndLoadUrl(in nsIImapUrl aImapUrl, + in nsISupports aConsumer); + + void RemoveConnection(in nsIImapProtocol aImapConnection); + void ResetNamespaceReferences(); + void pseudoInterruptMsgLoad(in nsIMsgFolder aImapFolder, in nsIMsgWindow aMsgWindow, out boolean interrupted); + void ResetConnection(in AUTF8String folderName); + void CloseConnectionForFolder(in nsIMsgFolder aMsgFolder); + void reDiscoverAllFolders(); + nsIURI subscribeToFolder(in AString name, in boolean subscribe); + void GetNewMessagesForNonInboxFolders(in nsIMsgFolder aRootFolder, + in nsIMsgWindow aWindow, + in boolean forceAllFolders, + in boolean performingBiff); + unsigned long long getCapability(); + + /** + * Get the password from the nsIMsgIncomingServer. May prompt the user + * if there's no password in the password manager or cached in the + * server object. + * @param aWindow msgWindow to associate the password prompt with + * @return Password string. + * @exception NS_ERROR_FAILURE The password could not be obtained. + * @note NS_MSG_PASSWORD_PROMPT_CANCELLED is a success code that is returned + * if the prompt was presented to the user but the user cancelled the + * prompt. + */ + AString PromptPassword(in nsIMsgWindow aWindow); + attribute boolean doingLsub; + + ACString getUriWithNamespacePrefixIfNecessary(in long namespaceType, in AUTF8String originalUri); + attribute boolean shuttingDown; + attribute boolean utf8AcceptEnabled; +}; diff --git a/comm/mailnews/imap/public/nsIImapMailFolderSink.idl b/comm/mailnews/imap/public/nsIImapMailFolderSink.idl new file mode 100644 index 0000000000..6868682640 --- /dev/null +++ b/comm/mailnews/imap/public/nsIImapMailFolderSink.idl @@ -0,0 +1,119 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +#include "MailNewsTypes2.idl" + +#include "nsIImapProtocol.idl" +#include "nsIMailboxSpec.idl" + +interface nsIMsgMailNewsUrl; +interface nsIImapMockChannel; +interface nsIImapHeaderXferInfo; + +typedef long ImapOnlineCopyState; + +[scriptable, uuid(5f7484b0-68b4-11d3-a53e-0060b0fc04b7)] +interface ImapOnlineCopyStateType : nsISupports +{ + const long kInProgress = 0; + const long kSuccessfulCopy = 1; + const long kSuccessfulMove = 2; + const long kSuccessfulDelete = 3; + const long kFailedDelete = 4; + const long kReadyForAppendData = 5; + const long kFailedAppend = 6; + const long kInterruptedState = 7; + const long kFailedCopy = 8; + const long kFailedMove = 9; +}; + +/** + * nsIImapMailFolderSink provides a way for the IMAP system to communicate + * with the local folder representation. + * + * The IMAP system could poke folders directly, but going through this + * interface has a couple of benefits: + * + * 1. It better defines the public coupling between the two systems. + * 2. It's easier to wrap with a proxy class so the IMAP system can safely + * call the methods across thread boundaries (see ImapMailFolderSinkProxy). + */ +[scriptable, uuid(525e1278-a39d-46d6-9dbc-b48c7e1d4faa)] +interface nsIImapMailFolderSink : nsISupports { + attribute boolean folderNeedsACLListed; + attribute boolean folderNeedsSubscribing; + attribute boolean folderNeedsAdded; + attribute unsigned long aclFlags; + attribute long uidValidity; + /** + * Whether we have asked the server for this folder's quota information. + * If the server supports quotas, this occurs when the folder is opened. + */ + attribute boolean folderQuotaCommandIssued; + + /** + * Set FolderQuotaData information + * @param aAction Invalidate, store or validate the quota data. + * Remaining params are relevant only for store. + * @param aFolderQuotaRoot The IMAP quotaroot and resource names for this + * folder separated by a slash as obtained from the + * GETQUOTAROOT IMAP command response. + * @param aFolderQuotaUsage Amount of resourse in use, in KB for STORAGE + * resource. + * @param aFolderQuotaLimit Maximum usage allowed for this resource. + **/ + void setFolderQuotaData( + in unsigned long aAction, in ACString aFolderQuotaRoot, + in unsigned long long aFolderQuotaUsed, in unsigned long long aFolderQuotaLimit); + + /// Should we download all the rfc822 headers of messages, instead of subset. + readonly attribute boolean shouldDownloadAllHeaders; + readonly attribute char onlineDelimiter; + void OnNewIdleMessages(); + // Tell mail master about the newly selected mailbox + void UpdateImapMailboxInfo(in nsIImapProtocol aProtocol, + in nsIMailboxSpec aSpec); + void UpdateImapMailboxStatus(in nsIImapProtocol aProtocol, + in nsIMailboxSpec aSpec); + /** + * Used when downloading headers in chunks. + * @param aSpec Mailbox spec of folder we're downloading headers for. + * @returns true if more to download, false otherwise. + * @returns total count of headers to download (across all chunks) + * @returns an array of msg keys to download, array size is this chunk's size + */ + void getMsgHdrsToDownload(out boolean aMore, out long aTotalCount, + out Array<nsMsgKey> aKeys); + void parseMsgHdrs(in nsIImapProtocol aProtocol, in nsIImapHeaderXferInfo aHdrXferInfo); + void AbortHeaderParseStream(in nsIImapProtocol aProtocol) ; + + void OnlineCopyCompleted(in nsIImapProtocol aProtocol, in ImapOnlineCopyState aCopyState); + void StartMessage(in nsIMsgMailNewsUrl aUrl); + void EndMessage(in nsIMsgMailNewsUrl aUrl, in nsMsgKey uidOfMessage); + + void NotifySearchHit(in nsIMsgMailNewsUrl aUrl, in string hitLine); + + void copyNextStreamMessage(in boolean copySucceeded, in nsISupports copyState); + void closeMockChannel(in nsIImapMockChannel aChannel); + void setUrlState(in nsIImapProtocol aProtocol, in nsIMsgMailNewsUrl aUrl, + in boolean isRunning, in boolean aSuspend, + in nsresult status); + void releaseUrlCacheEntry(in nsIMsgMailNewsUrl aUrl); + + void headerFetchCompleted(in nsIImapProtocol aProtocol); + void setBiffStateAndUpdate(in long biffState); + void progressStatusString(in nsIImapProtocol aProtocol, in string aMsgId, in wstring extraInfo); + void percentProgress(in nsIImapProtocol aProtocol, + in ACString aFmtStringName, in AString aMailboxName, + in long long aCurrentProgress, in long long aMaxProgressProgressInfo); + + void clearFolderRights(); + void setCopyResponseUid(in string msgIdString, + in nsIImapUrl aUrl); + void setAppendMsgUid(in nsMsgKey newKey, + in nsIImapUrl aUrl); + ACString getMessageId(in nsIImapUrl aUrl); +}; diff --git a/comm/mailnews/imap/public/nsIImapMessageSink.idl b/comm/mailnews/imap/public/nsIImapMessageSink.idl new file mode 100644 index 0000000000..858522856c --- /dev/null +++ b/comm/mailnews/imap/public/nsIImapMessageSink.idl @@ -0,0 +1,89 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +#include "MailNewsTypes2.idl" +#include "nsIImapUrl.idl" + +interface nsIFile; + +[scriptable, uuid(6ffb6a92-e43a-405f-92ea-92cf81a5e17b)] + +/** + * nsIImapMessageSink provides a way for the IMAP system to exchange + * message-related information with the local folder representation. + * + * The IMAP system could poke folders directly, but going through this + * interface has a couple of benefits: + * + * 1. It better defines the public coupling between the two systems. + * 2. It's easier to wrap with a proxy class so the IMAP system can safely + * call the methods across thread boundaries (see ImapMessageSinkProxy). + */ +interface nsIImapMessageSink : nsISupports { + // set up message download output stream + void setupMsgWriteStream(in nsIFile aFile, in boolean aAppendDummyEnvelope); + + /** + * 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); + + /** + * Notify the backend that the imap protocol is done downloading a message + * + * @param aUidOfMsg IMAP UID of the fetched message. + * @param aMarkMsgRead Set the SEEN flag on the message. + * @param aImapUrl IMAP Url used to fetch the message. + * @param aUpdatedMessageSize if this parameter is not -1, the stored size of the message + * should be set to this value to reflect the actual size of + * the downloaded message. + */ + void normalEndMsgWriteStream(in nsMsgKey aUidOfMessage, + in boolean aMarkMsgRead, in nsIImapUrl aImapUrl, + in long aUpdatedMessageSize); + + void abortMsgWriteStream(); + + 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 aKeywords keywords for the message + * @param aMessageKey - The UID of the message that changed + * @param aHighestModSeq - The highest mod seq the parser has seen + * for this folder + **/ + void notifyMessageFlags(in unsigned long aFlags, in ACString aKeywords, + in nsMsgKey aMessageKey, + in unsigned long long aHighestModSeq); + + void notifyMessageDeleted(in string aOnlineFolderName,in boolean aDeleteAllMsgs,in string aMsgIdString); + + void getMessageSizeFromDB(in string aId, out unsigned long aSize); + + /** + * For a message stored in a file, get the message metadata needed to copy + * that message to an imap folder + * + * @param aRunningUrl message URL + * @param aDate message date + * @param aKeywords message custom keywords (if supported by the server), + * including messages tags and junk status + * + * @return message flags + */ + unsigned long getCurMoveCopyMessageInfo(in nsIImapUrl aRunningUrl, + out PRTime aDate, out ACString aKeywords); +}; diff --git a/comm/mailnews/imap/public/nsIImapMockChannel.idl b/comm/mailnews/imap/public/nsIImapMockChannel.idl new file mode 100644 index 0000000000..cb624f314f --- /dev/null +++ b/comm/mailnews/imap/public/nsIImapMockChannel.idl @@ -0,0 +1,49 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + +/* + Because imap protocol connections (which are channels) run through a cache, + it isn't always the case that when you want to run a url, you actually get + a connection back right away. Often times, the url goes into a queue until + a connection becomes available. + + Unfortunately, if we want to be a truly pluggable protocol with necko, necko + requires the ability to get a channel back right away when it wants to run + a url. It doesn't let you wait until an imap connection becomes available. + + So I've created the notion of a "mock channel". This mock channel is what + gets returned to necko (or other callers) when they ask the imap service + for a new channel for a url. The mock channel has "mock" implementations + of nsIChannel. Eventually, when we actually assign the url to a real + channel, we set the real channel on the mock channel. From that point forward, + the mock channel forwards channel calls directly to the real channel. + + In short, this class is how I'm solving the problem where necko wants + a channel back as soon as they ask for when with the fact that it + may be a while until the url is loaded into a connection. + */ + +#include "nsISupports.idl" +#include "nsIChannel.idl" + +interface nsIStreamListener; +interface nsIProgressEventSink; +interface nsIURI; +interface nsIImapProtocol; + +[scriptable, uuid(e0178cd5-d37b-4bde-9ab8-752083536225)] +interface nsIImapMockChannel : nsIChannel +{ + attribute nsIProgressEventSink progressEventSink; + void GetChannelListener(out nsIStreamListener aChannelListener); + void Close(); + void setImapProtocol(in nsIImapProtocol aProtocol); + [noscript] void setSecurityInfo(in nsITransportSecurityInfo securityInfo); + + void setURI(in nsIURI uri); + void readFromImapConnection(); + attribute boolean writingToCache; +}; diff --git a/comm/mailnews/imap/public/nsIImapOfflineSync.idl b/comm/mailnews/imap/public/nsIImapOfflineSync.idl new file mode 100644 index 0000000000..c7f55cfa9c --- /dev/null +++ b/comm/mailnews/imap/public/nsIImapOfflineSync.idl @@ -0,0 +1,19 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIMsgFolder; +interface nsIMsgWindow; +interface nsIUrlListener; + +[scriptable, uuid(ffb683c5-f2c5-490a-b569-2f6b0de0a241)] +interface nsIImapOfflineSync : nsISupports { + void init(in nsIMsgWindow window, + in nsIUrlListener listener, + in nsIMsgFolder folder, + in bool isPseudoOffline); + + void processNextOperation(); +}; diff --git a/comm/mailnews/imap/public/nsIImapProtocol.idl b/comm/mailnews/imap/public/nsIImapProtocol.idl new file mode 100644 index 0000000000..7f7ea50720 --- /dev/null +++ b/comm/mailnews/imap/public/nsIImapProtocol.idl @@ -0,0 +1,90 @@ +/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +#include "MailNewsTypes2.idl" // for nsMsgKey + +interface nsIURI; +interface nsIImapUrl; +interface nsIImapIncomingServer; +interface nsIMsgFolder; +interface nsIMsgWindow; +interface nsIImapFlagAndUidState; + +[ptr] native nsIImapHostSessionList(nsIImapHostSessionList); +%{C++ +class nsIImapHostSessionList; +%} + +[scriptable, uuid(6ef189e5-8711-4845-9625-d1c74c22c4b5)] +interface nsIImapProtocol : nsISupports { + /** + * Set up this connection to run a URL. + * Called by nsImapIncomingServer to process a queued URL when it spots a + * free connection. + * Because nsImapProtocol is really a connection and doesn't follow the + * usual nsIChannel lifecycle, this function is provided to allow reuse. + * Over and over again. + */ + void LoadImapUrl(in nsIURI aUrl, in nsISupports aConsumer); + + /** + * IsBusy returns true if the connection is currently processing a URL + * and false otherwise. + */ + void IsBusy(out boolean aIsConnectionBusy, + out boolean isInboxConnection); + + /** + * Protocol instance examines the URL, looking at the host name, + * user name and folder the action would be on in order to figure out + * if it can process this URL. I decided to push the semantics about + * whether a connection can handle a URL down into the connection level + * instead of in the connection cache. + */ + void CanHandleUrl(in nsIImapUrl aImapUrl, out boolean aCanRunUrl, + out boolean hasToWait); + + /** + * Initialize a protocol object. + * @param aHostSessionList host session list service + * @param aServer imap server the protocol object will be talking to + */ + void Initialize(in nsIImapHostSessionList aHostSessionList, in nsIImapIncomingServer aServer); + + void NotifyBodysToDownload(in Array<nsMsgKey> keys); + + // methods to get data from the imap parser flag state. + void GetFlagsForUID(in unsigned long uid, out boolean foundIt, out unsigned short flags, out string customFlags); + void GetSupportedUserFlags(out unsigned short flags); + + void GetRunningImapURL(out nsIImapUrl aImapUrl); + + void GetRunningUrl(out nsIURI aUrl); + + readonly attribute nsIImapFlagAndUidState flagAndUidState; + /** + * Tell thread to die - only call from the UI thread + * + * @param aIsSafeToClose false if we're dropping a timed out connection. + */ + void tellThreadToDie(in boolean aIsSafeToClose); + + // Get last active time stamp + void GetLastActiveTimeStamp(out PRTime aTimeStamp); + + void pseudoInterruptMsgLoad(in nsIMsgFolder imapFolder, in nsIMsgWindow aMsgWindow, out boolean interrupted); + + /** + * Produce a pseudo-interrupt to trigger an abort of an imap mssage fetch. + * + * @param aInterrupt true to initiate a pseudo-interrupt; otherwise set false. + */ + void pseudoInterrupt(in boolean aInterrupt); + + void GetSelectedMailboxName(out string folderName); + // Reset folder connection to authenticated state + void ResetToAuthenticatedState(); +}; diff --git a/comm/mailnews/imap/public/nsIImapProtocolSink.idl b/comm/mailnews/imap/public/nsIImapProtocolSink.idl new file mode 100644 index 0000000000..b3cda13d6e --- /dev/null +++ b/comm/mailnews/imap/public/nsIImapProtocolSink.idl @@ -0,0 +1,33 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIMsgWindow; +interface nsIMsgMailNewsUrl; + +/** + * Helper interface that contains operations MUST be proxied + * over UI thread. + */ +[scriptable, uuid(1217cd9d-7678-4026-b323-0d4b45816af0)] +interface nsIImapProtocolSink : nsISupports { + + /** + * Does general cleanup for the imap protocol object. + */ + void closeStreams(); + /** + * Get the msg window associated with a url + * + * @param aUrl url whose msgWindow we want. + * @returns msgWindow associated with url. + */ + nsIMsgWindow getUrlWindow(in nsIMsgMailNewsUrl aUrl); + + /** + * Setup main thread proxies. + */ + void setupMainThreadProxies(); +}; diff --git a/comm/mailnews/imap/public/nsIImapServerSink.idl b/comm/mailnews/imap/public/nsIImapServerSink.idl new file mode 100644 index 0000000000..736487877e --- /dev/null +++ b/comm/mailnews/imap/public/nsIImapServerSink.idl @@ -0,0 +1,178 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +#include "MailNewsTypes2.idl" + +interface nsIMsgWindow; +interface nsIMsgMailNewsUrl; +interface nsIImapProtocol; +interface nsIImapUrl; +interface nsIImapMockChannel; + +/** + * nsIImapServerSink is designed to be used as a proxy to the application's UI + * thread from the running IMAP threads. + */ +[scriptable, uuid(2160c641-e4fa-4bbc-ab8b-d9ba45069027)] +interface nsIImapServerSink : nsISupports { + /** + * Check if the given folder path is a possible IMAP mailbox. + * @param folderPath folder path to check + * @param hierarchyDelimiter IMAP hierarchy delimiter in canonical format, + * i.e., hierarchy delimiter has been replaced + * with '/' + * @param boxFlags IMAP folder flags (for subscription, namespaces etc.) + * @return true if it's a new mailbox + */ + boolean possibleImapMailbox(in AUTF8String folderPath, + in char hierarchyDelimiter, in long boxFlags); + boolean folderNeedsACLInitialized(in AUTF8String folderPath); + void addFolderRights(in AUTF8String folderPath, in ACString userName, in ACString rights); + void refreshFolderRights(in AUTF8String folderPath); + void discoveryDone(); + void onlineFolderDelete(in AUTF8String folderName); + void onlineFolderCreateFailed(in AUTF8String aFolderName); + void onlineFolderRename(in nsIMsgWindow msgWindow, in AUTF8String oldName, in AUTF8String newName); + boolean folderIsNoSelect(in AUTF8String folderName); + void setFolderAdminURL(in AUTF8String folderName, in AUTF8String adminUrl); + boolean folderVerifiedOnline(in AUTF8String folderName); + + void setCapability(in unsigned long long capability); + /// RFC 2971 ID server response + void setServerID(in ACString aServerID); + boolean loadNextQueuedUrl(in nsIImapProtocol protocol); + + /** + * Prepare to retry the given URL. + * @param imapUrl the url we're going to retry + * @return channel to associate with the url. We return this because access + * to the channel should only happen on the ui thread. + */ + nsIImapMockChannel prepareToRetryUrl(in nsIImapUrl imapUrl); + + /** + * Suspend the url. This puts it at the end of the queue. If the queue is + * empty, the url will get resumed immediately. Currently, the plan is + * do this when we have to download a lot of headers in chunks, though we + * could find other uses for it. + * @param imapUrl url to suspend + */ + void suspendUrl(in nsIImapUrl aImapUrl); + + /** + * Retry the given URL. + * @param imapUrl url to retry + * @param channel the channel to associate with the url + */ + void retryUrl(in nsIImapUrl imapUrl, in nsIImapMockChannel channel); + + /** + * If previous URL failed, this gives server chance to abort URLs with same + * mock channel. + */ + void abortQueuedUrls(); + AString getImapStringByName(in string msgName); + /** + * Alerts the user that the login to the IMAP server failed. Asks whether the + * connection should: retry, cancel, or request a new password. + * + * @param aMsgWindow The message window associated with this action (cannot + * be null). + * @return The button pressed. 0 for retry, 1 for cancel, + * 2 for enter a new password. + */ + int32_t promptLoginFailed(in nsIMsgWindow aMsgWindow); + + /** + * Alerts the user with the given string (FE = 'Front End'). + * + * @param aAlertString The string to alert the user with. + * @param aUrl The running url. + */ + void fEAlert(in AString aAlertString, in nsIMsgMailNewsUrl aUrl); + + /** + * Alerts the user with a localized string. It will attempt to fill in + * the hostname into the string if necessary. + * + * @param aMsgName The id of the string to present to the user.. + * @param aUrl The running url. + */ + void fEAlertWithName(in string aMsgName, in nsIMsgMailNewsUrl aUrl); + /** + * Takes a response from the server and prepends it with IMAP_SERVER_SAID + * + * @param aServerString The string to alert the user with. + * @param url The running url. + */ + void fEAlertFromServer(in ACString aServerString, in nsIMsgMailNewsUrl aUrl); + + void commitNamespaces(); + + /** + * Returns a password via the out param, if we were able to prompt for one, + * or had one stored. + * If there is already a password prompt up, we return false, but we + * ask the async prompt service to notify us when we can put up a prompt. + * When that notification is received, we prompt the user and set the + * password on the protocol object, and signal a monitor that the imap + * thread should be waiting on. + * + * rv is NS_MSG_PASSWORD_PROMPT_CANCELLED if the user cancels the + * password prompt. That's not an exception, however. + * + * @param aProtocol imap protocol object requesting the password. + * @param aNewPasswordRequested Forces password prompt immediately + * @param aPassword returns the password, unless we had to prompt or use the, + * login manager and there was already a prompt up. + */ + void asyncGetPassword(in nsIImapProtocol aProtocol, + in boolean aNewPasswordRequested, + out AString aPassword); + + /** + * Returns a password via the out param if password is stored in login mgr. + * If no password is stored, this function returns NS_ERROR_NOT_AVAILABLE. + * This never triggers a password prompt. + * + * @param aPassword returns the stored password or empty string if not stored. + */ + void syncGetPassword(out AString aPassword); + + attribute boolean userAuthenticated; + void setMailServerUrls(in AUTF8String manageMailAccount, in AUTF8String manageLists, in AUTF8String manageFilters); + + /** Used by the imap thread when upgrading from the socketType + * trySTARTTLS. + * @param aSucceeded whether STARTTLS succeeded. If it did, the server + * will set the socket type to alwaysSTARTTLS, otherwise plain. + */ + void UpdateTrySTARTTLSPref(in boolean aSucceeded); + + readonly attribute AUTF8String arbitraryHeaders; + void forgetPassword(); + + readonly attribute boolean showAttachmentsInline; + string cramMD5Hash(in string decodedChallenge, in string key); + /// String to send to the imap server as the login user name. + readonly attribute ACString loginUsername; + /// String to send to the imap server as the user name. + readonly attribute ACString originalUsername; + /// Internal pref key, unique over all servers + readonly attribute ACString serverKey; + /// password for server login + readonly attribute AString serverPassword; + /// remove a connection to the server + void removeServerConnection(in nsIImapProtocol aProtocol); + /// is the imap server shutting down? + readonly attribute boolean serverShuttingDown; + /// reset the connection for a particular folder + void resetServerConnection(in AUTF8String aFolderName); + /// tell the server if listing using lsub command + void setServerDoingLsub(in boolean aDoingLsub); + /// set whether UTF8=ACCEPT enabled or not + void setServerUtf8AcceptEnabled(in boolean aEnabled); +}; diff --git a/comm/mailnews/imap/public/nsIImapService.idl b/comm/mailnews/imap/public/nsIImapService.idl new file mode 100644 index 0000000000..04b2667467 --- /dev/null +++ b/comm/mailnews/imap/public/nsIImapService.idl @@ -0,0 +1,253 @@ +/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//////////////////////////////////////////////////////////////////////////////////////// +// The IMAP Service is an interfaced designed to make building and running imap urls +// easier. Clients typically go to the imap service and ask it do things such as: +// get new mail, etc.... +// +// Oh and in case you couldn't tell by the name, the imap service is a service! and you +// should go through the service manager to obtain an instance of it. +//////////////////////////////////////////////////////////////////////////////////////// + +#include "nsISupports.idl" +#include "nsIImapUrl.idl" + +interface nsIImapMessageSink; +interface nsIUrlListener; +interface nsIURI; +interface nsIFile; +interface nsIMsgFolder; +interface nsIMsgWindow; +interface nsIMsgMailNewsUrl; +interface nsIImapIncomingServer; +interface nsICacheStorage; + +/** + * Most of the nsIImapService methods are friendly front ends for composing and + * issuing "imap://" protocol operations. Usually a nsImapUrl will be returned. + * This url object is stateful and tracks the issued request. + */ +[scriptable, uuid(aba44b3d-7a0f-4987-8794-96d2de66d966)] +interface nsIImapService : nsISupports +{ + nsIURI selectFolder(in nsIMsgFolder aImapMailFolder, + in nsIUrlListener aUrlListener, + in nsIMsgWindow aMsgWindow); + + /** + * Select the 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 aImapMailFolder the folder to select + * @param aUrlListener url listener, can be null + * @param aMsgWindow msg window url is running in, can be null + * + * @returns the url created to run the lite select in. + */ + nsIURI liteSelectFolder(in nsIMsgFolder aImapMailFolder, + in nsIUrlListener aUrlListener, + in nsIMsgWindow aMsgWindow); + + void addImapFetchToUrl(in nsIMsgMailNewsUrl aURL, + in nsIMsgFolder aImapMailFolder, + in ACString aMessageIdentifierList, + in ACString aAdditionalHeader); + + void fetchMessage(in nsIImapUrl aUrl, + in nsImapState aImapAction, + in nsIMsgFolder aImapMailFolder, + in nsIImapMessageSink aImapMessageSink, + in nsIMsgWindow aMsgWindow, + in nsISupports aConsumer, + in ACString aMessageIdentifierList, + in boolean convertDataToText, + out nsIURI aOutURL); + + void noop(in nsIMsgFolder aImapMailFolder, + in nsIUrlListener aUrlListener, + out nsIURI aURL); + + void getHeaders(in nsIMsgFolder aImapMailFolder, + in nsIUrlListener aUrlListener, + out nsIURI aURL, + in ACString aMessageIdentifierList, + in boolean aMessageIdsAreUID); + + nsIURI getBodyStart(in nsIMsgFolder aImapMailFolder, + in nsIUrlListener aUrlListener, + in ACString aMessageIdentifierList, + in long numBytes); + + /** + * Issue an EXPUNGE on the target folder. + * + * @param aImapMailFolder the folder to expunge + * @param aUrlListener url listener, can be null + * @param aMsgWindow msg window url is running in, can be null + */ + void expunge(in nsIMsgFolder aImapMailFolder, + in nsIUrlListener aUrlListener, + in nsIMsgWindow aMsgWindow); + + /** + * Issue a STATUS on the target folder. + * + * @param aImapMailFolder the folder to expunge + * @param aUrlListener url listener, can be null + * + * @returns the url created to run the status. + */ + nsIURI updateFolderStatus(in nsIMsgFolder aImapMailFolder, + in nsIUrlListener aUrlListener); + + /** + * Verify that we can login. + * + * @param aImapMailFolder - any old imap folder - we just need it to + * set url sinks. + * @param aMsgWindow - nsIMsgWindow to use for notification callbacks. + * @return - the url that we run. + */ + nsIURI verifyLogon(in nsIMsgFolder aImapMailFolder, + in nsIUrlListener aUrlListener, + in nsIMsgWindow aMsgWindow); + + void biff(in nsIMsgFolder aImapMailFolder, + in nsIUrlListener aUrlListener, + out nsIURI aURL, + in unsigned long aUidHighWater); + + void deleteMessages(in nsIMsgFolder aImapMailFolder, + in nsIUrlListener aUrlListener, + out nsIURI aURL, + in ACString aMessageIdentifierList, + in boolean aMessageIdsAreUID); + + void deleteAllMessages(in nsIMsgFolder aImapMailFolder, + in nsIUrlListener aUrlListener); + + void addMessageFlags(in nsIMsgFolder aImapMailFolder, + in nsIUrlListener aUrlListener, + in ACString aMessageIdentifierList, + in imapMessageFlagsType aFlags, + in boolean aMessageIdsAreUID); + + void subtractMessageFlags(in nsIMsgFolder aImapMailFolder, + in nsIUrlListener aUrlListener, + in ACString aMessageIdentifierList, + in imapMessageFlagsType aFlags, + in boolean aMessageIdsAreUID); + + void setMessageFlags(in nsIMsgFolder aImapMailFolder, + in nsIUrlListener aUrlListener, + out nsIURI aURL, + in ACString aMessageIdentifierList, + in imapMessageFlagsType aFlags, + in boolean aMessageIdsAreUID); + + void discoverAllFolders(in nsIMsgFolder aImapMailFolder, + in nsIUrlListener aUrlListener, + in nsIMsgWindow aMsgWindow); + + void discoverAllAndSubscribedFolders(in nsIMsgFolder aImapMailFolder, + in nsIUrlListener aUrlListener, + in nsIMsgWindow aMsgWindow); + + void discoverChildren(in nsIMsgFolder aImapMailFolder, + in nsIUrlListener aUrlListener, + in ACString folderPath); + + void onlineMessageCopy(in nsIMsgFolder aSrcFolder, + in ACString aMessageIds, + in nsIMsgFolder aDstFolder, + in boolean aIdsAreUids, + in boolean aIsMove, + in nsIUrlListener aUrlListener, + out nsIURI aURL, + in nsISupports aCopyState, + in nsIMsgWindow aWindow); + + + void appendMessageFromFile(in nsIFile aFile, + in nsIMsgFolder aDstFolder, + in ACString aMessageId, + in boolean idsAreUids, + in boolean aInSelectedState, + in nsIUrlListener aUrlListener, + in nsISupports aCopyState, + in nsIMsgWindow aMsgWindow); + + void downloadMessagesForOffline(in ACString aMessageIds, in nsIMsgFolder aSrcFolder, + in nsIUrlListener aListener, in nsIMsgWindow aMsgWindow); + + void moveFolder(in nsIMsgFolder aSrcFolder, + in nsIMsgFolder aDstFolder, + in nsIUrlListener aUrlListener, + in nsIMsgWindow msgWindow); + + void renameLeaf(in nsIMsgFolder aSrcFolder, + in AString aLeafName, + in nsIUrlListener aUrlListener, + in nsIMsgWindow msgWindow); + + void deleteFolder(in nsIMsgFolder aFolder, + in nsIUrlListener aUrlListener, + in nsIMsgWindow aMsgWindow); + + nsIURI createFolder(in nsIMsgFolder aParentFolder, + in AString aLeafName, + in nsIUrlListener aUrlListener); + + void listFolder(in nsIMsgFolder aMailFolder, + in nsIUrlListener aUrlListener); + + nsIURI subscribeFolder(in nsIMsgFolder aMailFolder, + in AString mailboxName, + in nsIUrlListener aUrlListener); + + nsIURI unsubscribeFolder(in nsIMsgFolder aMailFolder, + in AString mailboxName, + in nsIUrlListener aUrlListener); + + // this method will first check if the folder exists but is + // not subscribed to, in which case it will subscribe to the folder. + // otherwise, it will try to create the folder. It will try to do this + // with one url. + void ensureFolderExists(in nsIMsgFolder aParentFolder, + in AString aLeafName, + in nsIMsgWindow aMsgWindow, + in nsIUrlListener aUrlListener); + + + nsIURI getFolderAdminUrl(in nsIMsgFolder aMailFolder, + in nsIMsgWindow aMsgWindow, + in nsIUrlListener aUrlListener); + + nsIURI issueCommandOnMsgs(in nsIMsgFolder aMailFolder, + in nsIMsgWindow aMsgWindow, + in ACString aCommand, + in ACString aMessageIdentifierList); + + nsIURI fetchCustomMsgAttribute(in nsIMsgFolder aMailFolder, + in nsIMsgWindow aMsgWindow, + in ACString aAttribute, + in ACString aMessageIdentifierList); + + nsIURI storeCustomKeywords(in nsIMsgFolder aMailFolder, + in nsIMsgWindow aMsgWindow, + in ACString flagsToAdd, + in ACString flagsToSubtract, + in ACString aMessageIdentifierList); + + void getListOfFoldersOnServer(in nsIImapIncomingServer aServer, in nsIMsgWindow aMsgWindow); + void getListOfFoldersWithPath(in nsIImapIncomingServer aServer, in nsIMsgWindow aMsgWindow, in ACString folderPath); + + nsISupports playbackAllOfflineOperations(in nsIMsgWindow aMsgWindow, in nsIUrlListener aListener); + void downloadAllOffineImapFolders(in nsIMsgWindow aMsgWindow, in nsIUrlListener aListener); + + readonly attribute nsICacheStorage cacheStorage; +}; diff --git a/comm/mailnews/imap/public/nsIImapUrl.idl b/comm/mailnews/imap/public/nsIImapUrl.idl new file mode 100644 index 0000000000..75f85b93a5 --- /dev/null +++ b/comm/mailnews/imap/public/nsIImapUrl.idl @@ -0,0 +1,210 @@ +/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIImapMailFolderSink; +interface nsIImapMessageSink; +interface nsIImapServerSink; +interface nsIImapMockChannel; +interface nsIFile; + +typedef long nsImapAction; +typedef long nsImapState; + +typedef unsigned short imapMessageFlagsType; + +typedef long nsImapContentModifiedType; + +[scriptable, uuid(2e91901e-ff6c-11d3-b9fa-00108335942a)] +interface nsImapContentModifiedTypes : nsISupports +{ + 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(fe2a8f9e-2886-4146-9896-27fff660c69f)] +interface nsIImapUrl : nsISupports +{ + /////////////////////////////////////////////////////////////////////////////// + // Getters and Setters for the imap specific event sinks to bind to the url + /////////////////////////////////////////////////////////////////////////////// + attribute nsIImapMailFolderSink imapMailFolderSink; + attribute nsIImapMessageSink imapMessageSink; + attribute nsIImapServerSink imapServerSink; + + /////////////////////////////////////////////////////////////////////////////// + // Getters and Setters for the imap url state + /////////////////////////////////////////////////////////////////////////////// + attribute nsImapAction imapAction; + readonly attribute nsImapState requiredImapState; + readonly attribute string imapPartToFetch; + readonly attribute ACString customAttributeToFetch; + attribute ACString customAttributeResult; + readonly attribute ACString command; + attribute ACString customCommandResult; + readonly attribute ACString customAddFlags; + readonly attribute ACString customSubtractFlags; + void allocateCanonicalPath(in string aServerPath, in char aOnlineDelimiter, out string aAllocatedPath); + void allocateServerPath(in string aCanonicalPath, in char aOnlineDelimiter, out string aAllocatedPath); + string createServerSourceFolderPathString(); + string createCanonicalSourceFolderPathString(); + string createServerDestinationFolderPathString(); + + string addOnlineDirectoryIfNecessary(in string onlineMailboxName); + void createSearchCriteriaString(out string aResult); + readonly attribute ACString listOfMessageIds; + + boolean messageIdsAreUids(); + readonly attribute imapMessageFlagsType msgFlags; // kAddMsgFlags or kSubtractMsgFlags only + + readonly attribute long numBytesToFetch; + attribute char onlineSubDirSeparator; + attribute boolean mimePartSelectorDetected; + 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. + /** + * copyState is used by some IMAP copy operations. The exact type stashed + * here depends on the operation being performed. For online move/copy, + * it'll be an nsImapMailCopyState (private to nsImapMailFolder). For + * other operations it might be (say), an nsIStreamListener. + */ + attribute nsISupports copyState; + attribute nsIFile msgFile; + attribute nsIImapMockChannel mockChannel; + /** + * 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; + + /** + * This attribute defaults to false, but if we only want to use the offline + * cache (disk, memory, or offline store) to fetch the message, then we set + * this to true. Currently, nsIMsgMessageService.streamMessage does this. + */ + attribute boolean localFetchOnly; + + /// Server disconnected first time so we're retrying. + attribute boolean rerunningUrl; + + /** + * Do we have more headers to download? This is set when we decide to + * download newest headers first, followed by older headers in a subsequent + * run of the url, which allows other urls to run against the folder in the + * meantime. + */ + attribute boolean moreHeadersToDownload; + + /** + * @{ + * 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. + */ + attribute long extraStatus; + + /** + * Current possible extra status values + */ + const long ImapStatusNone = 0; + const long ImapStatusFlagChangeFailed = 1; + const long ImapStatusFlagsNotSettable = 2; + /** @} */ + + /////////////////////////////////////////////////////////////////////////////// + // Enumerated types specific to imap urls... + /////////////////////////////////////////////////////////////////////////////// + + // the following are nsImapState enums. + // we have a basic set of imap url actions. These actions are nsImapActions. + // Certain actions require us to be in the authenticated state and others require us to + // be in the selected state. nsImapState is used to store the state the url needs to + // be in. You'll later see us refer to the imap url state in the imap protocol when we + // are processing the current url. Don't confuse nsImapState with the generic url state + // used to keep track of whether the url is running or not... + const long nsImapAuthenticatedState = 0; + const long nsImapSelectedState = 1; + + const long nsImapActionSendText = 0; // a state used for testing purposes to send raw url text straight to the server.... + // nsImapAuthenticatedStateUrl urls + // since the following url actions require us to be in the authenticated + // state, the high bit is left blank.... + const long nsImapTest = 0x00000001; + const long nsImapCreateFolder = 0x00000005; + const long nsImapDeleteFolder = 0x00000006; + const long nsImapRenameFolder = 0x00000007; + const long nsImapMoveFolderHierarchy = 0x00000008; + const long nsImapLsubFolders = 0x00000009; + const long nsImapGetMailAccountUrl = 0x0000000A; + const long nsImapDiscoverChildrenUrl = 0x0000000B; + const long nsImapDiscoverAllBoxesUrl = 0x0000000D; + const long nsImapDiscoverAllAndSubscribedBoxesUrl = 0x0000000E; + const long nsImapAppendMsgFromFile = 0x0000000F; + const long nsImapSubscribe = 0x00000010; + const long nsImapUnsubscribe = 0x00000011; + const long nsImapRefreshACL = 0x00000012; + const long nsImapRefreshAllACLs = 0x00000013; + const long nsImapListFolder = 0x00000014; + const long nsImapUpgradeToSubscription = 0x00000015; + const long nsImapFolderStatus = 0x00000016; + const long nsImapRefreshFolderUrls = 0x00000017; + const long nsImapEnsureExistsFolder = 0x00000018; + const long nsImapOfflineToOnlineCopy = 0x00000019; + const long nsImapOfflineToOnlineMove = 0x0000001A; + const long nsImapVerifylogon = 0x0000001B; + // it's okay to add more imap actions that require us to + // be in the authenticated state here without renumbering + // the imap selected state url actions. just make sure you don't + // set the high bit... + + // nsImapSelectedState urls. Note, the high bit is always set for + // imap actions which require us to be in the selected state + const long nsImapSelectFolder = 0x10000002; + const long nsImapLiteSelectFolder = 0x10000003; + const long nsImapExpungeFolder = 0x10000004; + const long nsImapMsgFetch = 0x10000018; + const long nsImapMsgHeader = 0x10000019; + const long nsImapSearch = 0x1000001A; + const long nsImapDeleteMsg = 0x1000001B; + const long nsImapDeleteAllMsgs = 0x1000001C; + const long nsImapAddMsgFlags = 0x1000001D; + const long nsImapSubtractMsgFlags = 0x1000001E; + const long nsImapSetMsgFlags = 0x1000001F; + const long nsImapOnlineCopy = 0x10000020; + const long nsImapOnlineMove = 0x10000021; + const long nsImapOnlineToOfflineCopy = 0x10000022; + const long nsImapOnlineToOfflineMove = 0x10000023; + const long nsImapMsgPreview = 0x10000024; + const long nsImapBiff = 0x10000026; + const long nsImapSelectNoopFolder = 0x10000027; + const long nsImapAppendDraftFromFile = 0x10000028; + const long nsImapUidExpunge = 0x10000029; + const long nsImapSaveMessageToDisk = 0x10000030; + const long nsImapOpenMimePart = 0x10000031; + const long nsImapMsgDownloadForOffline = 0x10000032; + const long nsImapDeleteFolderAndMsgs = 0x10000033; + const long nsImapUserDefinedMsgCommand = 0x10000034; + const long nsImapUserDefinedFetchAttribute = 0x10000035; + const long nsImapMsgFetchPeek = 0x10000036; + const long nsImapMsgStoreCustomKeywords = 0x10000037; + + /// Constant for the default IMAP port number + const int32_t DEFAULT_IMAP_PORT = 143; + + /// Constant for the default IMAP over ssl port number + const int32_t DEFAULT_IMAPS_PORT = 993; +}; diff --git a/comm/mailnews/imap/public/nsIMailboxSpec.idl b/comm/mailnews/imap/public/nsIMailboxSpec.idl new file mode 100644 index 0000000000..729b867075 --- /dev/null +++ b/comm/mailnews/imap/public/nsIMailboxSpec.idl @@ -0,0 +1,47 @@ +/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIImapFlagAndUidState; + +[ptr] native nsImapNamespace(nsImapNamespace); +%{C++ +class nsImapNamespace; +%} + +[scriptable, uuid(a9fbbc80-5291-4ed8-a7f7-c2fcad231269)] +interface nsIMailboxSpec : nsISupports +{ + attribute long folder_UIDVALIDITY; + /** + * The highest modification sequence number the parser has seen + * for this mailbox. See IMAP RFC 4551 + **/ + attribute unsigned long long highestModSeq; + attribute long numMessages; + attribute long numUnseenMessages; + attribute long numRecentMessages; + + /// If server supports UIDNEXT, we store the result here. + attribute long nextUID; + + attribute unsigned long box_flags; + attribute unsigned long supportedUserFlags; + + attribute ACString allocatedPathName; + attribute AString unicharPathName; + attribute char hierarchyDelimiter; + attribute ACString hostName; + + attribute nsIImapFlagAndUidState flagState; + + attribute boolean folderSelected; + attribute boolean discoveredFromLsub; + + attribute boolean onlineVerified; + + [noscript] attribute nsImapNamespace namespaceForFolder; +}; diff --git a/comm/mailnews/imap/public/nsIMsgImapMailFolder.idl b/comm/mailnews/imap/public/nsIMsgImapMailFolder.idl new file mode 100644 index 0000000000..070276508a --- /dev/null +++ b/comm/mailnews/imap/public/nsIMsgImapMailFolder.idl @@ -0,0 +1,253 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +#include "MailNewsTypes2.idl" + +interface nsIMsgFolder; +interface nsIUTF8StringEnumerator; +interface nsIUrlListener; +interface nsIURI; +interface nsIMsgDBHdr; +interface nsIMsgWindow; +interface nsIImapIncomingServer; +interface nsIMsgParseMailMsgState; +interface nsIAutoSyncState; + +/** + * nsIMsgQuota defines the quota for a resource within a quota root. + * @see RFC 2087 + */ +[scriptable, uuid(9db60f97-45c1-45c2-8ab1-395690228f3f)] +interface nsIMsgQuota : nsISupports { + /** + * If quota root name not empty, the concatenation of the quota root name + * and the resource name separated by a slash , e.g., + * "User Quota / MESSAGE" or with empty quota root name, just resource + * name, e.g., "STORAGE". + */ + attribute AUTF8String name; + + /** + * Amount of resource in use for this quota root. E.g., number of messages + * or storage in KB (1024 octets). + */ + + attribute unsigned long long usage; + /** + * Maximum amount of usage permitted by the server for this quota root + * resource. + */ + attribute unsigned long long limit; +}; + +/** + * nsIMsgImapFolderProps is a simple interface which allows the IMAP folder to + * update some values that the folder props js code will use to update the + * sharing and quota tabs in the folder properties. + */ +[scriptable, uuid(09D99F2C-3E23-4f8c-A536-5C277BAA9585)] +interface nsIMsgImapFolderProps : nsISupports { + + void setFolderType(in AString folderType); + void setFolderTypeDescription(in AString folderTypeDescription); + void setFolderPermissions(in AString permissions); + void serverDoesntSupportACL(); + + /** + * Toggles the display of quota information in the Quota tab of the folder properties. + * If on, the quota root, usage, and percentage used are displayed. + * If off, a status message is displayed. The status message can be set with setQuotaStatus(). + * @param showData If true, display the quota root, usage information and usage percentage bar. + * If false, display the status message. + */ + void showQuotaData(in boolean showData); + + /** + * Sets the status string displayed in the Quota tab of the folder properties if quota + * information is not visible. + */ + void setQuotaStatus(in AString folderQuotaStatus); + + /** + * Sets the quota data displayed in the folder properties Quota tab. An + * array of quota items is passed in. + */ + void setQuotaData(in Array<nsIMsgQuota> quotaArray); +}; + +[scriptable, uuid(fea0f455-7adf-4683-bf2f-c95c3fff03df)] +interface nsIMsgImapMailFolder : nsISupports { + /** + * Remove the local version of this folder (used to clean up local folders + * which don't correspond to ones on the server). + */ + void removeLocalSelf(); + void createClientSubfolderInfo(in AUTF8String folderName, in char hierarchyDelimiter, + in long flags, in boolean suppressNotification); + void list(); + void renameLocal(in AUTF8String newname, in nsIMsgFolder parent); + void prepareToRename(); + void performExpand(in nsIMsgWindow aMsgWindow); + void recursiveCloseActiveConnections(in nsIImapIncomingServer aImapServer); + void renameClient(in nsIMsgWindow msgWindow, in nsIMsgFolder msgFolder, in AUTF8String oldName, in AUTF8String newName); + + // these are used for offline synchronization + void storeImapFlags(in long aFlags, in boolean aAddFlags, + in Array<nsMsgKey> aKeysToFlag, in nsIUrlListener aUrlListener); + nsIURI setImapFlags(in string uids, in long flags); + void replayOfflineMoveCopy(in Array<nsMsgKey> keys, in boolean isMove, + in nsIMsgFolder aDstFolder, in nsIUrlListener aUrlListener, + in nsIMsgWindow aWindow, in boolean srcFolderOffline); + 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 + */ + void liteSelect(in nsIUrlListener aUrlListener, in nsIMsgWindow aWindow); + + void fillInFolderProps(in nsIMsgImapFolderProps aFolderProps); + void resetNamespaceReferences(); + void folderPrivileges(in nsIMsgWindow aWindow); + nsIMsgImapMailFolder findOnlineSubFolder(in AUTF8String onlineName); + void addFolderRights(in ACString userName, in ACString rights); + void refreshFolderRights(); + + /** + * Mark/unmark the header as pending removal from the offline store. If mark, + * this also increases the expungedBytes count on the folder so we know + * there's more local disk space to be reclaimed. + * + * @param aHdr msg hdr to mark pending removal from offline store. + * @param aMark whether to set or clear the pending removal status. + * + */ + void markPendingRemoval(in nsIMsgDBHdr aHdr, in boolean aMark); + + /** + * Issue an expunge of this folder to the imap server. + * + * @param aUrlListener url listener, can be null + * @param aWindow msg window url is running in, can be null + * + * @returns status of attempt to run url. + */ + void expunge(in nsIUrlListener aListener, in nsIMsgWindow aMsgWindow); + + void updateStatus(in nsIUrlListener aListener, in nsIMsgWindow aMsgWindow); + void updateFolderWithListener(in nsIMsgWindow aMsgWindow, in nsIUrlListener aListener); + // this is used to issue an arbitrary imap command on the passed in msgs. + // It assumes the command needs to be run in the selected state. + nsIURI issueCommandOnMsgs(in ACString command, in string uids, in nsIMsgWindow aWindow); + nsIURI fetchCustomMsgAttribute(in ACString msgAttribute, in string uids, in nsIMsgWindow aWindow); + nsIURI storeCustomKeywords(in nsIMsgWindow aMsgWindow, + in ACString aFlagsToAdd, + in ACString aFlagsToSubtract, + in Array<nsMsgKey> aKeysToStore); + + void notifyIfNewMail(); + + void initiateAutoSync(in nsIUrlListener aUrlListener); + + attribute boolean verifiedAsOnlineFolder; + attribute boolean explicitlyVerify; + attribute char hierarchyDelimiter; + attribute long boxFlags; + /** + * onlineName is the IMAP name of the mailbox that this folder represents. + * It's a path with components separated by hierarchyDelimiter. + * For example, "INBOX/bar/wibble", "INBOX.bar.wibble", etc... + */ + attribute AUTF8String onlineName; + attribute boolean isNamespace; + readonly attribute boolean canOpenFolder; + attribute AUTF8String adminUrl; + readonly attribute boolean hasAdminUrl; + attribute boolean performingBiff; + readonly attribute nsIMsgParseMailMsgState hdrParser; + readonly attribute nsIImapIncomingServer imapIncomingServer; + readonly attribute nsIAutoSyncState autoSyncStateObj; + readonly attribute boolean shouldUseUtf8FolderName; + /** + * @{ + * These are used to access the response to the STATUS or SELECT command. + * The counts include deleted messages, or headers we haven't downloaded yet. + */ + readonly attribute long serverTotal; + readonly attribute long serverUnseen; + readonly attribute long serverRecent; + readonly attribute long serverNextUID; + /** @} */ + + /** + * Return an array of quota items of type nsIMsgQuota defined above. + * A not-empty array indicates that the server has provided one or more + * sets of quota information on this folder. The array will be empty + * - if the server does not supports quotas, + * - if there are no resource quotas on this folder, or + * - if the folder has yet to be opened (selected) by the user. + */ + Array<nsIMsgQuota> getQuota(); + + /** + * List all (human) users apart from the current user who have access to + * this folder. + * + * You can find out which rights they have with getRightsForUser(). + */ + nsIUTF8StringEnumerator getOtherUsersWithAccess(); + + /** + * Which access rights a certain user has for this folder. + * + * @return list of flags + * e.g. "lrswipcd" for write access and "lrs" for read only access. + * + * See RFC 2086 (e.g. Cyrus) and RFC 4314 (e.g. dovecot) + * + * l = locate = visible in folder list + * r = read = list mails, get/read mail contents + * s = set seen flag = mark read. Does not affect other users. + * d (or t) = delete mails + * w = write = change (other) flags of existing mails + * i = insert = add mails to this folder + * p = post = send mail directly to the submission address for folder + * c (or k) = create subfolders + * (e = expunge = compress) + * (x = delete folder) + * a = admin = change permissions + */ + ACString getPermissionsForUser(in ACString username); + + /** + * Change the number of "pending" messages in a folder, + * messages we know about, but don't have the headers for yet + * + * @param aDelta amount to change total by. + */ + void changePendingTotal(in long aDelta); + + /** + * Change the number of "pending" unread messages in a folder, + * unread messages we know about, but don't have the headers for yet + * + * @param aDelta amount to change the unread count by. + */ + void changePendingUnread(in long aDelta); +}; diff --git a/comm/mailnews/imap/src/ImapChannel.jsm b/comm/mailnews/imap/src/ImapChannel.jsm new file mode 100644 index 0000000000..0e5c333d9a --- /dev/null +++ b/comm/mailnews/imap/src/ImapChannel.jsm @@ -0,0 +1,318 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["ImapChannel"]; + +const { ImapUtils } = ChromeUtils.import("resource:///modules/ImapUtils.jsm"); +const { MailChannel } = ChromeUtils.importESModule( + "resource:///modules/MailChannel.sys.mjs" +); +const { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +/** + * A channel to interact with IMAP server. + * + * @implements {nsIChannel} + * @implements {nsIRequest} + * @implements {nsICacheEntryOpenCallback} + */ +class ImapChannel extends MailChannel { + QueryInterface = ChromeUtils.generateQI([ + "nsIMailChannel", + "nsIChannel", + "nsIRequest", + "nsIWritablePropertyBag", + "nsICacheEntryOpenCallback", + ]); + + _logger = ImapUtils.logger; + _status = Cr.NS_OK; + + /** + * @param {nsIURI} uri - The uri to construct the channel from. + * @param {nsILoadInfo} loadInfo - The loadInfo associated with the channel. + */ + constructor(uri, loadInfo) { + super(); + this._server = MailServices.accounts + .findServerByURI(uri) + .QueryInterface(Ci.nsIImapIncomingServer); + + // nsIChannel attributes. + this.originalURI = uri; + this.loadInfo = loadInfo; + this.contentLength = 0; + + this.uri = uri; + + uri = uri.QueryInterface(Ci.nsIMsgMessageUrl); + try { + this.contentLength = uri.messageHeader.messageSize; + } catch (e) { + // Got passed an IMAP folder URL. + this._isFolderURL = this._server && !/#(\d+)$/.test(uri.spec); + } + } + + /** + * @see nsIRequest + * @returns {string} + */ + get name() { + return this.URI?.spec; + } + + /** + * @see nsIRequest + * @returns {boolean} + */ + isPending() { + return !!this._pending; + } + + /** + * @see nsIRequest + * @returns {nsresult} + */ + get status() { + return this._status; + } + + /** + * @see nsICacheEntryOpenCallback + */ + onCacheEntryAvailable(entry, isNew, status) { + if (!Components.isSuccessCode(status)) { + // If memory cache doesn't work, read from the server. + this._readFromServer(); + return; + } + + if (isNew) { + if (Services.io.offline) { + this._status = Cr.NS_ERROR_OFFLINE; + return; + } + // It's a new entry, needs to read from the server. + let tee = Cc["@mozilla.org/network/stream-listener-tee;1"].createInstance( + Ci.nsIStreamListenerTee + ); + let outStream = entry.openOutputStream(0, -1); + // When the tee stream receives data from the server, it writes to both + // the original listener and outStream (memory cache). + tee.init(this._listener, outStream, null); + this._listener = tee; + this._cacheEntry = entry; + this._readFromServer(); + return; + } + + // It's an old entry, read from the memory cache. + this._readFromCacheStream(entry.openInputStream(0)); + } + + onCacheEntryCheck(entry) { + return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; + } + + /** + * Get readonly URI. + * @see nsIChannel + */ + get URI() { + return this.uri; + } + + get contentType() { + return this._contentType || "message/rfc822"; + } + + set contentType(value) { + this._contentType = value; + } + + get isDocument() { + return true; + } + + open() { + throw Components.Exception( + "ImapChannel.open() not implemented", + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + } + + asyncOpen(listener) { + this._logger.debug(`asyncOpen ${this.URI.spec}`); + if (this._isFolderURL) { + let handler = Cc[ + "@mozilla.org/uriloader/content-handler;1?type=x-application-imapfolder" + ].createInstance(Ci.nsIContentHandler); + handler.handleContent("x-application-imapfolder", null, this); + return; + } + + let url = new URL(this.URI.spec); + this._listener = listener; + if (url.searchParams.get("part")) { + let converter = Cc["@mozilla.org/streamConverters;1"].getService( + Ci.nsIStreamConverterService + ); + this._listener = converter.asyncConvertData( + "message/rfc822", + "*/*", + listener, + this + ); + } + + let msgIds = this.URI.QueryInterface(Ci.nsIImapUrl).QueryInterface( + Ci.nsIMsgMailNewsUrl + ).listOfMessageIds; + this._msgKey = parseInt(msgIds); + this.contentLength = 0; + try { + if (this.readFromLocalCache()) { + this._logger.debug("Read from local cache"); + return; + } + } catch (e) { + this._logger.warn(e); + } + + try { + let uri = this.URI; + if (this.URI.spec.includes("?")) { + uri = uri.mutate().setQuery("").finalize(); + } + // Check if a memory cache is available for the current URI. + MailServices.imap.cacheStorage.asyncOpenURI( + uri, + "", + this.URI.QueryInterface(Ci.nsIImapUrl).storeResultsOffline + ? // Don't write to the memory cache if storing offline. + Ci.nsICacheStorage.OPEN_READONLY + : Ci.nsICacheStorage.OPEN_NORMALLY, + this + ); + } catch (e) { + this._logger.warn(e); + this._readFromServer(); + } + if (this._status == Cr.NS_ERROR_OFFLINE) { + throw new Components.Exception( + "The requested action could not be completed in the offline state", + Cr.NS_ERROR_OFFLINE + ); + } + } + + /** + * Try to read the message from the offline storage. + * + * @returns {boolean} True if successfully read from the offline storage. + */ + readFromLocalCache() { + if ( + !this.URI.QueryInterface(Ci.nsIImapUrl).QueryInterface( + Ci.nsIMsgMailNewsUrl + ).msgIsInLocalCache && + !this.URI.folder.hasMsgOffline(this._msgKey, null, 10) + ) { + return false; + } + + let hdr = this.URI.folder.GetMessageHeader(this._msgKey); + let stream = this.URI.folder.getLocalMsgStream(hdr); + this._readFromCacheStream(stream); + return true; + } + + /** + * Read the message from the a stream. + * + * @param {nsIInputStream} cacheStream - The input stream to read. + */ + _readFromCacheStream(stream) { + let pump = Cc["@mozilla.org/network/input-stream-pump;1"].createInstance( + Ci.nsIInputStreamPump + ); + this._contentType = ""; + pump.init(stream, 0, 0, true); + pump.asyncRead({ + onStartRequest: () => { + this._listener.onStartRequest(this); + this.URI.SetUrlState(true, Cr.NS_OK); + this._pending = true; + }, + onStopRequest: (request, status) => { + this._listener.onStopRequest(this, status); + this.URI.SetUrlState(false, status); + try { + this.loadGroup?.removeRequest(this, null, Cr.NS_OK); + } catch (e) {} + this._pending = false; + }, + onDataAvailable: (request, stream, offset, count) => { + this.contentLength += count; + this._listener.onDataAvailable(this, stream, offset, count); + try { + if (!stream.available()) { + stream.close(); + } + } catch (e) {} + }, + }); + } + + /** + * Retrieve the message from the server. + */ + _readFromServer() { + this._logger.debug("Read from server"); + let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe); + pipe.init(true, true, 0, 0); + let inputStream = pipe.inputStream; + let outputStream = pipe.outputStream; + + this._server.wrappedJSObject.withClient(this.URI.folder, client => { + client.startRunningUrl(null, null, this.URI); + client.channel = this; + this._listener.onStartRequest(this); + this._pending = true; + client.onReady = () => { + client.fetchMessage(this.URI.folder, this._msgKey); + }; + + client.onData = data => { + this.contentLength += data.length; + outputStream.write(data, data.length); + this._listener.onDataAvailable(this, inputStream, 0, data.length); + }; + + client.onDone = status => { + try { + this.loadGroup?.removeRequest(this, null, status); + } catch (e) {} + this._listener.onStopRequest(this, status); + }; + this._pending = false; + }); + } + + /** @see nsIWritablePropertyBag */ + getProperty(key) { + return this[key]; + } + + setProperty(key, value) { + this[key] = value; + } + + deleteProperty(key) { + delete this[key]; + } +} diff --git a/comm/mailnews/imap/src/ImapClient.jsm b/comm/mailnews/imap/src/ImapClient.jsm new file mode 100644 index 0000000000..319a03b958 --- /dev/null +++ b/comm/mailnews/imap/src/ImapClient.jsm @@ -0,0 +1,1895 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["ImapClient"]; + +var { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +var { clearTimeout, setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); +var { MailStringUtils } = ChromeUtils.import( + "resource:///modules/MailStringUtils.jsm" +); +var { ImapAuthenticator } = ChromeUtils.import( + "resource:///modules/MailAuthenticator.jsm" +); +var { ImapResponse } = ChromeUtils.import( + "resource:///modules/ImapResponse.jsm" +); +var { ImapUtils } = ChromeUtils.import("resource:///modules/ImapUtils.jsm"); + +// There can be multiple ImapClient running concurrently, assign each logger a +// unique prefix. +let loggerInstanceId = 0; + +const PR_UINT32_MAX = 0xffffffff; + +/** + * A class to interact with IMAP server. + */ +class ImapClient { + _logger = console.createInstance({ + prefix: `mailnews.imap.${loggerInstanceId++}`, + maxLogLevel: "Warn", + maxLogLevelPref: "mailnews.imap.loglevel", + }); + + /** + * @param {nsIImapIncomingServer} server - The associated server instance. + */ + constructor(server) { + this._server = server.QueryInterface(Ci.nsIMsgIncomingServer); + this._serverSink = this._server.QueryInterface(Ci.nsIImapServerSink); + this._authenticator = new ImapAuthenticator(server); + + // Auth methods detected from the CAPABILITY response. + this._supportedAuthMethods = []; + // Subset of _supportedAuthMethods that are allowed by user preference. + this._possibleAuthMethods = []; + // Auth methods set by user preference. + this._preferredAuthMethods = + { + [Ci.nsMsgAuthMethod.passwordCleartext]: ["PLAIN", "LOGIN"], + [Ci.nsMsgAuthMethod.passwordEncrypted]: ["CRAM-MD5"], + [Ci.nsMsgAuthMethod.GSSAPI]: ["GSSAPI"], + [Ci.nsMsgAuthMethod.NTLM]: ["NTLM"], + [Ci.nsMsgAuthMethod.OAuth2]: ["XOAUTH2"], + [Ci.nsMsgAuthMethod.External]: ["EXTERNAL"], + }[server.authMethod] || []; + // The next auth method to try if the current failed. + this._nextAuthMethod = null; + + this._tag = Math.floor(100 * Math.random()); + this._charsetManager = Cc[ + "@mozilla.org/charset-converter-manager;1" + ].getService(Ci.nsICharsetConverterManager); + + this._messageUids = []; + this._messages = new Map(); + + this._loadPrefs(); + } + + /** + * @type {boolean} - Whether the socket is open. + */ + get isOnline() { + return this._socket?.readyState == "open"; + } + + /** + * Load imap related preferences, many behaviors depend on these pref values. + */ + _loadPrefs() { + this._prefs = { + tcpTimeout: Services.prefs.getIntPref("mailnews.tcptimeout"), + }; + } + + /** + * Reset some internal states to be safely reused. + */ + _reset() { + this.onData = () => {}; + this.onDone = () => {}; + + this._actionAfterDiscoverAllFolders = null; + this.channel = null; + this._urlListener = null; + this._msgWindow = null; + this._authenticating = false; + this.verifyLogon = false; + this._idling = false; + if (this._idleTimer) { + clearTimeout(this._idleTimer); + this._idleTimer = null; + } + } + + /** + * Initiate a connection to the server + */ + connect() { + if (this.isOnline) { + // Reuse the connection. + this.onReady(); + this._setSocketTimeout(this._prefs.tcpTimeout); + } else { + let hostname = this._server.hostName.toLowerCase(); + this._logger.debug(`Connecting to ${hostname}:${this._server.port}`); + this._greeted = false; + this._capabilities = null; + this._secureTransport = this._server.socketType == Ci.nsMsgSocketType.SSL; + this._socket = new TCPSocket(hostname, this._server.port, { + binaryType: "arraybuffer", + useSecureTransport: this._secureTransport, + }); + this._socket.onopen = this._onOpen; + this._socket.onerror = this._onError; + } + } + + /** + * Set socket timeout in seconds. + * + * @param {number} timeout - The timeout in seconds. + */ + _setSocketTimeout(timeout) { + this._socket.transport?.setTimeout( + Ci.nsISocketTransport.TIMEOUT_READ_WRITE, + timeout + ); + } + + /** + * Construct an nsIMsgMailNewsUrl instance, setup urlListener to notify when + * the current request is finished. + * + * @param {nsIUrlListener} urlListener - Callback for the request. + * @param {nsIMsgWindow} msgWindow - The associated msg window. + * @param {nsIMsgMailNewsUrl} [runningUri] - The url to run, if provided. + * @returns {nsIMsgMailNewsUrl} + */ + startRunningUrl(urlListener, msgWindow, runningUri) { + this._urlListener = urlListener; + this._msgWindow = msgWindow; + this.runningUri = runningUri; + if (!this.runningUri) { + this.runningUri = Services.io.newURI( + `imap://${this._server.hostName}:${this._server.port}` + ); + } + this._urlListener?.OnStartRunningUrl(this.runningUri, Cr.NS_OK); + this.runningUri + .QueryInterface(Ci.nsIMsgMailNewsUrl) + .SetUrlState(true, Cr.NS_OK); + return this.runningUri; + } + + /** + * Discover all folders if the current server hasn't already discovered. + */ + _discoverAllFoldersIfNecessary = () => { + if (this._server.hasDiscoveredFolders) { + this.onReady(); + return; + } + this._actionAfterDiscoverAllFolders = this.onReady; + this.discoverAllFolders(this._server.rootFolder); + }; + + /** + * Discover all folders. + * + * @param {nsIMsgFolder} folder - The associated folder. + */ + discoverAllFolders(folder) { + this._logger.debug("discoverAllFolders", folder.URI); + + let handleListResponse = res => { + this._hasTrash = res.mailboxes.some( + mailbox => mailbox.flags & ImapUtils.FLAG_IMAP_TRASH + ); + if (!this._hasTrash) { + let trashFolderName = this._server.trashFolderName.toLowerCase(); + let trashMailbox = res.mailboxes.find( + mailbox => mailbox.name.toLowerCase() == trashFolderName + ); + if (trashMailbox) { + this._hasTrash = true; + trashMailbox.flags |= ImapUtils.FLAG_IMAP_TRASH; + } + } + for (let mailbox of res.mailboxes) { + this._serverSink.possibleImapMailbox( + mailbox.name.replaceAll(mailbox.delimiter, "/"), + mailbox.delimiter, + mailbox.flags + ); + } + }; + + if (this._capabilities.includes("LIST-EXTENDED")) { + this._nextAction = res => { + handleListResponse(res); + this._actionFinishFolderDiscovery(); + }; + let command = 'LIST (SUBSCRIBED) "" "*"'; + if (this._capabilities.includes("SPECIAL-USE")) { + command += " RETURN (SPECIAL-USE)"; // rfc6154 + } + this._sendTagged(command); + return; + } + + this._nextAction = res => { + this._nextAction = res2 => { + // Per rfc3501#section-6.3.9, if LSUB returns different flags from LIST, + // use the LIST responses. + for (let mailbox of res2.mailboxes) { + let mailboxFromList = res.mailboxes.find(x => x.name == mailbox.name); + if ( + mailboxFromList?.flags && + mailboxFromList?.flags != mailbox.flags + ) { + mailbox.flags = mailboxFromList.flags; + } + } + handleListResponse(res2); + this._actionFinishFolderDiscovery(); + }; + this._sendTagged('LSUB "" "*"'); + }; + let command = 'LIST "" "*"'; + if (this._capabilities.includes("SPECIAL-USE")) { + command += " RETURN (SPECIAL-USE)"; // rfc6154 + } + this._sendTagged(command); + } + + /** + * Discover all folders for the subscribe dialog. + * + * @param {nsIMsgFolder} folder - The associated folder. + */ + discoverAllAndSubscribedFolders(folder) { + this._logger.debug("discoverAllAndSubscribedFolders", folder.URI); + let handleListResponse = res => { + for (let mailbox of res.mailboxes) { + this._serverSink.possibleImapMailbox( + mailbox.name.replaceAll(mailbox.delimiter, "/"), + mailbox.delimiter, + mailbox.flags + ); + } + }; + + this._nextAction = res => { + handleListResponse(res); + this._server.doingLsub = false; + this._nextAction = res2 => { + // Per rfc3501#section-6.3.9, if LSUB returns different flags from LIST, + // use the LIST responses. + for (let mailbox of res2.mailboxes) { + let mailboxFromList = res.mailboxes.find(x => x.name == mailbox.name); + if ( + mailboxFromList?.flags && + mailboxFromList?.flags != mailbox.flags + ) { + mailbox.flags = mailboxFromList.flags; + } + } + handleListResponse(res2); + this._actionDone(); + }; + this._sendTagged('LIST "" "*"'); + }; + this._sendTagged('LSUB "" "*"'); + this._server.doingLsub = true; + } + + /** + * Select a folder. + * + * @param {nsIMsgFolder} folder - The folder to select. + */ + selectFolder(folder) { + this._logger.debug("selectFolder", folder.URI); + if (this.folder == folder) { + this._actionNoop(); + return; + } + this._actionAfterSelectFolder = this._actionUidFetch; + this._nextAction = this._actionSelectResponse(folder); + this._sendTagged(`SELECT "${this._getServerFolderName(folder)}"`); + } + + /** + * Rename a folder. + * + * @param {nsIMsgFolder} folder - The folder to rename. + * @param {string} newName - The new folder name. + */ + renameFolder(folder, newName) { + this._logger.debug("renameFolder", folder.URI, newName); + let delimiter = + folder.QueryInterface(Ci.nsIMsgImapMailFolder).hierarchyDelimiter || "/"; + let names = this._getAncestorFolderNames(folder); + let oldName = this._getServerFolderName(folder); + newName = this._encodeMailboxName([...names, newName].join(delimiter)); + + this._nextAction = this._actionRenameResponse(oldName, newName); + this._sendTagged(`RENAME "${oldName}" "${newName}"`); + } + + /** + * Move a source folder to be a child of another folder. + * + * @param {nsIMsgFolder} srcFolder - The source folder to move. + * @param {nsIMsgFolder} dstFolder - The target parent folder. + */ + moveFolder(srcFolder, dstFolder) { + this._logger.debug("moveFolder", srcFolder.URI, dstFolder.URI); + let oldName = this._getServerFolderName(srcFolder); + let newName = this._getServerSubFolderName(dstFolder, srcFolder.name); + this._nextAction = this._actionRenameResponse(oldName, newName, true); + this._sendTagged(`RENAME "${oldName}" "${newName}"`); + } + + /** + * Send LIST command for a folder. + * + * @param {nsIMsgFolder} folder - The folder to list. + */ + listFolder(folder) { + this._logger.debug("listFolder", folder.URI); + this._actionList(this._getServerFolderName(folder), () => { + this._actionDone(); + }); + } + + /** + * Send DELETE command for a folder and all subfolders. + * + * @param {nsIMsgFolder} folder - The folder to delete. + */ + deleteFolder(folder) { + this._logger.debug("deleteFolder", folder.URI); + this._nextAction = res => { + // Leaves have longer names than parent mailbox, sort them by the name + // length, so that leaf mailbox will be deleted first. + let mailboxes = res.mailboxes.sort( + (x, y) => y.name.length - x.name.length + ); + let selfName = this._getServerFolderName(folder); + let selfIncluded = false; + this._nextAction = () => { + let mailbox = mailboxes.shift(); + if (mailbox) { + this._sendTagged(`DELETE "${mailbox.name}"`); + if (!selfIncluded && selfName == mailbox.name) { + selfIncluded = true; + } + } else if (!selfIncluded) { + this._nextAction = () => this._actionDone(); + this._sendTagged(`DELETE "${this._getServerFolderName(folder)}"`); + } else { + this._actionDone(); + } + }; + this._nextAction(); + }; + this._sendTagged(`LIST "" "${this._getServerFolderName(folder)}"`); + } + + /** + * Ensure a folder exists on the server. Create one if not already exists. + * + * @param {nsIMsgFolder} parent - The parent folder to check. + * @param {string} folderName - The folder name. + */ + ensureFolderExists(parent, folderName) { + this._logger.debug("ensureFolderExists", parent.URI, folderName); + let mailboxName = this._getServerSubFolderName(parent, folderName); + this._nextAction = res => { + if (res.mailboxes.length) { + // Already exists. + this._actionDone(); + return; + } + // Create one and subscribe to it. + this._actionCreateAndSubscribe(mailboxName, res => { + this._actionList(mailboxName, () => this._actionDone()); + }); + }; + this._sendTagged(`LIST "" "${mailboxName}"`); + } + + /** + * Create a folder on the server. + * + * @param {nsIMsgFolder} parent - The parent folder to check. + * @param {string} folderName - The folder name. + */ + createFolder(parent, folderName) { + this._logger.debug("createFolder", parent.URI, folderName); + let mailboxName = this._getServerSubFolderName(parent, folderName); + this._actionCreateAndSubscribe(mailboxName, res => { + this._actionList(mailboxName, () => this._actionDone()); + }); + } + + /** + * Subscribe a folder. + * + * @param {nsIMsgFolder} folder - The folder to subscribe. + * @param {string} folderName - The folder name. + */ + subscribeFolder(folder, folderName) { + this._logger.debug("subscribeFolder", folder.URI, folderName); + this._nextAction = () => this._server.performExpand(); + this._sendTagged(`SUBSCRIBE "${folderName}"`); + } + + /** + * Unsubscribe a folder. + * + * @param {nsIMsgFolder} folder - The folder to unsubscribe. + * @param {string} folderName - The folder name. + */ + unsubscribeFolder(folder, folderName) { + this._logger.debug("unsubscribeFolder", folder.URI, folderName); + this._nextAction = () => this._server.performExpand(); + this._sendTagged(`UNSUBSCRIBE "${folderName}"`); + } + + /** + * Fetch the attribute of messages. + * + * @param {nsIMsgFolder} folder - The folder to check. + * @param {string} uids - The message uids. + * @param {string} attribute - The message attribute to fetch + */ + fetchMsgAttribute(folder, uids, attribute) { + this._logger.debug("fetchMsgAttribute", folder.URI, uids, attribute); + this._nextAction = res => { + if (res.done) { + let resultAttributes = res.messages + .map(m => m.customAttributes[attribute]) + .flat(); + this.runningUri.QueryInterface(Ci.nsIImapUrl).customAttributeResult = + resultAttributes.length > 1 + ? `(${resultAttributes.join(" ")})` + : resultAttributes[0]; + this._actionDone(); + } + }; + this._sendTagged(`UID FETCH ${uids} (${attribute})`); + } + + /** + * Delete all the messages in a folder. + * + * @param {nsIMsgFolder} folder - The folder to delete messages. + */ + deleteAllMessages(folder) { + this._logger.debug("deleteAllMessages", folder.URI); + this._actionInFolder(folder, () => { + if (!this._messages.size) { + this._actionDone(); + return; + } + + this._nextAction = () => this.expunge(folder); + this._sendTagged("UID STORE 1:* +FLAGS.SILENT (\\Deleted)"); + }); + } + + /** + * Search in a folder. + * + * @param {nsIMsgFolder} folder - The folder to delete messages. + * @param {string} searchCommand - The SEARCH command together with the search + * criteria. + */ + search(folder, searchCommand) { + this._logger.debug("search", folder.URI); + this._actionInFolder(folder, () => { + this._nextAction = res => { + this.onData(res.search); + this._actionDone(); + }; + this._sendTagged(`UID ${searchCommand}`); + }); + } + + /** + * Get the names of all ancestor folders. For example, + * folder a/b/c will return ['a', 'b']. + * + * @param {nsIMsgFolder} folder - The input folder. + * @returns {string[]} + */ + _getAncestorFolderNames(folder) { + let matches = /imap:\/\/[^/]+\/(.+)/.exec(folder.URI); + return matches[1].split("/").slice(0, -1); + } + + /** + * When UTF8 is enabled, use the name directly. Otherwise, encode to mUTF-7. + * + * @param {string} name - The mailbox name. + */ + _encodeMailboxName(name) { + return this._utf8Enabled ? name : this._charsetManager.unicodeToMutf7(name); + } + + /** + * Get the server name of a msg folder. + * + * @param {nsIMsgFolder} folder - The input folder. + * @returns {string} + */ + _getServerFolderName(folder) { + if (folder.isServer) { + return ""; + } + + if (folder.onlineName) { + return folder.onlineName.replaceAll('"', '\\"'); + } + let delimiter = + folder.QueryInterface(Ci.nsIMsgImapMailFolder).hierarchyDelimiter || "/"; + let names = this._getAncestorFolderNames(folder); + return this._encodeMailboxName( + [...names, folder.name].join(delimiter) + ).replaceAll('"', '\\"'); + } + + /** + * Get the server name of a sub folder. The sub folder may or may not exist on + * the server. + * + * @param {nsIMsgFolder} parent - The parent folder. + * @param {string} folderName - The sub folder name. + * @returns {string} + */ + _getServerSubFolderName(parent, folderName) { + folderName = this._encodeMailboxName(folderName); + let mailboxName = this._getServerFolderName(parent); + if (mailboxName) { + let delimiter = parent.QueryInterface( + Ci.nsIMsgImapMailFolder + ).hierarchyDelimiter; + // @see nsImapCore.h. + const ONLINE_HIERARCHY_SEPARATOR_UNKNOWN = "^"; + if (!delimiter || delimiter == ONLINE_HIERARCHY_SEPARATOR_UNKNOWN) { + delimiter = "/"; + } + return mailboxName + delimiter + folderName; + } + return folderName; + } + + /** + * Fetch the full content of a message by UID. + * + * @param {nsIMsgFolder} folder - The associated folder. + * @param {number} uid - The message uid. + * @param {number} [size] - The body size to fetch. + */ + fetchMessage(folder, uid, size) { + this._logger.debug(`fetchMessage folder=${folder.name} uid=${uid}`); + if (folder.hasMsgOffline(uid, null, 10)) { + this.onDone = () => {}; + this.channel?.readFromLocalCache(); + this._actionDone(); + return; + } + this._actionInFolder(folder, () => { + this._nextAction = this._actionUidFetchBodyResponse; + let command; + if (size) { + command = `UID FETCH ${uid} (UID RFC822.SIZE FLAGS BODY.PEEK[HEADER.FIELDS (Content-Type Content-Transfer-Encoding)] BODY.PEEK[TEXT]<0.${size}>)`; + } else { + command = `UID FETCH ${uid} (UID RFC822.SIZE FLAGS BODY.PEEK[])`; + } + this._sendTagged(command); + }); + } + + /** + * Add, remove or replace flags of specified messages. + * + * @param {string} action - "+" means add, "-" means remove, "" means replace. + * @param {nsIMsgFolder} folder - The target folder. + * @param {string} messageIds - Message UIDs, e.g. "23,30:33". + * @param {number} flags - The internal flags number to update. + */ + updateMessageFlags(action, folder, messageIds, flags) { + this._actionInFolder(folder, () => { + this._nextAction = () => this._actionDone(); + // _supportedFlags is available after _actionSelectResponse. + let flagsStr = ImapUtils.flagsToString(flags, this._supportedFlags); + this._sendTagged(`UID STORE ${messageIds} ${action}FLAGS (${flagsStr})`); + }); + } + + /** + * Send EXPUNGE command to a folder. + * + * @param {nsIMsgFolder} folder - The associated folder. + */ + expunge(folder) { + this._actionInFolder(folder, () => { + this._nextAction = () => this._actionDone(); + this._sendTagged("EXPUNGE"); + }); + } + + /** + * Move or copy messages from a folder to another folder. + * + * @param {nsIMsgFolder} folder - The source folder. + * @param {nsIMsgFolder} folder - The target folder. + * @param {string} messageIds - The message identifiers. + * @param {boolean} idsAreUids - If true messageIds are UIDs, otherwise, + * messageIds are sequences. + * @param {boolean} isMove - If true, use MOVE command when supported. + */ + copy(folder, dstFolder, messageIds, idsAreUids, isMove) { + let command = idsAreUids ? "UID " : ""; + command += + isMove && this._capabilities.includes("MOVE") + ? "MOVE " // rfc6851 + : "COPY "; + command += messageIds + ` "${this._getServerFolderName(dstFolder)}"`; + this._actionInFolder(folder, () => { + this._nextAction = this._actionNoopResponse; + this._sendTagged(command); + }); + } + + /** + * Upload a message file to a folder. + * + * @param {nsIFile} file - The message file to upload. + * @param {nsIMsgFolder} dstFolder - The target folder. + * @param {nsImapMailCopyState} copyState - A state used by nsImapMailFolder. + * @param {boolean} isDraft - Is the uploaded file a draft. + */ + async uploadMessageFromFile(file, dstFolder, copyState, isDraft) { + this._logger.debug("uploadMessageFromFile", file.path, dstFolder.URI); + let mailbox = this._getServerFolderName(dstFolder); + let content = MailStringUtils.uint8ArrayToByteString( + await IOUtils.read(file.path) + ); + this._nextAction = res => { + if (res.tag != "+") { + this._actionDone(Cr.NS_ERROR_FAILURE); + return; + } + this._nextAction = res => { + this._folderSink = dstFolder.QueryInterface(Ci.nsIImapMailFolderSink); + if ( + // See rfc4315. + this._capabilities.includes("UIDPLUS") && + res.attributes.appenduid + ) { + // The response is like `<tag> OK [APPENDUID <uidvalidity> <uid>]`. + this._folderSink.setAppendMsgUid( + res.attributes.appenduid[1], + this.runningUri + ); + } + this._actionDone(); + if (res.exists) { + // FIXME: _actionNoopResponse should be enough here, but it breaks + // test_imapAttachmentSaves.js. + this.folder = null; + } + try { + this._folderSink.copyNextStreamMessage(true, copyState); + } catch (e) { + this._logger.warn("copyNextStreamMessage failed", e); + } + }; + this._send(content + (this._utf8Enabled ? ")" : "")); + }; + let outKeywords = {}; + let flags = dstFolder + .QueryInterface(Ci.nsIImapMessageSink) + .getCurMoveCopyMessageInfo(this.runningUri, {}, outKeywords); + let flagString = ImapUtils.flagsToString(flags, this._supportedFlags); + if (isDraft && !/\b\Draft\b/.test(flagString)) { + flagString += " \\Draft"; + } + if (outKeywords.value) { + flagString += " " + outKeywords.value; + } + let open = this._utf8Enabled ? "UTF8 (~{" : "{"; + let command = `APPEND "${mailbox}" (${flagString.trim()}) ${open}${ + content.length + }}`; + this._sendTagged(command); + } + + /** + * Check the status of a folder. + * + * @param {nsIMsgFolder} folder - The folder to check. + */ + updateFolderStatus(folder) { + this._logger.debug("updateFolderStatus", folder.URI); + if (this._folder == folder) { + // According to rfc3501, "the STATUS command SHOULD NOT be used on the + // currently selected mailbox", so use NOOP instead. + this._actionNoop(); + return; + } + + this._nextAction = res => { + if (res.status == "OK") { + folder + .QueryInterface(Ci.nsIImapMailFolderSink) + .UpdateImapMailboxStatus(this, { + QueryInterface: ChromeUtils.generateQI(["nsIMailboxSpec"]), + nextUID: res.attributes.uidnext, + numMessages: res.attributes.messages.length, + numUnseenMessages: res.attributes.unseen, + }); + folder.msgDatabase = null; + } + this._actionDone(); + }; + this._sendTagged( + `STATUS "${this._getServerFolderName(folder)}" (UIDNEXT MESSAGES UNSEEN)` + ); + } + + /** + * Update message flags. + * + * @param {nsIMsgFolder} folder - The associated folder. + * @param {string} flagsToAdd - The flags to add. + * @param {string} flagsToSubtract - The flags to subtract. + * @param {string} uids - The message uids. + */ + storeCustomKeywords(folder, flagsToAdd, flagsToSubtract, uids) { + this._logger.debug( + "storeCustomKeywords", + folder.URI, + flagsToAdd, + flagsToSubtract, + uids + ); + let subtractFlags = () => { + if (flagsToSubtract) { + this._nextAction = () => { + this._actionDone(); + }; + this._sendTagged(`UID STORE ${uids} -FLAGS (${flagsToSubtract})`); + } else { + this._actionDone(); + } + }; + this._actionInFolder(folder, () => { + if (flagsToAdd) { + this._nextAction = () => { + subtractFlags(); + }; + this._sendTagged(`UID STORE ${uids} +FLAGS (${flagsToAdd})`); + } else { + subtractFlags(); + } + }); + } + + /** + * Get message headers by the specified uids. + * + * @param {nsIMsgFolder} folder - The folder of the messages. + * @param {string[]} uids - The message uids. + */ + getHeaders(folder, uids) { + this._logger.debug("getHeaders", folder.URI, uids); + this._actionInFolder(folder, () => { + this._nextAction = this._actionUidFetchHeaderResponse; + let extraItems = ""; + if (this._server.isGMailServer) { + extraItems += "X-GM-MSGID X-GM-THRID X-GM-LABELS "; + } + this._sendTagged( + `UID FETCH ${uids} (UID ${extraItems}RFC822.SIZE FLAGS BODY.PEEK[HEADER])` + ); + }); + } + + /** + * Send IDLE command to the server. + */ + idle() { + if (!this.folder) { + this._actionDone(); + return; + } + this._nextAction = res => { + if (res.tag == "*") { + this.folder.performingBiff = true; + this._actionNoopResponse(res); + } + }; + this._sendTagged("IDLE"); + this._setSocketTimeout(PR_UINT32_MAX); + this._idling = true; + this._idleTimer = setTimeout(() => { + this.endIdle(() => { + this._actionNoop(); + }); + // Per rfc2177, should terminate the IDLE and re-issue it at least every + // 29 minutes. But in practice many servers timeout before that. A noop + // every 5min is better than timeout. + }, 5 * 60 * 1000); + this._logger.debug(`Idling in ${this.folder.URI}`); + } + + /** + * Send DONE to end the IDLE command. + * + * @param {Function} nextAction - Callback function after IDLE is ended. + */ + endIdle(nextAction) { + this._nextAction = res => { + if (res.status == "OK") { + nextAction(); + } + }; + this._send("DONE"); + this._idling = false; + this.busy = true; + clearTimeout(this._idleTimer); + this._idleTimer = null; + } + + /** + * Send LOGOUT and close the socket. + */ + logout() { + this._sendTagged("LOGOUT"); + this._socket.close(); + } + + /** + * The open event handler. + */ + _onOpen = () => { + this._logger.debug("Connected"); + this._socket.ondata = this._onData; + this._socket.onclose = this._onClose; + this._nextAction = res => { + this._greeted = true; + this._actionCapabilityResponse(res); + }; + + this._setSocketTimeout(this._prefs.tcpTimeout); + }; + + /** + * The data event handler. + * + * @param {TCPSocketEvent} event - The data event. + */ + _onData = async event => { + // Without this, some tests are blocked waiting for response from Maild.jsm. + // Don't know the real cause, but possibly because ImapClient and Maild runs + // on the same process. We also have this in Pop3Client. + await new Promise(resolve => setTimeout(resolve)); + + let stringPayload = this._utf8Enabled + ? new TextDecoder().decode(event.data) + : MailStringUtils.uint8ArrayToByteString(new Uint8Array(event.data)); + this._logger.debug(`S: ${stringPayload}`); + if (!this._response || this._idling || this._response.done) { + this._response = new ImapResponse(); + this._response.onMessage = this._onMessage; + } + this._response.parse(stringPayload); + if ( + !this._authenticating && + this._response.done && + this._response.status && + this._response.tag != "+" && + !["OK", "+"].includes(this._response.status) + ) { + this._actionDone(ImapUtils.NS_MSG_ERROR_IMAP_COMMAND_FAILED); + return; + } + if (!this._greeted || this._idling || this._response.done) { + this._nextAction?.(this._response); + } + }; + + /** + * The error event handler. + * + * @param {TCPSocketErrorEvent} event - The error event. + */ + _onError = async event => { + this._logger.error(`${event.name}: a ${event.message} error occurred`); + if (event.errorCode == Cr.NS_ERROR_NET_TIMEOUT) { + this._actionError("imapNetTimeoutError"); + this._actionDone(event.errorCode); + return; + } + + let secInfo = + await event.target.transport?.tlsSocketControl?.asyncGetSecurityInfo(); + if (secInfo) { + this._logger.error(`SecurityError info: ${secInfo.errorCodeString}`); + if (secInfo.failedCertChain.length) { + let chain = secInfo.failedCertChain.map(c => { + return c.commonName + "; serial# " + c.serialNumber; + }); + this._logger.error(`SecurityError cert chain: ${chain.join(" <- ")}`); + } + this.runningUri.failedSecInfo = secInfo; + this._server.closeCachedConnections(); + } else { + this.logout(); + } + + this._actionDone(event.errorCode); + }; + + /** + * The close event handler. + */ + _onClose = () => { + this._logger.debug("Connection closed."); + this.folder = null; + }; + + /** + * Send a command to the server. + * + * @param {string} str - The command string to send. + * @param {boolean} [suppressLogging=false] - Whether to suppress logging the str. + */ + _send(str, suppressLogging) { + if (suppressLogging && AppConstants.MOZ_UPDATE_CHANNEL != "default") { + this._logger.debug( + "C: Logging suppressed (it probably contained auth information)" + ); + } else { + // Do not suppress for non-release builds, so that debugging auth problems + // is easier. + this._logger.debug(`C: ${str}`); + } + + if (!this.isOnline) { + if (!str.includes("LOGOUT")) { + this._logger.warn( + `Failed to send because socket state is ${this._socket?.readyState}` + ); + } + return; + } + + let encode = this._utf8Enabled + ? x => new TextEncoder().encode(x) + : MailStringUtils.byteStringToUint8Array; + this._socket.send(encode(str + "\r\n").buffer); + } + + /** + * Same as _send, but prepend a tag to the command. + */ + _sendTagged(str, suppressLogging) { + if (this._idling) { + let nextAction = this._nextAction; + this.endIdle(() => { + this._nextAction = nextAction; + this._sendTagged(str, suppressLogging); + }); + } else { + this._send(`${this._getNextTag()} ${str}`, suppressLogging); + } + } + + /** + * Get the next command tag. + * + * @returns {number} + */ + _getNextTag() { + this._tag = (this._tag + 1) % 100; + return this._tag; + } + + /** + * Send CAPABILITY command to the server. + */ + _actionCapability() { + this._nextAction = this._actionCapabilityResponse; + this._sendTagged("CAPABILITY"); + } + + /** + * Handle the capability response. + * + * @param {ImapResponse} res - Response received from the server. + */ + _actionCapabilityResponse = res => { + if (res.capabilities) { + this._capabilities = res.capabilities; + this._server.wrappedJSObject.capabilities = res.capabilities; + if (this._capabilities.includes("X-GM-EXT-1")) { + this._server.isGMailServer = true; + } + + this._supportedAuthMethods = res.authMethods; + this._actionChooseFirstAuthMethod(); + } else { + this._actionCapability(); + } + }; + + /** + * Decide the first auth method to try. + */ + _actionChooseFirstAuthMethod = () => { + if ( + [ + Ci.nsMsgSocketType.trySTARTTLS, + Ci.nsMsgSocketType.alwaysSTARTTLS, + ].includes(this._server.socketType) && + !this._secureTransport + ) { + if (this._capabilities.includes("STARTTLS")) { + // Init STARTTLS negotiation if required by user pref and supported. + this._nextAction = this._actionStarttlsResponse; + this._sendTagged("STARTTLS"); + } else { + // Abort if not supported. + this._logger.error("Server doesn't support STARTTLS. Aborting."); + this._actionError("imapServerDisconnected"); + this._actionDone(Cr.NS_ERROR_FAILURE); + } + return; + } + + this._possibleAuthMethods = this._preferredAuthMethods.filter(x => + this._supportedAuthMethods.includes(x) + ); + if ( + !this._possibleAuthMethods.length && + this._server.authMethod == Ci.nsMsgAuthMethod.passwordCleartext && + !this._capabilities.includes("LOGINDISABLED") + ) { + this._possibleAuthMethods = ["OLDLOGIN"]; + } + this._logger.debug(`Possible auth methods: ${this._possibleAuthMethods}`); + this._nextAuthMethod = this._possibleAuthMethods[0]; + if (this._capabilities.includes("CLIENTID") && this._server.clientid) { + this._nextAction = res => { + if (res.status == "OK") { + this._actionAuth(); + } else { + this._actionDone(Cr.NS_ERROR_FAILURE); + } + }; + this._sendTagged(`CLIENTID UUID ${this._server.clientid}`); + } else { + this._actionAuth(); + } + }; + + /** + * Handle the STARTTLS response. + * + * @param {ImapResponse} res - The server response. + */ + _actionStarttlsResponse(res) { + if (!res.status == "OK") { + this._actionDone(Cr.NS_ERROR_FAILURE); + return; + } + this._socket.upgradeToSecure(); + this._secureTransport = true; + this._actionCapability(); + } + + /** + * Init authentication depending on server capabilities and user prefs. + */ + _actionAuth = async () => { + if (!this._nextAuthMethod) { + this._socket.close(); + this._actionDone(Cr.NS_ERROR_FAILURE); + return; + } + + this._authenticating = true; + + this._currentAuthMethod = this._nextAuthMethod; + this._nextAuthMethod = + this._possibleAuthMethods[ + this._possibleAuthMethods.indexOf(this._currentAuthMethod) + 1 + ]; + + switch (this._currentAuthMethod) { + case "OLDLOGIN": + this._nextAction = this._actionAuthResponse; + let password = await this._getPassword(); + this._sendTagged( + `LOGIN ${this._authenticator.username} ${password}`, + true + ); + break; + case "PLAIN": + this._nextAction = this._actionAuthPlain; + this._sendTagged("AUTHENTICATE PLAIN"); + break; + case "LOGIN": + this._nextAction = this._actionAuthLoginUser; + this._sendTagged("AUTHENTICATE LOGIN"); + break; + case "CRAM-MD5": + this._nextAction = this._actionAuthCramMd5; + this._sendTagged("AUTHENTICATE CRAM-MD5"); + break; + case "GSSAPI": { + this._nextAction = this._actionAuthGssapi; + this._authenticator.initGssapiAuth("imap"); + let token; + try { + token = this._authenticator.getNextGssapiToken(""); + } catch (e) { + this._logger.error(e); + this._actionDone(Cr.NS_ERROR_FAILURE); + return; + } + this._sendTagged(`AUTHENTICATE GSSAPI ${token}`, true); + break; + } + case "NTLM": { + this._nextAction = this._actionAuthNtlm; + this._authenticator.initNtlmAuth("imap"); + let token; + try { + token = this._authenticator.getNextNtlmToken(""); + } catch (e) { + this._logger.error(e); + this._actionDone(Cr.NS_ERROR_FAILURE); + return; + } + this._sendTagged(`AUTHENTICATE NTLM ${token}`, true); + break; + } + case "XOAUTH2": + this._nextAction = this._actionAuthResponse; + let token = await this._authenticator.getOAuthToken(); + this._sendTagged(`AUTHENTICATE XOAUTH2 ${token}`, true); + break; + case "EXTERNAL": + this._nextAction = this._actionAuthResponse; + this._sendTagged( + `AUTHENTICATE EXTERNAL ${this._authenticator.username}` + ); + break; + default: + this._actionDone(); + } + }; + + /** + * @param {ImapResponse} res - Response received from the server. + */ + _actionAuthResponse = res => { + this._authenticating = false; + + if (this.verifyLogon) { + this._actionDone(res.status == "OK" ? Cr.NS_OK : Cr.NS_ERROR_FAILURE); + return; + } + if (res.status == "OK") { + this._serverSink.userAuthenticated = true; + if (res.capabilities) { + this._capabilities = res.capabilities; + this._server.wrappedJSObject.capabilities = res.capabilities; + this._actionId(); + } else { + this._nextAction = res => { + this._capabilities = res.capabilities; + this._server.wrappedJSObject.capabilities = res.capabilities; + this._actionId(); + }; + this._sendTagged("CAPABILITY"); + } + return; + } + if ( + ["OLDLOGIN", "PLAIN", "LOGIN", "CRAM-MD5"].includes( + this._currentAuthMethod + ) + ) { + // Ask user what to do. + let action = this._authenticator.promptAuthFailed(); + if (action == 1) { + // Cancel button pressed. + this._socket.close(); + this._actionDone(Cr.NS_ERROR_FAILURE); + return; + } + if (action == 2) { + // 'New password' button pressed. + this._authenticator.forgetPassword(); + } + + // Retry. + this._nextAuthMethod = this._possibleAuthMethods[0]; + this._actionAuth(); + return; + } + this._logger.error("Authentication failed."); + this._actionDone(Cr.NS_ERROR_FAILURE); + }; + + /** + * Returns the saved/cached server password, or show a password dialog. If the + * user cancels the dialog, stop the process. + * + * @returns {string} The server password. + */ + async _getPassword() { + try { + let password = await this._authenticator.getPassword(); + return password; + } catch (e) { + if (e.result == Cr.NS_ERROR_ABORT) { + this._actionDone(e.result); + } + throw e; + } + } + + /** + * The second step of PLAIN auth. Send the auth token to the server. + * + * @param {ImapResponse} res - Response received from the server. + */ + _actionAuthPlain = async res => { + this._nextAction = this._actionAuthResponse; + this._send(await this._authenticator.getPlainToken(), true); + }; + + /** + * The second step of LOGIN auth. Send the username to the server. + * + * @param {ImapResponse} res - The server response. + */ + _actionAuthLoginUser = res => { + this._nextAction = this._actionAuthLoginPass; + this._send(btoa(this._authenticator.username), true); + }; + + /** + * The third step of LOGIN auth. Send the password to the server. + * + * @param {ImapResponse} res - The server response. + */ + _actionAuthLoginPass = async res => { + this._nextAction = this._actionAuthResponse; + let password = MailStringUtils.stringToByteString( + await this._getPassword() + ); + this._send(btoa(password), true); + }; + + /** + * The second step of CRAM-MD5 auth, send a HMAC-MD5 signature to the server. + * + * @param {ImapResponse} res - The server response. + */ + _actionAuthCramMd5 = async res => { + this._nextAction = this._actionAuthResponse; + let password = await this._getPassword(); + this._send( + this._authenticator.getCramMd5Token(password, res.statusText), + true + ); + }; + + /** + * The second and next step of GSSAPI auth. + * + * @param {ImapResponse} res - The server response. + */ + _actionAuthGssapi = res => { + if (res.tag != "+") { + this._actionAuthResponse(res); + return; + } + + // Server returns a challenge, we send a new token. Can happen multiple times. + let token; + try { + token = this._authenticator.getNextGssapiToken(res.statusText); + } catch (e) { + this._logger.error(e); + this._actionAuthResponse(res); + return; + } + this._send(token, true); + }; + + /** + * The second and next step of NTLM auth. + * + * @param {ImapResponse} res - The server response. + */ + _actionAuthNtlm = res => { + if (res.tag != "+") { + this._actionAuthResponse(res); + return; + } + + // Server returns a challenge, we send a new token. Can happen multiple times. + let token; + try { + token = this._authenticator.getNextNtlmToken(res.statusText); + } catch (e) { + this._logger.error(e); + this._actionAuthResponse(res); + return; + } + this._send(token, true); + }; + + /** + * Send ID command to the server. + * + * @param {Function} [actionAfter] - A callback after processing ID command. + */ + _actionId = (actionAfter = this._actionEnableUtf8) => { + if (this._capabilities.includes("ID") && Services.appinfo.name) { + this._nextAction = res => { + this._server.serverIDPref = res.id; + actionAfter(); + }; + this._sendTagged( + `ID ("name" "${Services.appinfo.name}" "version" "${Services.appinfo.version}")` + ); + } else { + actionAfter(); + } + }; + + /** + * Enable UTF8 if supported by the server. + * + * @param {Function} [actionAfter] - A callback after processing ENABLE UTF8. + */ + _actionEnableUtf8 = (actionAfter = this._discoverAllFoldersIfNecessary) => { + if ( + this._server.allowUTF8Accept && + (this._capabilities.includes("UTF8=ACCEPT") || + this._capabilities.includes("UTF8=ONLY")) + ) { + this._nextAction = res => { + this._utf8Enabled = res.status == "OK"; + this._server.utf8AcceptEnabled = this._utf8Enabled; + actionAfter(); + }; + this._sendTagged("ENABLE UTF8=ACCEPT"); + } else { + this._utf8Enabled = false; + actionAfter(); + } + }; + + /** + * Execute an action with a folder selected. + * + * @param {nsIMsgFolder} folder - The folder to select. + * @param {Function} actionInFolder - The action to execute. + */ + _actionInFolder(folder, actionInFolder) { + if (this.folder == folder) { + // If already in the folder, execute the action now. + actionInFolder(); + } else { + // Send the SELECT command and queue the action. + this._actionAfterSelectFolder = actionInFolder; + this._nextAction = this._actionSelectResponse(folder); + this._sendTagged(`SELECT "${this._getServerFolderName(folder)}"`); + } + } + + /** + * Send LSUB or LIST command depending on the server capabilities. + * + * @param {string} [mailbox="*"] - The mailbox to list, default to list all. + */ + _actionListOrLsub(mailbox = "*") { + this._nextAction = this._actionListResponse(); + let command = this._capabilities.includes("LIST-EXTENDED") + ? "LIST (SUBSCRIBED)" // rfc5258 + : "LSUB"; + command += ` "" "${mailbox}"`; + if (this._capabilities.includes("SPECIAL-USE")) { + command += " RETURN (SPECIAL-USE)"; // rfc6154 + } + this._sendTagged(command); + this._listInboxSent = false; + } + + /** + * Handle LIST response. + * + * @param {Function} actionAfterResponse - A callback after handling the response. + * @param {ImapResponse} res - Response received from the server. + */ + _actionListResponse = + (actionAfterResponse = this._actionFinishFolderDiscovery) => + res => { + if (!this._hasInbox) { + this._hasInbox = res.mailboxes.some( + mailbox => mailbox.flags & ImapUtils.FLAG_IMAP_INBOX + ); + } + for (let mailbox of res.mailboxes) { + this._serverSink.possibleImapMailbox( + mailbox.name.replaceAll(mailbox.delimiter, "/"), + mailbox.delimiter, + mailbox.flags + ); + } + + actionAfterResponse(res); + }; + + /** + * Send LIST command. + * + * @param {string} folderName - The name of the folder to list. + * @param {Function} actionAfterResponse - A callback after handling the response. + */ + _actionList(folderName, actionAfterResponse) { + this._nextAction = this._actionListResponse(actionAfterResponse); + this._sendTagged(`LIST "" "${folderName}"`); + } + + /** + * Finish folder discovery after checking Inbox and Trash folders. + */ + _actionFinishFolderDiscovery = () => { + if (!this._hasInbox && !this._listInboxSent) { + this._actionList("INBOX"); + this._listInboxSent = true; + return; + } + if (!this._hasTrash && !this._listTrashSent) { + this._actionCreateTrashFolderIfNeeded(); + return; + } + this._serverSink.discoveryDone(); + this._actionAfterDiscoverAllFolders + ? this._actionAfterDiscoverAllFolders() + : this._actionDone(); + }; + + /** + * If Trash folder is not found on server, create one and subscribe to it. + */ + _actionCreateTrashFolderIfNeeded() { + let trashFolderName = this._server.trashFolderName; + this._actionList(trashFolderName, res => { + this._hasTrash = res.mailboxes.length > 0; + if (this._hasTrash) { + // Trash folder exists. + this._actionFinishFolderDiscovery(); + } else { + // Trash folder doesn't exist, create one and subscribe to it. + this._nextAction = res => { + this._actionList(trashFolderName, () => { + // After subscribing, finish folder discovery. + this._nextAction = this._actionFinishFolderDiscovery; + this._sendTagged(`SUBSCRIBE "${trashFolderName}"`); + }); + }; + this._sendTagged(`CREATE "${trashFolderName}"`); + } + }); + this._listTrashSent = true; + } + + /** + * Create and subscribe to a folder. + * + * @param {string} folderName - The folder name. + * @param {Function} callbackAfterSubscribe - The action after the subscribe + * command. + */ + _actionCreateAndSubscribe(folderName, callbackAfterSubscribe) { + this._nextAction = res => { + this._nextAction = callbackAfterSubscribe; + this._sendTagged(`SUBSCRIBE "${folderName}"`); + }; + this._sendTagged(`CREATE "${folderName}"`); + } + + /** + * Handle SELECT response. + */ + _actionSelectResponse = folder => res => { + if (folder) { + this.folder = folder; + } + this._supportedFlags = res.permanentflags || res.flags; + this._folderState = res; + if (this._capabilities.includes("QUOTA")) { + this._actionGetQuotaData(); + } else { + this._actionAfterSelectFolder(); + } + }; + + /** + * Send GETQUOTAROOT command and handle the response. + */ + _actionGetQuotaData() { + this._folderSink = this.folder.QueryInterface(Ci.nsIImapMailFolderSink); + this._nextAction = res => { + const INVALIDATE_QUOTA = 0; + const STORE_QUOTA = 1; + const VALIDATE_QUOTA = 2; + for (let root of res.quotaRoots || []) { + this._folderSink.setFolderQuotaData(INVALIDATE_QUOTA, root, 0, 0); + } + for (let [mailbox, resource, usage, limit] of res.quotas || []) { + this._folderSink.setFolderQuotaData( + STORE_QUOTA, + mailbox ? `${mailbox} / ${resource}` : resource, + usage, + limit + ); + } + this._folderSink.setFolderQuotaData(VALIDATE_QUOTA, "", 0, 0); + this._actionAfterSelectFolder(); + }; + this._sendTagged( + `GETQUOTAROOT "${this._getServerFolderName(this.folder)}"` + ); + this._folderSink.folderQuotaCommandIssued = true; + } + + /** + * Handle RENAME response. Three steps are involved. + * + * @param {string} oldName - The old folder name. + * @param {string} newName - The new folder name. + * @param {boolean} [isMove] - Is it response to MOVE command. + * @param {ImapResponse} res - The server response. + */ + _actionRenameResponse = (oldName, newName, isMove) => res => { + // Step 3: Rename the local folder and send LIST command to re-sync folders. + let actionAfterUnsubscribe = () => { + this._serverSink.onlineFolderRename(this._msgWindow, oldName, newName); + if (isMove) { + this._actionDone(); + } else { + this._actionListOrLsub(newName); + } + }; + // Step 2: unsubscribe to the oldName. + this._nextAction = () => { + this._nextAction = actionAfterUnsubscribe; + this._sendTagged(`UNSUBSCRIBE "${oldName}"`); + }; + // Step 1: subscribe to the newName. + this._sendTagged(`SUBSCRIBE "${newName}"`); + }; + + /** + * Send UID FETCH request to the server. + */ + _actionUidFetch() { + if (this.runningUri.imapAction == Ci.nsIImapUrl.nsImapLiteSelectFolder) { + this._nextAction = () => this._actionDone(); + } else { + this._nextAction = this._actionUidFetchResponse; + } + this._sendTagged("UID FETCH 1:* (FLAGS)"); + } + + /** + * Handle UID FETCH response. + * + * @param {ImapResponse} res - Response received from the server. + */ + _actionUidFetchResponse(res) { + let outFolderInfo = {}; + this.folder.getDBFolderInfoAndDB(outFolderInfo); + let highestUid = outFolderInfo.value.getUint32Property( + "highestRecordedUID", + 0 + ); + this._folderSink = this.folder.QueryInterface(Ci.nsIImapMailFolderSink); + this._folderSink.UpdateImapMailboxInfo(this, this._getMailboxSpec()); + let latestUid = this._messageUids.at(-1); + if (latestUid > highestUid) { + let extraItems = ""; + if (this._server.isGMailServer) { + extraItems += "X-GM-MSGID X-GM-THRID X-GM-LABELS "; + } + this._nextAction = this._actionUidFetchHeaderResponse; + this._sendTagged( + `UID FETCH ${ + highestUid + 1 + }:${latestUid} (UID ${extraItems}RFC822.SIZE FLAGS BODY.PEEK[HEADER])` + ); + } else { + this._folderSink.headerFetchCompleted(this); + if (this._bodysToDownload.length) { + let uids = this._bodysToDownload.join(","); + this._nextAction = this._actionUidFetchBodyResponse; + this._sendTagged( + `UID FETCH ${uids} (UID RFC822.SIZE FLAGS BODY.PEEK[])` + ); + return; + } + this._actionDone(); + } + } + + /** + * Make an nsIMailboxSpec instance to interact with nsIImapMailFolderSink. + * + * @returns {nsIMailboxSpec} + */ + _getMailboxSpec() { + return { + QueryInterface: ChromeUtils.generateQI(["nsIMailboxSpec"]), + folder_UIDVALIDITY: this._folderState.uidvalidity, + box_flags: this._folderState.flags, + supportedUserFlags: this._folderState.supportedUserFlags, + nextUID: this._folderState.attributes.uidnext, + numMessages: this._messages.size, + numUnseenMessages: this._folderState.attributes.unseen, + flagState: this.flagAndUidState, + }; + } + + /** + * Handle UID FETCH BODY.PEEK[HEADER] response. + * + * @param {ImapResponse} res - Response received from the server. + */ + _actionUidFetchHeaderResponse(res) { + this.folder + .QueryInterface(Ci.nsIImapMailFolderSink) + .headerFetchCompleted(this); + if (this._bodysToDownload.length) { + // nsImapMailFolder decides to fetch the full body by calling + // NotifyBodysToDownload. + let uids = this._bodysToDownload.join(","); + this._nextAction = this._actionUidFetchBodyResponse; + this._sendTagged(`UID FETCH ${uids} (UID RFC822.SIZE FLAGS BODY.PEEK[])`); + return; + } + this._actionDone(); + } + + /** + * Handle UID FETCH BODY response. + * + * @param {ImapResponse} res - Response received from the server. + */ + _actionUidFetchBodyResponse(res) { + this._actionDone(); + } + + /** + * Handle a single message data response. + * + * @param {MessageData} msg - Message data parsed in ImapResponse. + */ + _onMessage = msg => { + this._msgSink = this.folder.QueryInterface(Ci.nsIImapMessageSink); + this._folderSink = this.folder.QueryInterface(Ci.nsIImapMailFolderSink); + + // Handle message flags. + if ((msg.uid || msg.sequence) && msg.flags != undefined) { + let uid = msg.uid; + if (uid && msg.sequence) { + this._messageUids[msg.sequence] = uid; + this._messages.set(uid, msg); + } else if (msg.sequence) { + uid = this._messageUids[msg.sequence]; + } + if (uid) { + this.folder + .QueryInterface(Ci.nsIImapMessageSink) + .notifyMessageFlags( + msg.flags, + msg.keywords, + uid, + this._folderState.highestmodseq + ); + } + } + + if (msg.body) { + if (!msg.body.endsWith("\r\n")) { + msg.body += "\r\n"; + } + if (msg.bodySection.length == 1 && msg.bodySection[0] == "HEADER") { + // Handle message headers. + this._messageUids[msg.sequence] = msg.uid; + this._messages.set(msg.uid, msg); + this._folderSink.StartMessage(this.runningUri); + let hdrXferInfo = { + numHeaders: 1, + getHeader() { + return { + msgUid: msg.uid, + msgSize: msg.size, + get msgHdrs() { + let sepIndex = msg.body.indexOf("\r\n\r\n"); + return sepIndex == -1 + ? msg.body + "\r\n" + : msg.body.slice(0, sepIndex + 2); + }, + }; + }, + }; + this._folderSink.parseMsgHdrs(this, hdrXferInfo); + } else { + // Handle message body. + let shouldStoreMsgOffline = false; + try { + shouldStoreMsgOffline = this.folder.shouldStoreMsgOffline(msg.uid); + } catch (e) {} + if ( + (shouldStoreMsgOffline || + this.runningUri.QueryInterface(Ci.nsIImapUrl) + .storeResultsOffline) && + msg.body + ) { + this._folderSink.StartMessage(this.runningUri); + this._msgSink.parseAdoptedMsgLine(msg.body, msg.uid, this.runningUri); + this._msgSink.normalEndMsgWriteStream( + msg.uid, + true, + this.runningUri, + msg.body.length + ); + this._folderSink.EndMessage(this.runningUri, msg.uid); + } + + this.onData?.(msg.body); + // Release some memory. + msg.body = ""; + } + } + }; + + /** + * Send NOOP command. + */ + _actionNoop() { + this._nextAction = this._actionNoopResponse; + this._sendTagged("NOOP"); + } + + /** + * Handle NOOP response. + * + * @param {ImapResponse} res - Response received from the server. + */ + _actionNoopResponse(res) { + if ( + (res.exists && res.exists != this._folderState.exists) || + res.expunged.length + ) { + // Handle messages number changes, re-sync the folder. + this._folderState.exists = res.exists; + this._actionAfterSelectFolder = this._actionUidFetch; + this._nextAction = this._actionSelectResponse(); + if (res.expunged.length) { + this._messageUids = []; + this._messages.clear(); + } + let folder = this.folder; + this.folder = null; + this.selectFolder(folder); + } else if (res.messages.length || res.exists) { + let outFolderInfo = {}; + this.folder.getDBFolderInfoAndDB(outFolderInfo); + let highestUid = outFolderInfo.value.getUint32Property( + "highestRecordedUID", + 0 + ); + this._nextAction = this._actionUidFetchResponse; + this._sendTagged(`UID FETCH ${highestUid + 1}:* (FLAGS)`); + } else { + if (res.exists == 0) { + this._messageUids = []; + this._messages.clear(); + this.folder + .QueryInterface(Ci.nsIImapMailFolderSink) + .UpdateImapMailboxInfo(this, this._getMailboxSpec()); + } + if (!this._idling) { + this._actionDone(); + } + } + } + + /** + * Show an error prompt. + * + * @param {string} errorName - An error name corresponds to an entry of + * imapMsgs.properties. + */ + _actionError(errorName) { + if (!this._msgWindow) { + return; + } + let bundle = Services.strings.createBundle( + "chrome://messenger/locale/imapMsgs.properties" + ); + let errorMsg = bundle.formatStringFromName(errorName, [ + this._server.hostName, + ]); + Services.prompt.alert(this._msgWindow.domWindow, null, errorMsg); + } + + /** + * Finish a request and do necessary cleanup. + */ + _actionDone = (status = Cr.NS_OK) => { + this._logger.debug(`Done with status=${status}`); + this._nextAction = null; + this._urlListener?.OnStopRunningUrl(this.runningUri, status); + this.runningUri.SetUrlState(false, status); + this.onDone?.(status); + this._reset(); + // Tell ImapIncomingServer this client can be reused now. + this.onFree?.(); + }; + + /** @see nsIImapProtocol */ + NotifyBodysToDownload(keys) { + this._logger.debug("NotifyBodysToDownload", keys); + this._bodysToDownload = keys; + } + + GetRunningUrl() { + this._logger.debug("GetRunningUrl"); + } + + get flagAndUidState() { + // The server sequence is 1 based, nsIImapFlagAndUidState sequence is 0 based. + let getUidOfMessage = index => this._messageUids[index + 1]; + let getMessageFlagsByUid = uid => this._messages.get(uid)?.flags; + + return { + QueryInterface: ChromeUtils.generateQI(["nsIImapFlagAndUidState"]), + numberOfMessages: this._messages.size, + getUidOfMessage, + getMessageFlags: index => getMessageFlagsByUid(getUidOfMessage(index)), + hasMessage: uid => this._messages.has(uid), + getMessageFlagsByUid, + getCustomFlags: uid => this._messages.get(uid)?.keywords, + getCustomAttribute: (uid, name) => { + let value = this._messages.get(uid)?.customAttributes[name]; + return Array.isArray(value) ? value.join(" ") : value; + }, + }; + } +} diff --git a/comm/mailnews/imap/src/ImapFolderContentHandler.sys.mjs b/comm/mailnews/imap/src/ImapFolderContentHandler.sys.mjs new file mode 100644 index 0000000000..da33c08829 --- /dev/null +++ b/comm/mailnews/imap/src/ImapFolderContentHandler.sys.mjs @@ -0,0 +1,71 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +const lazy = {}; +XPCOMUtils.defineLazyModuleGetters(lazy, { + MailUtils: "resource:///modules/MailUtils.jsm", +}); + +/** + * A service for handling content type x-application-imapfolder; + * that is, opening IMAP folder URLs. + * + * Set mailnews.imap.jsmodule to true to use this module. + * + * @implements {nsIContentHandler} + */ +export class ImapFolderContentHandler { + QueryInterface = ChromeUtils.generateQI(["nsIContentHandler"]); + + /** + * @param contentType - The content type of request. + * @param windowContest - Window context, used to get things like the current + * nsIDOMWindow for this request. + * @param request - A request whose content type is already known. + * @see {nsIContentHandler} + */ + handleContent(contentType, windowContext, request) { + if (contentType != "x-application-imapfolder") { + throw Components.Exception( + `Won't handle ${contentType}`, + Cr.NS_ERROR_WONT_HANDLE_CONTENT + ); + } + request = request.QueryInterface(Ci.nsIChannel); + + let imapFolderURL = Services.io.unescapeString( + request.URI.spec, + Ci.nsINetUtil.ESCAPE_URL_PATH + ); + + if (Services.wm.getMostRecentWindow("mail:3pane")) { + // Clicked IMAP folder URL in the window. + let folder = MailServices.folderLookup.getFolderForURL(imapFolderURL); + if (folder) { + lazy.MailUtils.displayFolderIn3Pane(folder.URI); + } else { + folder = + MailServices.folderLookup.getOrCreateFolderForURL(imapFolderURL); + // TODO: ask and maybe subscribe, like + // https://searchfox.org/comm-central/rev/1dd06be9d6c1178a34e6c28db03161e07e97d98c/mailnews/imap/src/nsImapService.cpp#2471-2534 + dump(`Maybe subscribe to folder ${folder.URI}\n`); + } + } else { + // Got IMAP folder URL from command line (most likely). + Cc["@mozilla.org/messenger/windowservice;1"] + .getService(Ci.nsIMessengerWindowService) + .openMessengerWindowWithUri("mail:3pane", imapFolderURL, -1); + } + } +} + +ImapFolderContentHandler.prototype.classID = Components.ID( + "{d927a82f-2d15-4972-ab88-6d84601aae68}" +); diff --git a/comm/mailnews/imap/src/ImapIncomingServer.jsm b/comm/mailnews/imap/src/ImapIncomingServer.jsm new file mode 100644 index 0000000000..c9cf86bfdc --- /dev/null +++ b/comm/mailnews/imap/src/ImapIncomingServer.jsm @@ -0,0 +1,783 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["ImapIncomingServer"]; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +const { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +const { MsgIncomingServer } = ChromeUtils.import( + "resource:///modules/MsgIncomingServer.jsm" +); + +const lazy = {}; + +XPCOMUtils.defineLazyModuleGetters(lazy, { + ImapClient: "resource:///modules/ImapClient.jsm", + ImapCapFlags: "resource:///modules/ImapUtils.jsm", + ImapUtils: "resource:///modules/ImapUtils.jsm", + MailUtils: "resource:///modules/MailUtils.jsm", +}); + +/** + * @implements {nsIImapServerSink} + * @implements {nsIImapIncomingServer} + * @implements {nsIMsgIncomingServer} + * @implements {nsIUrlListener} + * @implements {nsISupportsWeakReference} + * @implements {nsISubscribableServer} + */ +class ImapIncomingServer extends MsgIncomingServer { + QueryInterface = ChromeUtils.generateQI([ + "nsIImapServerSink", + "nsIImapIncomingServer", + "nsIMsgIncomingServer", + "nsIUrlListener", + "nsISupportsWeakReference", + "nsISubscribableServer", + ]); + + _logger = lazy.ImapUtils.logger; + + constructor() { + super(); + + this._userAuthenticated = false; + + // nsIMsgIncomingServer attributes. + this.localStoreType = "imap"; + this.localDatabaseType = "imap"; + this.canBeDefaultServer = true; + this.canSearchMessages = true; + + // nsISubscribableServer attributes. + this.supportsSubscribeSearch = false; + + // nsIImapIncomingServer attributes that map directly to pref values. + this._mapAttrsToPrefs([ + ["Bool", "allowUTF8Accept", "allow_utf8_accept"], + ["Bool", "autoSyncOfflineStores", "autosync_offline_stores"], + ["Bool", "checkAllFoldersForNew", "check_all_folders_for_new"], + ["Bool", "cleanupInboxOnExit", "cleanup_inbox_on_exit"], + ["Bool", "downloadBodiesOnGetNewMail", "download_bodies_on_get_new_mail"], + ["Bool", "dualUseFolders", "dual_use_folders"], + ["Bool", "fetchByChunks", "fetch_by_chunks"], + ["Bool", "forceSelect", "force_select_imap"], + ["Bool", "isGMailServer", "is_gmail"], + ["Bool", "offlineDownload", "offline_download"], + ["Bool", "sendID", "send_client_info"], + ["Bool", "useCompressDeflate", "use_compress_deflate"], + ["Bool", "useCondStore", "use_condstore"], + ["Bool", "useIdle", "use_idle"], + ["Char", "adminUrl", "admin_url"], + ["Char", "otherUsersNamespace", "namespace.other_users"], + ["Char", "personalNamespace", "namespace.personal"], + ["Char", "publicNamespace", "namespace.public"], + ["Char", "serverIDPref", "serverIDResponse"], + ["Int", "autoSyncMaxAgeDays", "autosync_max_age_days"], + ["Int", "timeOutLimits", "timeout"], + ]); + } + + /** + * Most of nsISubscribableServer interfaces are delegated to + * this._subscribable. + */ + get _subscribable() { + if (!this._subscribableServer) { + this._subscribableServer = Cc[ + "@mozilla.org/messenger/subscribableserver;1" + ].createInstance(Ci.nsISubscribableServer); + this._subscribableServer.setIncomingServer(this); + } + return this._subscribableServer; + } + + /** @see nsISubscribableServer */ + get folderView() { + return this._subscribable.folderView; + } + + get subscribeListener() { + return this._subscribable.subscribeListener; + } + + set subscribeListener(value) { + this._subscribable.subscribeListener = value; + } + + set delimiter(value) { + this._subscribable.delimiter = value; + } + + subscribeCleanup() { + this._subscribableServer = null; + } + + startPopulating(msgWindow, forceToServer, getOnlyNew) { + this._loadingInSubscribeDialog = true; + this._subscribable.startPopulating(msgWindow, forceToServer, getOnlyNew); + this.delimiter = "/"; + this.setShowFullName(false); + MailServices.imap.getListOfFoldersOnServer(this, msgWindow); + } + + stopPopulating(msgWindow) { + this._loadingInSubscribeDialog = false; + this._subscribable.stopPopulating(msgWindow); + } + + addTo(name, addAsSubscribed, subscribable, changeIfExists) { + this._subscribable.addTo( + name, + addAsSubscribed, + subscribable, + changeIfExists + ); + } + + subscribe(name) { + this.subscribeToFolder(name, true); + } + + unsubscribe(name) { + this.subscribeToFolder(name, false); + } + + commitSubscribeChanges() { + this.performExpand(); + } + + setAsSubscribed(path) { + this._subscribable.setAsSubscribed(path); + } + + updateSubscribed() {} + + setState(path, state) { + return this._subscribable.setState(path, state); + } + + setShowFullName(showFullName) { + this._subscribable.setShowFullName(showFullName); + } + + hasChildren(path) { + return this._subscribable.hasChildren(path); + } + + isSubscribed(path) { + return this._subscribable.isSubscribed(path); + } + + isSubscribable(path) { + return this._subscribable.isSubscribable(path); + } + + getLeafName(path) { + return this._subscribable.getLeafName(path); + } + + getFirstChildURI(path) { + return this._subscribable.getFirstChildURI(path); + } + + getChildURIs(path) { + return this._subscribable.getChildURIs(path); + } + + /** @see nsIUrlListener */ + OnStartRunningUrl() {} + + OnStopRunningUrl(url, exitCode) { + switch (url.QueryInterface(Ci.nsIImapUrl).imapAction) { + case Ci.nsIImapUrl.nsImapDiscoverAllAndSubscribedBoxesUrl: + this.stopPopulating(); + break; + } + } + + /** @see nsIMsgIncomingServer */ + get serverRequiresPasswordForBiff() { + return !this._userAuthenticated; + } + + get offlineSupportLevel() { + const OFFLINE_SUPPORT_LEVEL_UNDEFINED = -1; + const OFFLINE_SUPPORT_LEVEL_REGULAR = 10; + let level = this.getIntValue("offline_support_level"); + return level != OFFLINE_SUPPORT_LEVEL_UNDEFINED + ? level + : OFFLINE_SUPPORT_LEVEL_REGULAR; + } + + get constructedPrettyName() { + let identity = MailServices.accounts.getFirstIdentityForServer(this); + let email; + if (identity) { + email = identity.email; + } else { + email = `${this.username}`; + if (this.hostName) { + email += `@${this.hostName}`; + } + } + let bundle = Services.strings.createBundle( + "chrome://messenger/locale/imapMsgs.properties" + ); + return bundle.formatStringFromName("imapDefaultAccountName", [email]); + } + + performBiff(msgWindow) { + this.performExpand(msgWindow); + } + + performExpand(msgWindow) { + this._setFolderToUnverified(); + this.hasDiscoveredFolders = false; + MailServices.imap.discoverAllFolders(this.rootFolder, this, msgWindow); + } + + /** + * Recursively set a folder and its subFolders to unverified state. + * + * @param {nsIMsgFolder} folder - The folder to operate on. + */ + _setFolderToUnverified(folder) { + if (!folder) { + this._setFolderToUnverified(this.rootFolder); + return; + } + + folder.QueryInterface( + Ci.nsIMsgImapMailFolder + ).verifiedAsOnlineFolder = false; + for (let child of folder.subFolders) { + this._setFolderToUnverified(child); + } + } + + closeCachedConnections() { + // Close all connections. + for (let client of this._connections) { + client.logout(); + } + // Cancel all waitings in queue. + for (let resolve of this._connectionWaitingQueue) { + resolve(false); + } + this._connections = []; + } + + verifyLogon(urlListener, msgWindow) { + return MailServices.imap.verifyLogon( + this.rootFolder, + urlListener, + msgWindow + ); + } + + subscribeToFolder(name, subscribe) { + let folder = this.rootMsgFolder.findSubFolder(name); + if (subscribe) { + return MailServices.imap.subscribeFolder(folder, name, null); + } + return MailServices.imap.unsubscribeFolder(folder, name, null); + } + + /** @see nsIImapServerSink */ + + /** @type {boolean} - User has authenticated with the server. */ + get userAuthenticated() { + return this._userAuthenticated; + } + + set userAuthenticated(value) { + this._userAuthenticated = value; + if (value) { + MailServices.accounts.userNeedsToAuthenticate = false; + } + } + + possibleImapMailbox(folderPath, delimiter, boxFlags) { + let explicitlyVerify = false; + + if (folderPath.endsWith("/")) { + folderPath = folderPath.slice(0, -1); + if (!folderPath) { + throw Components.Exception( + "Empty folder path", + Cr.NS_ERROR_INVALID_ARG + ); + } + explicitlyVerify = !(boxFlags & lazy.ImapUtils.FLAG_NAMESPACE); + } + + if (this.hasDiscoveredFolders && this._loadingInSubscribeDialog) { + // Populate the subscribe dialog. + let noSelect = boxFlags & lazy.ImapUtils.FLAG_NO_SELECT; + this.addTo( + folderPath, + this.doingLsub && !noSelect, + !noSelect, + this.doingLsub + ); + return false; + } + + let slashIndex = folderPath.indexOf("/"); + let token = folderPath; + let rest = ""; + if (slashIndex > 0) { + token = folderPath.slice(0, slashIndex); + rest = folderPath.slice(slashIndex); + } + + folderPath = (/^inbox/i.test(token) ? "INBOX" : token) + rest; + + let uri = this.rootFolder.URI; + let parentName = folderPath; + let parentUri = uri; + let hasParent = false; + let lastSlashIndex = folderPath.lastIndexOf("/"); + if (lastSlashIndex > 0) { + parentName = parentName.slice(0, lastSlashIndex); + hasParent = true; + parentUri += "/" + parentName; + } + + if (/^inbox/i.test(folderPath) && delimiter == "|") { + delimiter = "/"; + this.rootFolder.QueryInterface( + Ci.nsIMsgImapMailFolder + ).hierarchyDelimiter = delimiter; + } + + uri += "/" + folderPath; + let child = this.rootFolder.getChildWithURI( + uri, + true, + /^inbox/i.test(folderPath) + ); + + let isNewFolder = !child; + if (isNewFolder) { + if (hasParent) { + let parent = this.rootFolder.getChildWithURI( + parentUri, + true, + /^inbox/i.test(parentName) + ); + + if (!parent) { + this.possibleImapMailbox( + parentName, + delimiter, + lazy.ImapUtils.FLAG_NO_SELECT | + (boxFlags & + (lazy.ImapUtils.FLAG_PUBLIC_MAILBOX | + lazy.ImapUtils.FLAG_OTHER_USERS_MAILBOX | + lazy.ImapUtils.FLAG_PERSONAL_MAILBOX)) + ); + } + } + this.rootFolder + .QueryInterface(Ci.nsIMsgImapMailFolder) + .createClientSubfolderInfo(folderPath, delimiter, boxFlags, false); + child = this.rootFolder.getChildWithURI( + uri, + true, + /^inbox/i.test(folderPath) + ); + } + if (child) { + let imapFolder = child.QueryInterface(Ci.nsIMsgImapMailFolder); + imapFolder.verifiedAsOnlineFolder = true; + imapFolder.hierarchyDelimiter = delimiter; + if (boxFlags & lazy.ImapUtils.FLAG_IMAP_TRASH) { + if (this.deleteModel == Ci.nsMsgImapDeleteModels.MoveToTrash) { + child.setFlag(Ci.nsMsgFolderFlags.Trash); + } + } + imapFolder.boxFlags = boxFlags; + imapFolder.explicitlyVerify = explicitlyVerify; + let onlineName = imapFolder.onlineName; + folderPath = folderPath.replaceAll("/", delimiter); + if (delimiter != "/") { + folderPath = decodeURIComponent(folderPath); + } + + if (boxFlags & lazy.ImapUtils.FLAG_IMAP_INBOX) { + // GMail gives us a localized name for the inbox but doesn't let + // us select that localized name. + imapFolder.onlineName = "INBOX"; + } else if (!onlineName || onlineName != folderPath) { + imapFolder.onlineName = folderPath; + } + + child.prettyName = imapFolder.name; + if (isNewFolder) { + // Close the db so we don't hold open all the .msf files for new folders. + child.msgDatabase = null; + } + } + + return isNewFolder; + } + + discoveryDone() { + this.hasDiscoveredFolders = true; + // No need to verify the root. + this.rootFolder.QueryInterface( + Ci.nsIMsgImapMailFolder + ).verifiedAsOnlineFolder = true; + let unverified = this._getUnverifiedFolders(this.rootFolder); + this._logger.debug( + `discoveryDone, unverified folders count=${unverified.length}.` + ); + for (let folder of unverified) { + if (folder.flags & Ci.nsMsgFolderFlags.Virtual) { + // Do not remove virtual folders. + continue; + } + let imapFolder = folder.QueryInterface(Ci.nsIMsgImapMailFolder); + if ( + !this.usingSubscription || + imapFolder.explicitlyVerify || + (folder.hasSubFolders && this._noDescendentsAreVerified(folder)) + ) { + imapFolder.explicitlyVerify = false; + imapFolder.list(); + } else if (folder.parent) { + imapFolder.removeLocalSelf(); + this._logger.debug(`Removed unverified folder name=${folder.name}`); + } + } + } + + /** + * Find local folders that do not exist on the server. + * + * @param {nsIMsgFolder} parentFolder - The folder to check. + * @returns {nsIMsgFolder[]} + */ + _getUnverifiedFolders(parentFolder) { + let folders = []; + let imapFolder = parentFolder.QueryInterface(Ci.nsIMsgImapMailFolder); + if (!imapFolder.verifiedAsOnlineFolder || imapFolder.explicitlyVerify) { + folders.push(imapFolder); + } + for (let folder of parentFolder.subFolders) { + folders.push(...this._getUnverifiedFolders(folder)); + } + return folders; + } + + /** + * Returns true if all sub folders are unverified. + * + * @param {nsIMsgFolder} parentFolder - The folder to check. + * @returns {nsIMsgFolder[]} + */ + _noDescendentsAreVerified(parentFolder) { + for (let folder of parentFolder.subFolders) { + let imapFolder = folder.QueryInterface(Ci.nsIMsgImapMailFolder); + if ( + imapFolder.verifiedAsOnlineFolder || + !this._noDescendentsAreVerified(folder) + ) { + return false; + } + } + return true; + } + + onlineFolderRename(msgWindow, oldName, newName) { + let folder = this._getFolder(oldName).QueryInterface( + Ci.nsIMsgImapMailFolder + ); + let index = newName.lastIndexOf("/"); + let parent = + index > 0 ? this._getFolder(newName.slice(0, index)) : this.rootFolder; + folder.renameLocal(newName, parent); + if (parent instanceof Ci.nsIMsgImapMailFolder) { + try { + parent.renameClient(msgWindow, folder, oldName, newName); + } catch (e) { + this._logger.error("renameClient failed", e); + } + } + + this._getFolder(newName).NotifyFolderEvent("RenameCompleted"); + } + + /** + * Given a canonical folder name, returns the corresponding msg folder. + * + * @param {string} name - The canonical folder name, e.g. a/b/c. + * @returns {nsIMsgFolder} The corresponding msg folder. + */ + _getFolder(name) { + return lazy.MailUtils.getOrCreateFolder(this.rootFolder.URI + "/" + name); + } + + abortQueuedUrls() {} + + setCapability(capabilityFlags) { + this._capabilityFlags = capabilityFlags; + if (capabilityFlags & lazy.ImapCapFlags.Gmail) { + this.isGMailServer = true; + } + } + + /** @see nsIImapIncomingServer */ + getCapability() { + return this._capabilityFlags; + } + + get deleteModel() { + return this.getIntValue("delete_model"); + } + + set deleteModel(value) { + this.setIntValue("delete_model", value); + let trashFolder = this._getFolder(this.trashFolderName); + if (trashFolder) { + if (value == Ci.nsMsgImapDeleteModels.MoveToTrash) { + trashFolder.setFlag(Ci.nsMsgFolderFlags.Trash); + } else { + trashFolder.clearFlag(Ci.nsMsgFolderFlags.Trash); + } + } + } + + get usingSubscription() { + return this.getBoolValue("using_subscription"); + } + + get trashFolderName() { + return this.getUnicharValue("trash_folder_name") || "Trash"; + } + + get maximumConnectionsNumber() { + let maxConnections = this.getIntValue("max_cached_connections", 0); + if (maxConnections > 0) { + return maxConnections; + } + // The default is 5 connections, if the pref value is 0, we use the default. + // If it's negative, treat it as 1. + maxConnections = maxConnections == 0 ? 5 : 1; + this.maximumConnectionsNumber = maxConnections; + return maxConnections; + } + + set maximumConnectionsNumber(value) { + this.setIntValue("max_cached_connections", value); + } + + GetNewMessagesForNonInboxFolders( + folder, + msgWindow, + forceAllFolders, + performingBiff + ) { + let flags = folder.flags; + + if ( + folder.QueryInterface(Ci.nsIMsgImapMailFolder).canOpenFolder && + ((forceAllFolders && + !( + flags & + (Ci.nsMsgFolderFlags.Inbox | + Ci.nsMsgFolderFlags.Trash | + Ci.nsMsgFolderFlags.Junk | + Ci.nsMsgFolderFlags.Virtual) + )) || + flags & Ci.nsMsgFolderFlags.CheckNew) + ) { + folder.gettingNewMessages = true; + if (performingBiff) { + folder.performingBiff = true; + } + } + + if ( + Services.prefs.getBoolPref("mail.imap.use_status_for_biff", false) && + !MailServices.mailSession.IsFolderOpenInWindow(folder) + ) { + folder.updateStatus(this, msgWindow); + } else { + folder.updateFolder(msgWindow); + } + + for (let subFolder of folder.subFolders) { + this.GetNewMessagesForNonInboxFolders( + subFolder, + msgWindow, + forceAllFolders, + performingBiff + ); + } + } + + CloseConnectionForFolder(folder) { + for (let client of this._connections) { + if (client.folder == folder) { + client.logout(); + } + } + } + + _capabilities = []; + + set capabilities(value) { + this._capabilities = value; + this.setCapability(lazy.ImapCapFlags.stringsToFlags(value)); + } + + // @type {ImapClient[]} - An array of connections. + _connections = []; + // @type {Function[]} - An array of Promise.resolve functions. + _connectionWaitingQueue = []; + + /** + * Wait for a free connection. + * + * @param {nsIMsgFolder} folder - The folder to operate on. + * @returns {ImapClient} + */ + async _waitForNextClient(folder) { + // Wait until a connection is available. canGetNext is false when + // closeCachedConnections is called. + let canGetNext = await new Promise(resolve => + this._connectionWaitingQueue.push(resolve) + ); + if (canGetNext) { + return this._getNextClient(folder); + } + return null; + } + + /** + * Check if INBOX folder is selected in a connection. + * + * @param {ImapClient} client - The client to check. + * @returns {boolean} + */ + _isInboxConnection(client) { + return client.folder?.onlineName.toUpperCase() == "INBOX"; + } + + /** + * Get a free connection that can be used. + * + * @param {nsIMsgFolder} folder - The folder to operate on. + * @returns {ImapClient} + */ + async _getNextClient(folder) { + let client; + + for (client of this._connections) { + if (folder && client.folder == folder) { + if (client.busy) { + // Prevent operating on the same folder in two connections. + return this._waitForNextClient(folder); + } + // If we're idling in the target folder, reuse it. + client.busy = true; + return client; + } + } + + // Create a new client if the pool is not full. + if (this._connections.length < this.maximumConnectionsNumber) { + client = new lazy.ImapClient(this); + this._connections.push(client); + client.busy = true; + return client; + } + + let freeConnections = this._connections.filter(c => !c.busy); + + // Wait if no free connection. + if (!freeConnections.length) { + return this._waitForNextClient(folder); + } + + // Reuse any free connection if only have one connection or IDLE not used. + if ( + this.maximumConnectionsNumber <= 1 || + !this.useIdle || + !this._capabilities.includes("IDLE") + ) { + freeConnections[0].busy = true; + return freeConnections[0]; + } + + // Reuse non-inbox free connection. + client = freeConnections.find(c => !this._isInboxConnection(c)); + if (client) { + client.busy = true; + return client; + } + return this._waitForNextClient(folder); + } + + /** + * Do some actions with a connection. + * + * @param {nsIMsgFolder} folder - The folder to operate on. + * @param {Function} handler - A callback function to take a ImapClient + * instance, and do some actions. + */ + async withClient(folder, handler) { + let client = await this._getNextClient(folder); + if (!client) { + return; + } + let startIdle = async () => { + if (!this.useIdle || !this._capabilities.includes("IDLE")) { + return; + } + + // IDLE is configed and supported, use IDLE to receive server pushes. + let hasInboxConnection = this._connections.some(c => + this._isInboxConnection(c) + ); + let alreadyIdling = + client.folder && + this._connections.find( + c => c != client && !c.busy && c.folder == client.folder + ); + if (!hasInboxConnection) { + client.selectFolder( + this.rootFolder.getFolderWithFlags(Ci.nsMsgFolderFlags.Inbox) + ); + } else if (client.folder && !alreadyIdling) { + client.idle(); + } else if (alreadyIdling) { + client.folder = null; + } + }; + client.onFree = () => { + client.busy = false; + let resolve = this._connectionWaitingQueue.shift(); + if (resolve) { + // Resolve the first waiting in queue. + resolve(true); + } else if (client.isOnline) { + startIdle(); + } + }; + handler(client); + client.connect(); + } +} + +ImapIncomingServer.prototype.classID = Components.ID( + "{b02a4e1c-0d9e-498c-8b9d-18917ba9f65b}" +); diff --git a/comm/mailnews/imap/src/ImapMessageService.jsm b/comm/mailnews/imap/src/ImapMessageService.jsm new file mode 100644 index 0000000000..e88e0f8090 --- /dev/null +++ b/comm/mailnews/imap/src/ImapMessageService.jsm @@ -0,0 +1,292 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["ImapMessageService", "ImapMessageMessageService"]; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +const { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +const { ImapUtils } = ChromeUtils.import("resource:///modules/ImapUtils.jsm"); + +const lazy = {}; + +XPCOMUtils.defineLazyModuleGetters(lazy, { + MailUtils: "resource:///modules/MailUtils.jsm", +}); + +/** + * @implements {nsIMsgMessageService} + */ +class BaseMessageService { + QueryInterface = ChromeUtils.generateQI(["nsIMsgMessageService"]); + + _logger = ImapUtils.logger; + + copyMessage(messageUri, copyListener, moveMessage, urlListener, msgWindow) { + this._logger.debug("copyMessage", messageUri, moveMessage); + let { serverURI, folder, folderName, key } = + this._decomposeMessageUri(messageUri); + let imapUrl = Services.io + .newURI(`${serverURI}/fetch>UID>/${folderName}>${key}`) + .QueryInterface(Ci.nsIImapUrl); + + if (urlListener) { + imapUrl + .QueryInterface(Ci.nsIMsgMailNewsUrl) + .RegisterListener(urlListener); + } + + return MailServices.imap.fetchMessage( + imapUrl, + moveMessage + ? Ci.nsIImapUrl.nsImapOnlineToOfflineMove + : Ci.nsIImapUrl.nsImapOnlineToOfflineCopy, + folder, + folder.QueryInterface(Ci.nsIImapMessageSink), + msgWindow, + copyListener, + key, + false, + {} + ); + } + + loadMessage( + messageUri, + displayConsumer, + msgWindow, + urlListener, + autodetectCharset + ) { + this._logger.debug("loadMessage", messageUri); + let { serverURI, folder, folderName, key } = + this._decomposeMessageUri(messageUri); + let imapUrl = Services.io + .newURI(`${serverURI}/fetch>UID>/${folderName}>${key}`) + .QueryInterface(Ci.nsIImapUrl); + + let mailnewsUrl = imapUrl.QueryInterface(Ci.nsIMsgMailNewsUrl); + if (urlListener) { + mailnewsUrl.RegisterListener(urlListener); + } + + return MailServices.imap.fetchMessage( + imapUrl, + Ci.nsIImapUrl.nsImapMsgFetch, + folder, + folder.QueryInterface(Ci.nsIImapMessageSink), + msgWindow, + displayConsumer, + key, + false, + {} + ); + } + + SaveMessageToDisk( + messageUri, + file, + addDummyEnvelope, + urlListener, + outUrl, + canonicalLineEnding, + msgWindow + ) { + this._logger.debug("SaveMessageToDisk", messageUri); + let { serverURI, folder, folderName, key } = + this._decomposeMessageUri(messageUri); + let imapUrl = Services.io + .newURI(`${serverURI}/fetch>UID>/${folderName}>${key}`) + .QueryInterface(Ci.nsIImapUrl); + + let msgUrl = imapUrl.QueryInterface(Ci.nsIMsgMessageUrl); + msgUrl.messageFile = file; + msgUrl.AddDummyEnvelope = addDummyEnvelope; + msgUrl.canonicalLineEnding = canonicalLineEnding; + let mailnewsUrl = imapUrl.QueryInterface(Ci.nsIMsgMailNewsUrl); + mailnewsUrl.RegisterListener(urlListener); + mailnewsUrl.msgIsInLocalCache = folder.hasMsgOffline(key, null, 10); + + return MailServices.imap.fetchMessage( + imapUrl, + Ci.nsIImapUrl.nsImapSaveMessageToDisk, + folder, + folder.QueryInterface(Ci.nsIImapMessageSink), + msgWindow, + mailnewsUrl.getSaveAsListener(addDummyEnvelope, file), + key, + false, + {} + ); + } + + getUrlForUri(messageUri, msgWindow) { + if (messageUri.includes("&type=application/x-message-display")) { + return Services.io.newURI(messageUri); + } + + let { serverURI, folder, folderName, key } = + this._decomposeMessageUri(messageUri); + let delimiter = + folder.QueryInterface(Ci.nsIMsgImapMailFolder).hierarchyDelimiter || "/"; + let imapUrl = Services.io + .newURI( + `${serverURI}:${folder.server.port}/fetch>UID>${delimiter}${folderName}>${key}` + ) + .QueryInterface(Ci.nsIImapUrl); + + return imapUrl; + } + + streamMessage( + messageUri, + consumer, + msgWindow, + urlListener, + convertData, + additionalHeader, + localOnly + ) { + this._logger.debug("streamMessage", messageUri); + let { serverURI, folder, folderName, key } = + this._decomposeMessageUri(messageUri); + let url = `${serverURI}/fetch>UID>/${folderName}>${key}`; + if (additionalHeader) { + url += `?header=${additionalHeader}`; + } + let imapUrl = Services.io.newURI(url).QueryInterface(Ci.nsIImapUrl); + imapUrl.localFetchOnly = localOnly; + + let mailnewsUrl = imapUrl.QueryInterface(Ci.nsIMsgMailNewsUrl); + mailnewsUrl.folder = folder; + mailnewsUrl.msgWindow = msgWindow; + mailnewsUrl.msgIsInLocalCache = folder.hasMsgOffline(key); + if (urlListener) { + mailnewsUrl.RegisterListener(urlListener); + } + + return MailServices.imap.fetchMessage( + imapUrl, + Ci.nsIImapUrl.nsImapMsgFetchPeek, + folder, + folder.QueryInterface(Ci.nsIImapMessageSink), + msgWindow, + consumer, + key, + convertData, + {} + ); + } + + streamHeaders(messageUri, consumer, urlListener, localOnly) { + this._logger.debug("streamHeaders", messageUri); + let { folder, key } = this._decomposeMessageUri(messageUri); + + let hasMsgOffline = folder.hasMsgOffline(key); + if (!hasMsgOffline) { + return; + } + + let localMsgStream = folder.getLocalMsgStream(folder.GetMessageHeader(key)); + let sstream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + sstream.init(localMsgStream); + let headers = ""; + let str = ""; + do { + str = sstream.read(4096); + let index = str.indexOf("\r\n\r\n"); + if (index != -1) { + headers += str.slice(0, index) + "\r\n"; + break; + } else { + headers += str; + } + } while (str.length); + + let headersStream = Cc[ + "@mozilla.org/io/string-input-stream;1" + ].createInstance(Ci.nsIStringInputStream); + headersStream.setData(headers, headers.length); + let pump = Cc["@mozilla.org/network/input-stream-pump;1"].createInstance( + Ci.nsIInputStreamPump + ); + pump.init(headersStream, 0, 0, true); + pump.asyncRead(consumer); + } + + /** + * Go from message uri to go nsIMsgDBHdr. + * + * @param {string} uri - A message uri to get the nsIMsgDBHdr for. + * @returns {?nsIMsgDBHdr} Hdr for the uri, or or null if failed. + */ + messageURIToMsgHdr(uri) { + try { + let { folder, key } = this._decomposeMessageUri(uri); + return folder.GetMessageHeader(key); + } catch (e) { + return null; + } + } + + Search(searchSession, msgWindow, folder, searchUri) { + let server = folder.server.QueryInterface(Ci.nsIMsgIncomingServer); + server.wrappedJSObject.withClient(folder, client => { + client.startRunningUrl( + searchSession.QueryInterface(Ci.nsIUrlListener), + msgWindow + ); + client.onReady = () => { + client.search(folder, searchUri); + }; + client.onData = uids => { + for (let uid of uids) { + let msgHdr = folder.msgDatabase.getMsgHdrForKey(uid); + searchSession.runningAdapter.AddResultElement(msgHdr); + } + }; + }); + } + + /** + * Parse a message uri to hostname, folder and message key. + * + * @param {string} uri - The imap-message:// url to parse. + * @returns {serverURI: string, folder: nsIMsgFolder, folderName: string, key: string} + */ + _decomposeMessageUri(messageUri) { + let matches = /imap-message:\/\/([^:/]+)\/(.+)#(\d+)/.exec(messageUri); + if (!matches) { + throw new Error(`Unexpected IMAP URL: ${messageUri}`); + } + let [, host, folderName, key] = matches; + let folder = lazy.MailUtils.getOrCreateFolder( + `imap://${host}/${folderName}` + ); + return { serverURI: folder.server.serverURI, folder, folderName, key }; + } +} + +/** + * A message service for imap://. + */ +class ImapMessageService extends BaseMessageService {} + +ImapMessageService.prototype.classID = Components.ID( + "{d63af753-c2f3-4f1d-b650-9d12229de8ad}" +); + +/** + * A message service for imap-message://. + */ +class ImapMessageMessageService extends BaseMessageService {} + +ImapMessageMessageService.prototype.classID = Components.ID( + "{2532ae4f-a852-4c96-be45-1308ba23d62e}" +); diff --git a/comm/mailnews/imap/src/ImapModuleLoader.jsm b/comm/mailnews/imap/src/ImapModuleLoader.jsm new file mode 100644 index 0000000000..427fe2a3f2 --- /dev/null +++ b/comm/mailnews/imap/src/ImapModuleLoader.jsm @@ -0,0 +1,131 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["ImapModuleLoader"]; + +/** + * Determine whether to use nsImapService.cpp or ImapService.jsm. When + * `mailnews.imap.jsmodule` is `true`, use ImapService.jsm. + */ +function ImapModuleLoader() { + try { + this.loadModule(); + } catch (e) { + console.error(e); + } +} + +var imapJSModules = [ + // moduleName, interfaceId, contractId + [ + "ImapIncomingServer", + "{b02a4e1c-0d9e-498c-8b9d-18917ba9f65b}", + "@mozilla.org/messenger/server;1?type=imap", + ], + [ + "ImapService", + "{2ea8fbe6-029b-4bff-ae05-b794cf955afb}", + "@mozilla.org/messenger/imapservice;1", + ], + [ + "ImapMessageService", + "{d63af753-c2f3-4f1d-b650-9d12229de8ad}", + "@mozilla.org/messenger/messageservice;1?type=imap", + "ImapMessageService", + ], + [ + "ImapFolderContentHandler", + "{d927a82f-2d15-4972-ab88-6d84601aae68}", + "@mozilla.org/uriloader/content-handler;1?type=x-application-imapfolder", + ], + [ + "ImapMessageMessageService", + "{2532ae4f-a852-4c96-be45-1308ba23d62e}", + "@mozilla.org/messenger/messageservice;1?type=imap-message", + "ImapMessageService", + ], + [ + "ImapProtocolHandler", + "{ebb06c58-6ccd-4bde-9087-40663e0388ae}", + "@mozilla.org/network/protocol;1?name=imap", + ], + [ + "ImapProtocolInfo", + "{1d9473bc-423a-4632-ad5d-802154e80f6f}", + "@mozilla.org/messenger/protocol/info;1?type=imap", + ], +]; + +ImapModuleLoader.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + + observe() { + // Nothing to do here, just need the entry so this is instantiated. + }, + + loadModule() { + if (Services.prefs.getBoolPref("mailnews.imap.jsmodule", false)) { + let registrar = Components.manager.QueryInterface( + Ci.nsIComponentRegistrar + ); + + for (let [ + moduleName, + interfaceId, + contractId, + fileName, + ] of imapJSModules) { + // Register a module. + let classId = Components.ID(interfaceId); + registrar.registerFactory( + classId, + "", + contractId, + lazyFactoryFor(fileName || moduleName, moduleName) + ); + } + + dump("[ImapModuleLoader] Using ImapService.jsm\n"); + + const { ImapProtocolHandler } = ChromeUtils.import( + `resource:///modules/ImapProtocolHandler.jsm` + ); + let protocolFlags = + Ci.nsIProtocolHandler.URI_NORELATIVE | + Ci.nsIProtocolHandler.URI_FORBIDS_AUTOMATIC_DOCUMENT_REPLACEMENT | + Ci.nsIProtocolHandler.URI_DANGEROUS_TO_LOAD | + Ci.nsIProtocolHandler.ALLOWS_PROXY | + Ci.nsIProtocolHandler.URI_FORBIDS_COOKIE_ACCESS | + Ci.nsIProtocolHandler.ORIGIN_IS_FULL_SPEC; + + Services.io.registerProtocolHandler( + "imap", + new ImapProtocolHandler(), + protocolFlags, + Ci.nsIImapUrl.DEFAULT_IMAP_PORT + ); + } else { + dump("[ImapModuleLoader] Using nsImapService.cpp\n"); + // Ensure the imap protocol is actually registered. + Cc["@mozilla.org/network/protocol;1?name=imap"].getService( + Ci.nsIImapService + ); + } + }, +}; + +function lazyFactoryFor(fileName, constructorName) { + let factory = { + get scope() { + delete this.scope; + this.scope = ChromeUtils.import(`resource:///modules/${fileName}.jsm`); + return this.scope; + }, + createInstance(interfaceID) { + let componentConstructor = this.scope[constructorName]; + return new componentConstructor().QueryInterface(interfaceID); + }, + }; + return factory; +} diff --git a/comm/mailnews/imap/src/ImapProtocolHandler.jsm b/comm/mailnews/imap/src/ImapProtocolHandler.jsm new file mode 100644 index 0000000000..949dca9632 --- /dev/null +++ b/comm/mailnews/imap/src/ImapProtocolHandler.jsm @@ -0,0 +1,40 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var EXPORTED_SYMBOLS = ["ImapProtocolHandler"]; + +var { ImapChannel } = ChromeUtils.import("resource:///modules/ImapChannel.jsm"); + +/** + * @implements {nsIProtocolHandler} + */ +class ImapProtocolHandler { + QueryInterface = ChromeUtils.generateQI(["nsIProtocolHandler"]); + + scheme = "imap"; + + newChannel(uri, loadInfo) { + let channel = new ImapChannel(uri, loadInfo); + let spec = uri.spec; + if ( + spec.includes("part=") && + !spec.includes("type=message/rfc822") && + !spec.includes("type=application/x-message-display") && + !spec.includes("type=application/pdf") + ) { + channel.contentDisposition = Ci.nsIChannel.DISPOSITION_ATTACHMENT; + } else { + channel.contentDisposition = Ci.nsIChannel.DISPOSITION_INLINE; + } + return channel; + } + + allowPort(port, scheme) { + return true; + } +} + +ImapProtocolHandler.prototype.classID = Components.ID( + "{ebb06c58-6ccd-4bde-9087-40663e0388ae}" +); diff --git a/comm/mailnews/imap/src/ImapProtocolInfo.jsm b/comm/mailnews/imap/src/ImapProtocolInfo.jsm new file mode 100644 index 0000000000..9d12175791 --- /dev/null +++ b/comm/mailnews/imap/src/ImapProtocolInfo.jsm @@ -0,0 +1,44 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["ImapProtocolInfo"]; + +var { MsgProtocolInfo } = ChromeUtils.importESModule( + "resource:///modules/MsgProtocolInfo.sys.mjs" +); + +/** + * @implements {nsIMsgProtocolInfo} + */ +class ImapProtocolInfo extends MsgProtocolInfo { + QueryInterface = ChromeUtils.generateQI(["nsIMsgProtocolInfo"]); + + serverIID = Components.ID("{b02a4e1c-0d9e-498c-8b9d-18917ba9f65b}"); + + requiresUsername = true; + preflightPrettyNameWithEmailAddress = true; + canDelete = true; + canLoginAtStartUp = true; + canDuplicate = true; + canGetMessages = true; + canGetIncomingMessages = true; + defaultDoBiff = true; + showComposeMsgLink = true; + foldersCreatedAsync = true; + + getDefaultServerPort(isSecure) { + return isSecure + ? Ci.nsIImapUrl.DEFAULT_IMAPS_PORT + : Ci.nsIImapUrl.DEFAULT_IMAP_PORT; + } + + // @see MsgProtocolInfo.sys.mjs + RELATIVE_PREF = "mail.root.imap-rel"; + ABSOLUTE_PREF = "mail.root.imap"; + DIR_SERVICE_PROP = "IMapMD"; +} + +ImapProtocolInfo.prototype.classID = Components.ID( + "{1d9473bc-423a-4632-ad5d-802154e80f6f}" +); diff --git a/comm/mailnews/imap/src/ImapResponse.jsm b/comm/mailnews/imap/src/ImapResponse.jsm new file mode 100644 index 0000000000..08d1c25916 --- /dev/null +++ b/comm/mailnews/imap/src/ImapResponse.jsm @@ -0,0 +1,479 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["ImapResponse"]; + +var { ImapUtils } = ChromeUtils.import("resource:///modules/ImapUtils.jsm"); + +/** + * A structure to represent a server response. + */ +class ImapResponse { + constructor() { + // @type {MailboxData[]} The mailbox-data in this response. + this.mailboxes = []; + // @type {MessageData[]} The message-data in this response. + this.messages = []; + // A holder for attributes. + this.attributes = {}; + // Expunged message sequences. + this.expunged = []; + + // The remaining string to parse. + this._response = ""; + + this.onMessage = () => {}; + } + + /** + * A server response can span multiple chunks, this function parses one chunk. + * + * @param {string} str - A chunk of server response. + */ + parse(str) { + this._response += str; + if (this._pendingMessage) { + // We have an unfinished message in the last chunk. + let remaining = + this._pendingMessage.bodySize - this._pendingMessage.body.length; + if (remaining + ")\r\n".length <= this._response.length) { + // Consume the message together with the ending ")\r\n". + this._pendingMessage.body += this._response.slice(0, remaining); + this.onMessage(this._pendingMessage); + this._pendingMessage = null; + this._advance(remaining + ")\r\n".length); + } else { + this.done = false; + return; + } + } + this._parse(); + } + + /** + * Drop n characters from _response. + * + * @param {number} n - The number of characters to drop. + */ + _advance(n) { + this._response = this._response.slice(n); + } + + /** + * Parse the response line by line. Because a single response can contain + * multiple types of data, update the corresponding properties after parsing + * a line, e.g. this.capabilities, this.flags, this.messages. + */ + _parse() { + if (!this._response && this.tag != "*") { + // Nothing more to parse. + this.done = true; + return; + } + let index = this._response.indexOf("\r\n"); + if (index == -1) { + // Expect more string in the next chunk. + this.done = false; + return; + } + + let line = this._response.slice(0, index); + this._advance(index + 2); // Consume the line and "\r\n". + let tokens = this._parseLine(line); + this.tag = tokens[0]; + this.status = tokens[1]; + if (this.tag == "+") { + this.statusText = tokens.slice(1).join(" "); + if (!this._response) { + this.done = true; + return; + } + } + + let parsed; + + if (this.tag == "*") { + parsed = true; + switch (tokens[1].toUpperCase()) { + case "CAPABILITY": + // * CAPABILITY IMAP4rev1 IDLE STARTTLS AUTH=LOGIN AUTH=PLAIN + let { capabilities, authMethods } = new CapabilityData( + tokens.slice(2) + ); + this.capabilities = capabilities; + this.authMethods = authMethods; + break; + case "FLAGS": + // * FLAGS (\Seen \Draft $Forwarded) + this.flags = ImapUtils.stringsToFlags(tokens[2]); + if (tokens[2].includes("\\*")) { + this.supportedUserFlags = + ImapUtils.FLAG_LABEL | + ImapUtils.FLAG_MDN_SENT | + ImapUtils.FLAG_SUPPORT_FORWARDED_FLAG | + ImapUtils.FLAG_SUPPORT_USER_FLAG; + } + break; + case "ID": + // * ID ("name" "imap" "vendor" "Example, Inc.") + this.id = line.slice("* ID ".length); + break; + case "LIST": + case "LSUB": + // * LIST (\Subscribed \NoInferiors \UnMarked \Sent) "/" Sent + this.mailboxes.push(new MailboxData(tokens)); + break; + case "QUOTAROOT": + // * QUOTAROOT Sent INBOX + this.quotaRoots = tokens.slice(3); + break; + case "QUOTA": + // S: * QUOTA INBOX (STORAGE 95295 97656832) + if (!this.quotas) { + this.quotas = []; + } + this.quotas.push([tokens[2], ...tokens[3]]); + break; + case "SEARCH": + // * SEARCH 1 4 9 + this.search = tokens.slice(2).map(x => Number(x)); + break; + case "STATUS": + // * STATUS \"folder 2\" (UIDNEXT 2 MESSAGES 1 UNSEEN 1) + this.attributes = new StatusData(tokens).attributes; + break; + default: + if (Number.isInteger(+tokens[1])) { + this._parseNumbered(tokens); + } else { + parsed = false; + } + break; + } + } + if (!parsed && Array.isArray(tokens[2])) { + let type = tokens[2][0].toUpperCase(); + let data = tokens[2].slice(1); + switch (type) { + case "CAPABILITY": + // 32 OK [CAPABILITY IMAP4rev1 IDLE STARTTLS AUTH=LOGIN AUTH=PLAIN] + let { capabilities, authMethods } = new CapabilityData(data); + this.capabilities = capabilities; + this.authMethods = authMethods; + break; + case "PERMANENTFLAGS": + // * OK [PERMANENTFLAGS (\\Seen \\Draft $Forwarded \\*)] + this.permanentflags = ImapUtils.stringsToFlags(tokens[2][1]); + if (tokens[2][1].includes("\\*")) { + this.supportedUserFlags = + ImapUtils.FLAG_LABEL | + ImapUtils.FLAG_MDN_SENT | + ImapUtils.FLAG_SUPPORT_FORWARDED_FLAG | + ImapUtils.FLAG_SUPPORT_USER_FLAG; + } + break; + default: + let field = type.toLowerCase(); + if (tokens[2].length == 1) { + // A boolean attribute, e.g. 12 OK [READ-WRITE] + this[field] = true; + } else if (tokens[2].length == 2) { + // An attribute/value pair, e.g. 12 OK [UIDNEXT 600] + this[field] = tokens[2][1]; + } else { + // Hold other attributes. + this.attributes[field] = data; + } + } + } + this._parse(); + } + + /** + * Handle the tokens of a line in the form of "* NUM TYPE". + * + * @params {Array<string|string[]>} tokens - The tokens of the line. + */ + _parseNumbered(tokens) { + let intValue = +tokens[1]; + let type = tokens[2].toUpperCase(); + switch (type) { + case "FETCH": + // * 1 FETCH (UID 5 FLAGS (\SEEN) BODY[HEADER.FIELDS (FROM TO)] {12} + let message = new MessageData(intValue, tokens[3]); + this.messages.push(message); + if (message.bodySize) { + if (message.bodySize + ")\r\n".length <= this._response.length) { + // Consume the message together with the ending ")\r\n". + message.body = this._response.slice(0, message.bodySize); + this.onMessage(message); + } else { + message.body = this._response; + this._pendingMessage = message; + this.done = false; + } + this._advance(message.bodySize + ")\r\n".length); + } else { + this.onMessage(message); + } + break; + case "EXISTS": + // * 6 EXISTS + this.exists = intValue; + break; + case "EXPUNGE": + // * 2 EXPUNGE + this.expunged.push(intValue); + break; + case "RECENT": + // Deprecated in rfc9051. + break; + default: + throw Components.Exception( + `Unrecognized response: ${tokens.join(" ")}`, + Cr.NS_ERROR_ILLEGAL_VALUE + ); + } + } + + /** + * Break a line into flat tokens array. For example, + * "(UID 24 FLAGS (NonJunk))" will be tokenized to + * ["(", "UID", "24", "FLAGS", "(", "NonJunk", ")", ")"]. + * + * @param {string} line - A single line of string. + * @returns {string[]} + */ + _tokenize(line) { + const SEPARATORS = /[()\[\]" ]/; + let tokens = []; + while (line) { + // Find the first separator. + let index = line.search(SEPARATORS); + if (index == -1) { + tokens.push(line); + break; + } + let sep = line[index]; + let token = line.slice(0, index); + if (token) { + tokens.push(token); + } + if (sep == '"') { + // Parse the whole string as a token. + line = line.slice(index + 1); + let str = sep; + while (true) { + index = line.indexOf('"'); + if (line[index - 1] == "\\") { + // Not the ending quote. + str += line.slice(0, index + 1); + line = line.slice(index + 1); + continue; + } else { + // The ending quote. + str += line.slice(0, index + 1); + tokens.push(str); + line = line.slice(index + 1); + break; + } + } + continue; + } else if (sep != " ") { + tokens.push(sep); + } + line = line.slice(index + 1); + } + return tokens; + } + + /** + * Parse a line into nested tokens array. For example, + * "(UID 24 FLAGS (NonJunk))" will be parsed to + * ["UID", "24", "FLAGS", ["NonJunk"]]. + * + * @param {string} line - A single line of string. + * @returns {Array<string|string[]>} + */ + _parseLine(line) { + let tokens = []; + let arrayDepth = 0; + + for (let token of this._tokenize(line)) { + let depth = arrayDepth; + let arr = tokens; + while (depth-- > 0) { + arr = arr.at(-1); + } + switch (token) { + case "(": + case "[": + arr.push([]); + arrayDepth++; + break; + case ")": + case "]": + arrayDepth--; + break; + default: + arr.push(token); + } + } + + return tokens; + } +} + +/** + * A structure to represent capability-data. + */ +class CapabilityData { + /** + * @param {string[]} tokens - An array like: ["IMAP4rev1", "IDLE", "STARTTLS", + * "AUTH=LOGIN", "AUTH=PLAIN"]. + */ + constructor(tokens) { + this.capabilities = []; + this.authMethods = []; + for (let cap of tokens) { + cap = cap.toUpperCase(); + if (cap.startsWith("AUTH=")) { + this.authMethods.push(cap.slice(5)); + } else { + this.capabilities.push(cap); + } + } + } +} + +/** + * A structure to represent message-data. + */ +class MessageData { + /** + * @param {number} sequence - The sequence number of this message. + * @param {string[]} tokens - An array like: ["UID", "24", "FLAGS", ["\Seen"]]. + */ + constructor(sequence, tokens) { + this.sequence = sequence; + this.customAttributes = {}; + for (let i = 0; i < tokens.length; i += 2) { + let name = tokens[i].toUpperCase(); + switch (name) { + case "UID": + this.uid = +tokens[i + 1]; + break; + case "FLAGS": + this.flags = ImapUtils.stringsToFlags(tokens[i + 1]); + this.keywords = tokens[i + 1] + .filter(x => !x.startsWith("\\")) + .join(" "); + break; + case "BODY": { + // bodySection is the part between [ and ]. + this.bodySection = tokens[i + 1]; + i++; + // {123} means the following 123 bytes are the body. + let matches = tokens[i + 1].match(/{(\d+)}/); + if (matches) { + this.bodySize = +matches[1]; + this.body = ""; + } + break; + } + case "RFC822.SIZE": { + this.size = +tokens[i + 1]; + break; + } + default: + this.customAttributes[tokens[i]] = tokens[i + 1]; + break; + } + } + } +} + +/** + * A structure to represent mailbox-data. + */ +class MailboxData { + constructor(tokens) { + let [, , attributes, delimiter, name] = tokens; + this.flags = this._stringsToFlags(attributes); + this.delimiter = unwrapString(delimiter); + this.name = unwrapString(name); + } + + /** + * Convert an array of flag string to an internal flag number. + * + * @param {string[]} arr - An array of flag string. + * @returns {number} An internal flag number. + */ + _stringsToFlags(arr) { + let stringToFlag = { + "\\MARKED": ImapUtils.FLAG_MARKED, + "\\UNMARKED": ImapUtils.FLAG_UNMARKED, + "\\NOINFERIORS": + // RFC 5258 \NoInferiors implies \HasNoChildren + ImapUtils.FLAG_NO_INFERIORS | ImapUtils.FLAG_HAS_NO_CHILDREN, + "\\NOSELECT": ImapUtils.FLAG_NO_SELECT, + "\\TRASH": ImapUtils.FLAG_IMAP_TRASH | ImapUtils.FLAG_IMAP_XLIST_TRASH, + "\\SENT": ImapUtils.FLAG_IMAP_SENT, + "\\DRAFTS": ImapUtils.FLAG_IMAP_DRAFTS, + "\\SPAM": ImapUtils.FLAG_IMAP_SPAM, + "\\JUNK": ImapUtils.FLAG_IMAP_SPAM, + "\\ARCHIVE": ImapUtils.FLAG_IMAP_ARCHIVE, + "\\ALL": ImapUtils.FLAG_IMAP_ALL_MAIL, + "\\ALLMAIL": ImapUtils.FLAG_IMAP_ALL_MAIL, + "\\INBOX": ImapUtils.FLAG_IMAP_INBOX, + "\\NONEXISTENT": + // RFC 5258 \NonExistent implies \NoSelect + ImapUtils.FLAG_NON_EXISTENT | ImapUtils.FLAG_NO_SELECT, + "\\SUBSCRIBED": ImapUtils.FLAG_SUBSCRIBED, + "\\REMOTE": ImapUtils.FLAG_REMOTE, + "\\HASCHILDREN": ImapUtils.FLAG_HAS_CHILDREN, + "\\HASNOCHILDREN": ImapUtils.FLAG_HAS_NO_CHILDREN, + }; + let flags = 0; + for (let str of arr) { + flags |= stringToFlag[str.toUpperCase()] || 0; + } + return flags; + } +} + +/** + * A structure to represent STATUS data. + * STATUS \"folder 2\" (UIDNEXT 2 MESSAGES 1 UNSEEN 1) + */ +class StatusData { + /** + * @params {Array<string|string[]>} tokens - The tokens of the line. + */ + constructor(tokens) { + this.attributes = {}; + + // The first two tokens are ["*", "STATUS"], the last token is the attribute + // list, the middle part is the mailbox name. + this.attributes.mailbox = unwrapString(tokens[2]); + + let attributes = tokens.at(-1); + for (let i = 0; i < attributes.length; i += 2) { + let type = attributes[i].toLowerCase(); + this.attributes[type] = attributes[i + 1]; + } + } +} + +/** + * Following rfc3501 section-5.1 and section-9, this function does two things: + * 1. Remove the wrapping DQUOTE. + * 2. Unesacpe QUOTED-CHAR. + * + * @params {string} name - E.g. `"a \"b\" c"` will become `a "b" c`. + */ +function unwrapString(name) { + return name.replace(/(^"|"$)/g, "").replaceAll('\\"', '"'); +} diff --git a/comm/mailnews/imap/src/ImapService.jsm b/comm/mailnews/imap/src/ImapService.jsm new file mode 100644 index 0000000000..abc4036300 --- /dev/null +++ b/comm/mailnews/imap/src/ImapService.jsm @@ -0,0 +1,518 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["ImapService"]; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; + +XPCOMUtils.defineLazyModuleGetters(lazy, { + ImapChannel: "resource:///modules/ImapChannel.jsm", + MailStringUtils: "resource:///modules/MailStringUtils.jsm", +}); + +/** + * Set mailnews.imap.jsmodule to true to use this module. + * + * @implements {nsIImapService} + */ +class ImapService { + QueryInterface = ChromeUtils.generateQI(["nsIImapService"]); + + constructor() { + // Initialize nsIAutoSyncManager. + Cc["@mozilla.org/imap/autosyncmgr;1"].getService(Ci.nsIAutoSyncManager); + } + + get cacheStorage() { + if (!this._cacheStorage) { + this._cacheStorage = Services.cache2.memoryCacheStorage( + Services.loadContextInfo.custom(false, {}) + ); + } + return this._cacheStorage; + } + + selectFolder(folder, urlListener, msgWindow) { + return this._withClient(folder, (client, runningUrl) => { + client.startRunningUrl( + urlListener || folder.QueryInterface(Ci.nsIUrlListener), + msgWindow, + runningUrl + ); + runningUrl.updatingFolder = true; + client.onReady = () => { + client.selectFolder(folder); + }; + }); + } + + liteSelectFolder(folder, urlListener, msgWindow) { + return this._withClient(folder, (client, runningUrl) => { + client.startRunningUrl( + urlListener || folder.QueryInterface(Ci.nsIUrlListener), + msgWindow, + runningUrl + ); + runningUrl.QueryInterface(Ci.nsIImapUrl).imapAction = + Ci.nsIImapUrl.nsImapLiteSelectFolder; + client.onReady = () => { + client.selectFolder(folder); + }; + }); + } + + discoverAllFolders(folder, urlListener, msgWindow) { + let server = folder.QueryInterface( + Ci.nsIMsgImapMailFolder + ).imapIncomingServer; + if (server.wrappedJSObject.hasDiscoveredFolders) { + return; + } + server.wrappedJSObject.hasDiscoveredFolders = true; + this._withClient(folder, client => { + client.startRunningUrl(urlListener, msgWindow); + client.onReady = () => { + client.discoverAllFolders(folder); + }; + }); + } + + discoverAllAndSubscribedFolders(folder, urlListener, msgWindow) { + this._withClient(folder, client => { + let runningUrl = client.startRunningUrl(urlListener, msgWindow); + runningUrl.QueryInterface(Ci.nsIImapUrl).imapAction = + Ci.nsIImapUrl.nsImapDiscoverAllAndSubscribedBoxesUrl; + client.onReady = () => { + client.discoverAllAndSubscribedFolders(folder); + }; + }); + } + + getListOfFoldersOnServer(server, msgWindow) { + this.discoverAllAndSubscribedFolders( + server.rootMsgFolder, + server.QueryInterface(Ci.nsIUrlListener), + msgWindow + ); + } + + subscribeFolder(folder, name, urlListener) { + return this._withClient(folder, client => { + client.startRunningUrl(urlListener); + client.onReady = () => { + client.subscribeFolder(folder, name); + }; + }); + } + + unsubscribeFolder(folder, name, urlListener) { + return this._withClient(folder, client => { + client.startRunningUrl(urlListener); + client.onReady = () => { + client.unsubscribeFolder(folder, name); + }; + }); + } + + addMessageFlags(folder, urlListener, messageIds, flags, messageIdsAreUID) { + this._updateMessageFlags("+", folder, urlListener, messageIds, flags); + } + + subtractMessageFlags( + folder, + urlListener, + messageIds, + flags, + messageIdsAreUID + ) { + this._updateMessageFlags("-", folder, urlListener, messageIds, flags); + } + + setMessageFlags( + folder, + urlListener, + outURL, + messageIds, + flags, + messageIdsAreUID + ) { + outURL.value = this._updateMessageFlags( + "", + folder, + urlListener, + messageIds, + flags + ); + } + + _updateMessageFlags(action, folder, urlListener, messageIds, flags) { + return this._withClient(folder, (client, runningUrl) => { + client.startRunningUrl(urlListener, null, runningUrl); + client.onReady = () => { + client.updateMessageFlags(action, folder, messageIds, flags); + }; + }); + } + + renameLeaf(folder, newName, urlListener, msgWindow) { + this._withClient(folder, client => { + client.startRunningUrl(urlListener, msgWindow); + client.onReady = () => { + client.renameFolder(folder, newName); + }; + }); + } + + fetchMessage( + imapUrl, + imapAction, + folder, + msgSink, + msgWindow, + displayConsumer, + msgIds, + convertDataToText + ) { + imapUrl.imapAction = imapAction; + imapUrl.QueryInterface(Ci.nsIMsgMailNewsUrl).msgWindow = msgWindow; + if (displayConsumer instanceof Ci.nsIDocShell) { + imapUrl + .QueryInterface(Ci.nsIMsgMailNewsUrl) + .loadURI( + displayConsumer.QueryInterface(Ci.nsIDocShell), + Ci.nsIWebNavigation.LOAD_FLAGS_NONE + ); + } else { + let streamListener = displayConsumer.QueryInterface(Ci.nsIStreamListener); + let channel = new lazy.ImapChannel(imapUrl, { + QueryInterface: ChromeUtils.generateQI(["nsILoadInfo"]), + loadingPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + securityFlags: + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + internalContentPolicy: Ci.nsIContentPolicy.TYPE_OTHER, + }); + let listener = streamListener; + if (convertDataToText) { + let converter = Cc["@mozilla.org/streamConverters;1"].getService( + Ci.nsIStreamConverterService + ); + listener = converter.asyncConvertData( + "message/rfc822", + "*/*", + streamListener, + channel + ); + } + channel.asyncOpen(listener); + } + } + + fetchCustomMsgAttribute(folder, msgWindow, attribute, uids) { + return this._withClient(folder, (client, runningUrl) => { + client.startRunningUrl(null, msgWindow, runningUrl); + client.onReady = () => { + client.fetchMsgAttribute(folder, uids, attribute); + }; + }); + } + + expunge(folder, urlListener, msgWindow) { + this._withClient(folder, client => { + client.startRunningUrl(urlListener, msgWindow); + client.onReady = () => { + client.expunge(folder); + }; + }); + } + + onlineMessageCopy( + folder, + messageIds, + dstFolder, + idsAreUids, + isMove, + urlListener, + outURL, + copyState, + msgWindow + ) { + this._withClient(folder, client => { + let runningUrl = client.startRunningUrl(urlListener, msgWindow); + runningUrl.QueryInterface(Ci.nsIImapUrl).imapAction = isMove + ? Ci.nsIImapUrl.nsImapOnlineMove + : Ci.nsIImapUrl.nsImapOnlineCopy; + client.onReady = () => { + client.copy(folder, dstFolder, messageIds, idsAreUids, isMove); + }; + }); + } + + appendMessageFromFile( + file, + dstFolder, + messageId, + idsAreUids, + inSelectedState, + urlListener, + copyState, + msgWindow + ) { + let server = dstFolder.server; + let imapUrl = Services.io + .newURI( + `imap://${server.hostName}:${server.port}/fetch>UID>/${dstFolder.name}>${messageId}` + ) + .QueryInterface(Ci.nsIImapUrl); + imapUrl.QueryInterface(Ci.nsIImapUrl).imapAction = + Ci.nsIImapUrl.nsImapAppendMsgFromFile; + imapUrl.copyState = copyState; + if (Services.io.offline) { + this._offlineAppendMessageFile(file, imapUrl, dstFolder, urlListener); + return; + } + this._withClient(dstFolder, client => { + client.startRunningUrl(urlListener, msgWindow, imapUrl); + client.onReady = () => { + client.uploadMessageFromFile( + file, + dstFolder, + copyState, + inSelectedState + ); + }; + }); + } + + /** + * Append a message file to a folder locally. + * + * @param {nsIFile} file - The message file to append. + * @param {nsIURI} url - The imap url to run. + * @param {nsIMsgFolder} dstFolder - The target message folder. + * @param {nsIUrlListener} urlListener - Callback for the request. + */ + async _offlineAppendMessageFile(file, url, dstFolder, urlListener) { + if (dstFolder.locked) { + const NS_MSG_FOLDER_BUSY = 2153054218; + throw Components.Exception( + "Destination folder locked", + NS_MSG_FOLDER_BUSY + ); + } + + let db = dstFolder.msgDatabase.QueryInterface(Ci.nsIMsgOfflineOpsDatabase); + let fakeKey = db.nextFakeOfflineMsgKey; + let op = db.getOfflineOpForKey(fakeKey, true); + op.operation = Ci.nsIMsgOfflineImapOperation.kAppendDraft; + op.destinationFolderURI = dstFolder.URI; + // Release op eagerly, to make test_offlineDraftDataloss happy in debug build. + op = null; + Cu.forceGC(); + + let server = dstFolder.server; + let newMsgHdr = db.createNewHdr(fakeKey); + let outputStream = dstFolder.getOfflineStoreOutputStream(newMsgHdr); + let content = lazy.MailStringUtils.uint8ArrayToByteString( + await IOUtils.read(file.path) + ); + + let msgParser = Cc[ + "@mozilla.org/messenger/messagestateparser;1" + ].createInstance(Ci.nsIMsgParseMailMsgState); + msgParser.SetMailDB(db); + msgParser.state = Ci.nsIMsgParseMailMsgState.ParseHeadersState; + msgParser.newMsgHdr = newMsgHdr; + msgParser.setNewKey(fakeKey); + + for (let line of content.split("\r\n")) { + line += "\r\n"; + msgParser.ParseAFolderLine(line, line.length); + outputStream.write(line, line.length); + } + msgParser.FinishHeader(); + + newMsgHdr.orFlags(Ci.nsMsgMessageFlags.Offline | Ci.nsMsgMessageFlags.Read); + newMsgHdr.offlineMessageSize = content.length; + db.addNewHdrToDB(newMsgHdr, true); + dstFolder.setFlag(Ci.nsMsgFolderFlags.OfflineEvents); + if (server.msgStore) { + server.msgStore.finishNewMessage(outputStream, newMsgHdr); + } + + urlListener.OnStopRunningUrl(url, Cr.NS_OK); + outputStream.close(); + db.close(true); + } + + ensureFolderExists(parent, folderName, msgWindow, urlListener) { + this._withClient(parent, client => { + let runningUrl = client.startRunningUrl(urlListener, msgWindow); + runningUrl.QueryInterface(Ci.nsIImapUrl).imapAction = + Ci.nsIImapUrl.nsImapEnsureExistsFolder; + client.onReady = () => { + client.ensureFolderExists(parent, folderName); + }; + }); + } + + updateFolderStatus(folder, urlListener) { + this._withClient(folder, client => { + let runningUrl = client.startRunningUrl(urlListener); + runningUrl.QueryInterface(Ci.nsIImapUrl).imapAction = + Ci.nsIImapUrl.nsImapFolderStatus; + client.onReady = () => { + client.updateFolderStatus(folder); + }; + }); + } + + createFolder(parent, folderName, urlListener) { + return this._withClient(parent, (client, runningUrl) => { + client.startRunningUrl(urlListener, null, runningUrl); + runningUrl.QueryInterface(Ci.nsIImapUrl).imapAction = + Ci.nsIImapUrl.nsImapCreateFolder; + client.onReady = () => { + client.createFolder(parent, folderName); + }; + }); + } + + moveFolder(srcFolder, dstFolder, urlListener, msgWindow) { + this._withClient(srcFolder, client => { + let runningUrl = client.startRunningUrl(urlListener, msgWindow); + runningUrl.QueryInterface(Ci.nsIImapUrl).imapAction = + Ci.nsIImapUrl.nsImapMoveFolderHierarchy; + client.onReady = () => { + client.moveFolder(srcFolder, dstFolder); + }; + }); + } + + listFolder(folder, urlListener) { + this._withClient(folder, client => { + let runningUrl = client.startRunningUrl(urlListener); + runningUrl.QueryInterface(Ci.nsIImapUrl).imapAction = + Ci.nsIImapUrl.nsImapListFolder; + client.onReady = () => { + client.listFolder(folder); + }; + }); + } + + deleteFolder(folder, urlListener, msgWindow) { + this._withClient(folder, client => { + client.startRunningUrl(urlListener, msgWindow); + client.onReady = () => { + client.deleteFolder(folder); + }; + }); + } + + storeCustomKeywords(folder, msgWindow, flagsToAdd, flagsToSubtract, uids) { + return this._withClient(folder, (client, runningUrl) => { + client.startRunningUrl(null, msgWindow, runningUrl); + runningUrl.QueryInterface(Ci.nsIImapUrl).imapAction = + Ci.nsIImapUrl.nsImapMsgStoreCustomKeywords; + client.onReady = () => { + client.storeCustomKeywords(folder, flagsToAdd, flagsToSubtract, uids); + }; + }); + } + + downloadMessagesForOffline(messageIds, folder, urlListener, msgWindow) { + let server = folder.QueryInterface( + Ci.nsIMsgImapMailFolder + ).imapIncomingServer; + let imapUrl = Services.io + .newURI( + `imap://${server.hostName}:${server.port}/fetch>UID>/${folder.name}>${messageIds}` + ) + .QueryInterface(Ci.nsIImapUrl); + imapUrl.storeResultsOffline = true; + if (urlListener) { + imapUrl + .QueryInterface(Ci.nsIMsgMailNewsUrl) + .RegisterListener(urlListener); + } + this._withClient(folder, client => { + client.startRunningUrl(urlListener, msgWindow, imapUrl); + client.onReady = () => { + client.fetchMessage(folder, messageIds); + }; + }); + } + + playbackAllOfflineOperations(msgWindow, urlListener) { + let offlineSync = Cc["@mozilla.org/imap/offlinesync;1"].createInstance( + Ci.nsIImapOfflineSync + ); + offlineSync.init(msgWindow, urlListener, null, false); + offlineSync.processNextOperation(); + } + + getHeaders(folder, urlListener, outURL, messageIds, messageIdsAreUID) { + return this._withClient(folder, (client, runningUrl) => { + client.startRunningUrl(urlListener, null, runningUrl); + runningUrl.QueryInterface(Ci.nsIImapUrl).imapAction = + Ci.nsIImapUrl.nsImapMsgFetch; + client.onReady = () => { + client.getHeaders(folder, messageIds); + }; + }); + } + + getBodyStart(folder, urlListener, messageIds, numBytes) { + return this._withClient(folder, (client, runningUrl) => { + runningUrl.QueryInterface(Ci.nsIImapUrl).imapAction = + Ci.nsIImapUrl.nsImapMsgPreview; + client.startRunningUrl(urlListener, null, runningUrl); + client.onReady = () => { + client.fetchMessage(folder, messageIds, numBytes); + }; + }); + } + + deleteAllMessages(folder, urlListener) { + this._withClient(folder, client => { + client.startRunningUrl(urlListener); + client.onReady = () => { + client.deleteAllMessages(folder); + }; + }); + } + + verifyLogon(folder, urlListener, msgWindow) { + return this._withClient(folder, (client, runningUrl) => { + client.verifyLogon = true; + client.startRunningUrl(urlListener, msgWindow, runningUrl); + client.onReady = () => {}; + }); + } + + /** + * Do some actions with a connection. + * + * @param {nsIMsgFolder} folder - The associated folder. + * @param {Function} handler - A callback function to take a ImapClient + * instance, and do some actions. + */ + _withClient(folder, handler) { + let server = folder.server.QueryInterface(Ci.nsIMsgIncomingServer); + let runningUrl = Services.io + .newURI(`imap://${server.hostName}:${server.port}`) + .QueryInterface(Ci.nsIMsgMailNewsUrl); + server.wrappedJSObject.withClient(folder, client => + handler(client, runningUrl) + ); + return runningUrl; + } +} + +ImapService.prototype.classID = Components.ID( + "{2ea8fbe6-029b-4bff-ae05-b794cf955afb}" +); diff --git a/comm/mailnews/imap/src/ImapUtils.jsm b/comm/mailnews/imap/src/ImapUtils.jsm new file mode 100644 index 0000000000..468d0c98a5 --- /dev/null +++ b/comm/mailnews/imap/src/ImapUtils.jsm @@ -0,0 +1,197 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["ImapCapFlags", "ImapUtils"]; + +/** + * The purpose here is not to convert all capabilities to flag number, but to + * interact with nsImapMailFolder through nsIImapIncomingServer.getCapability + * interface. + * + * @see nsImapCore.h + */ +var ImapCapFlags = { + Undefined: 0x00000000, + Defined: 0x00000001, + AuthLogin: 0x00000002, // AUTH LOGIN + XSender: 0x00000008, + IMAP4: 0x00000010, // RFC1734 + IMAP4rev1: 0x00000020, // RFC2060 + NoHierarchyRename: 0x00000080, // no hierarchy rename + ACL: 0x00000100, // ACL extension + Namespace: 0x00000200, // IMAP4 Namespace Extension + ID: 0x00000400, // client user agent id extension + XServerInfo: 0x00000800, // XSERVERINFO extension for admin urls + AuthPlain: 0x00001000, // new form of auth plain base64 login + Uidplus: 0x00002000, // RFC 2359 UIDPLUS extension + LiteralPlus: 0x00004000, // RFC 2088 LITERAL+ extension + AOLImap: 0x00008000, // aol imap extensions + Language: 0x00010000, // language extensions + CRAM: 0x00020000, // CRAM auth extension + Quota: 0x00040000, // RFC 2087 quota extension + Idle: 0x00080000, // RFC 2177 idle extension + AuthNTLM: 0x00100000, // AUTH NTLM extension + AuthMSN: 0x00200000, // AUTH MSN extension + StartTLS: 0x00400000, // STARTTLS support + AuthNone: 0x00800000, // needs no login + AuthGssApi: 0x01000000, // GSSAPI AUTH + CondStore: 0x02000000, // RFC 3551 CondStore extension + Enable: 0x04000000, // RFC 5161 ENABLE extension + XList: 0x08000000, // XLIST extension + CompressDeflate: 0x10000000, // RFC 4978 COMPRESS extension + AuthExternal: 0x20000000, // RFC 2222 SASL AUTH EXTERNAL + Move: 0x40000000, // Proposed MOVE RFC + HighestModSeq: 0x80000000, // Subset of RFC 3551 + ListExtended: 0x100000000, // RFC 5258 + SpecialUse: 0x200000000, // RFC 6154: Sent, Draft etc. folders + Gmail: 0x400000000, // X-GM-EXT-1 capability extension for gmail + XOAuth2: 0x800000000, // AUTH XOAUTH2 extension + ClientID: 0x1000000000, // ClientID capability + UTF8Accept: 0x2000000000, // RFC 6855: UTF8:ACCEPT + + /** + * Convert an array of capability string to an internal flag number, for example, + * ["QUOTA", "X-GM-EXT-1"] will become 0x400040000. + * + * @param {string[]} arr - An array of flag string. + * @returns {number} An internal flag number. + */ + stringsToFlags(arr) { + let flags = 0; + for (let str of arr) { + switch (str) { + case "QUOTA": + flags |= this.Quota; + break; + case "X-GM-EXT-1": + flags |= this.Gmail; + break; + default: + break; + } + } + return flags; + }, +}; + +/** + * Collection of helper functions for IMAP. + */ +var ImapUtils = { + NS_MSG_ERROR_IMAP_COMMAND_FAILED: 0x80550021, + + /** @see nsImapCore.h */ + FLAG_NONE: 0x0000, + /** mailbox flags */ + FLAG_MARKED: 0x01, + FLAG_UNMARKED: 0x02, + FLAG_NO_INFERIORS: 0x04, + FLAG_NO_SELECT: 0x08, + FLAG_IMAP_TRASH: 0x10, + FLAG_JUST_EXPUNGED: 0x20, + FLAG_PERSONAL_MAILBOX: 0x40, + FLAG_PUBLIC_MAILBOX: 0x80, + FLAG_OTHER_USERS_MAILBOX: 0x100, + FLAG_NAMESPACE: 0x200, + FLAG_NEWLY_CREATED_FOLDER: 0x400, + FLAG_IMAP_DRAFTS: 0x800, + FLAG_IMAP_SPAM: 0x1000, + FLAG_IMAP_SENT: 0x2000, + FLAG_IMAP_INBOX: 0x4000, + FLAG_IMAP_ALL_MAIL: 0x8000, + FLAG_IMAP_XLIST_TRASH: 0x10000, + FLAG_NON_EXISTENT: 0x20000, + FLAG_SUBSCRIBED: 0x40000, + FLAG_REMOTE: 0x80000, + FLAG_HAS_CHILDREN: 0x100000, + FLAG_HAS_NO_CHILDREN: 0x200000, + FLAG_IMAP_ARCHIVE: 0x400000, + + /** message flags */ + FLAG_SEEN: 0x0001, + FLAG_ANSWERED: 0x0002, + FLAG_FLAGGED: 0x0004, + FLAG_DELETED: 0x0008, + FLAG_DRAFT: 0x0010, + FLAG_FORWARDED: 0x0040, + FLAG_MDN_SENT: 0x0080, + FLAG_CUSTOM_KEYWORD: 0x0100, + FLAG_LABEL: 0x0e00, + FLAG_SUPPORT_FORWARDED_FLAG: 0x4000, + FLAG_SUPPORT_USER_FLAG: 0x8000, + + logger: console.createInstance({ + prefix: "mailnews.imap", + maxLogLevel: "Warn", + maxLogLevelPref: "mailnews.imap.loglevel", + }), + + /** + * Convert internal flag number to flag string, for example, + * 0x3 will become "\\Seen \\Answered". + * + * @param {number} flags - Internal flag number. + * @param {number} supportedFlags - Server supported flags. + * @returns {string} Flags string that can be sent to the server. + */ + flagsToString(flags, supportedFlags) { + let arr = []; + let strFlags = [ + ["\\Seen", this.FLAG_SEEN], + ["\\Answered", this.FLAG_ANSWERED], + ["\\Flagged", this.FLAG_FLAGGED], + ["\\Deleted", this.FLAG_DELETED], + ["\\Draft", this.FLAG_DRAFT], + ["$Forwarded", this.FLAG_FORWARDED], + ["$MDNSent", this.FLAG_MDN_SENT], + ]; + for (let [str, flag] of strFlags) { + if (flags & flag && supportedFlags & flag) { + arr.push(str); + } + } + return arr.join(" "); + }, + + /** + * Convert a flag string to an internal flag number, for example, + * "\\Seen" will become 0x1. + * + * @param {string} str - A single flag string. + * @returns {number} An internal flag number. + */ + stringToFlag(str) { + return ( + { + "\\SEEN": this.FLAG_SEEN, + "\\ANSWERED": this.FLAG_ANSWERED, + "\\FLAGGED": this.FLAG_FLAGGED, + "\\DELETED": this.FLAG_DELETED, + "\\DRAFT": this.FLAG_DRAFT, + "\\*": + this.FLAG_LABEL | + this.FLAG_MDN_SENT | + this.FLAG_FORWARDED | + this.FLAG_SUPPORT_USER_FLAG, + $MDNSENT: this.FLAG_MDN_SENT, + $FORWARDED: this.FLAG_FORWARDED, + }[str.toUpperCase()] || this.FLAG_NONE + ); + }, + + /** + * Convert an array of flag string to an internal flag number, for example, + * ["\\Seen", "\\Answered"] will become 0x3. + * + * @param {string[]} arr - An array of flag string. + * @returns {number} An internal flag number. + */ + stringsToFlags(arr) { + let flags = 0; + for (let str of arr) { + flags |= this.stringToFlag(str); + } + return flags; + }, +}; diff --git a/comm/mailnews/imap/src/components.conf b/comm/mailnews/imap/src/components.conf new file mode 100644 index 0000000000..c3c0f692cf --- /dev/null +++ b/comm/mailnews/imap/src/components.conf @@ -0,0 +1,76 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Classes = [ + { + "cid": "{26b5aa4d-742d-4112-b834-d6c4b1276878}", + "contract_ids": ["@mozilla.org/messenger/imap-module-loader;1"], + "jsm": "resource:///modules/ImapModuleLoader.jsm", + "constructor": "ImapModuleLoader", + "categories": {"profile-after-change": "ImapModuleLoader"}, + }, + { + "cid": "{21a89611-dc0d-11d2-806c-006008128c4e}", + "type": "nsImapUrl", + "headers": ["/comm/mailnews/imap/src/nsImapUrl.h"], + }, + { + "cid": "{8c0c40d1-e173-11d2-806e-006008128c4e}", + "type": "nsImapProtocol", + "headers": ["/comm/mailnews/imap/src/nsImapProtocol.h"], + }, + { + "cid": "{4eca51df-6734-11d3-989a-001083010e9b}", + "type": "nsImapMockChannel", + "headers": ["/comm/mailnews/imap/src/nsImapProtocol.h"], + }, + { + "cid": "{479ce8fc-e725-11d2-a505-0060b0fc04b7}", + "type": "nsImapHostSessionList", + "init_method": "Init", + "headers": ["/comm/mailnews/imap/src/nsImapHostSessionList.h"], + }, + { + "cid": "{8d3675e0-ed46-11d2-8077-006008128c4e}", + "contract_ids": ["@mozilla.org/messenger/server;1?type=imap"], + "type": "nsImapIncomingServer", + "init_method": "Init", + "headers": ["/comm/mailnews/imap/src/nsImapIncomingServer.h"], + }, + { + "cid": "{fa32d000-f6a0-11d2-af8d-001083002da8}", + "contract_ids": ["@mozilla.org/mail/folder-factory;1?name=imap"], + "type": "nsImapMailFolder", + "headers": ["/comm/mailnews/imap/src/nsImapMailFolder.h"], + }, + { + "cid": "{c5852b22-ebe2-11d2-95ad-000064657374}", + "contract_ids": [ + "@mozilla.org/messenger/messageservice;1?type=imap-message", + "@mozilla.org/messenger/messageservice;1?type=imap", + "@mozilla.org/messenger/imapservice;1", + "@mozilla.org/network/protocol;1?name=imap", + "@mozilla.org/messenger/protocol/info;1?type=imap", + "@mozilla.org/uriloader/content-handler;1?type=x-application-imapfolder", + ], + "type": "nsImapService", + "headers": ["/comm/mailnews/imap/src/nsImapService.h"], + "name": "Imap", + "interfaces": ["nsIImapService"], + }, + { + "cid": "{c358c568-47b2-42b2-8146-3c0f8d1fad6e}", + "contract_ids": ["@mozilla.org/imap/autosyncmgr;1"], + "type": "nsAutoSyncManager", + "headers": ["/comm/mailnews/imap/src/nsAutoSyncManager.h"], + }, + { + "cid": "{64fa0a31-a494-4f4b-ac4d-b29910d6ccd6}", + "contract_ids": ["@mozilla.org/imap/offlinesync;1"], + "type": "nsImapOfflineSync", + "headers": ["/comm/mailnews/imap/src/nsImapOfflineSync.h"], + }, +] diff --git a/comm/mailnews/imap/src/moz.build b/comm/mailnews/imap/src/moz.build new file mode 100644 index 0000000000..0d1f2150de --- /dev/null +++ b/comm/mailnews/imap/src/moz.build @@ -0,0 +1,56 @@ +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +EXPORTS += [ + "nsImapCore.h", +] + +SOURCES += [ + "nsAutoSyncManager.cpp", + "nsAutoSyncState.cpp", + "nsImapFlagAndUidState.cpp", + "nsImapGenericParser.cpp", + "nsImapHostSessionList.cpp", + "nsImapIncomingServer.cpp", + "nsImapMailFolder.cpp", + "nsImapNamespace.cpp", + "nsImapOfflineSync.cpp", + "nsImapProtocol.cpp", + "nsImapSearchResults.cpp", + "nsImapServerResponseParser.cpp", + "nsImapService.cpp", + "nsImapStringBundle.cpp", + "nsImapUndoTxn.cpp", + "nsImapUrl.cpp", + "nsImapUtils.cpp", + "nsSyncRunnableHelpers.cpp", +] + +FINAL_LIBRARY = "mail" + +LOCAL_INCLUDES += [ + # for nsImapProtocol.cpp + "!/ipc/ipdl/_ipdlheaders", + "/ipc/chromium/src", + "/netwerk/base", +] + +EXTRA_JS_MODULES += [ + "ImapChannel.jsm", + "ImapClient.jsm", + "ImapFolderContentHandler.sys.mjs", + "ImapIncomingServer.jsm", + "ImapMessageService.jsm", + "ImapModuleLoader.jsm", + "ImapProtocolHandler.jsm", + "ImapProtocolInfo.jsm", + "ImapResponse.jsm", + "ImapService.jsm", + "ImapUtils.jsm", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] diff --git a/comm/mailnews/imap/src/nsAutoSyncManager.cpp b/comm/mailnews/imap/src/nsAutoSyncManager.cpp new file mode 100644 index 0000000000..e0b9611dc0 --- /dev/null +++ b/comm/mailnews/imap/src/nsAutoSyncManager.cpp @@ -0,0 +1,1372 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsAutoSyncManager.h" +#include "nsAutoSyncState.h" +#include "nsIMsgImapMailFolder.h" +#include "nsIMsgHdr.h" +#include "nsIObserverService.h" +#include "nsIMsgAccountManager.h" +#include "nsIMsgIncomingServer.h" +#include "nsIImapIncomingServer.h" +#include "nsIMsgMailSession.h" +#include "nsMsgFolderFlags.h" +#include "nsMsgMessageFlags.h" +#include "nsMsgUtils.h" +#include "nsIIOService.h" +#include "nsITimer.h" +#include "nsComponentManagerUtils.h" +#include "nsServiceManagerUtils.h" +#include "mozilla/Services.h" +#include "mozilla/Logging.h" + +using namespace mozilla; + +NS_IMPL_ISUPPORTS(nsDefaultAutoSyncMsgStrategy, nsIAutoSyncMsgStrategy) + +const char* kAppIdleNotification = "mail:appIdle"; +const char* kStartupDoneNotification = "mail-startup-done"; +LazyLogModule gAutoSyncLog("IMAPAutoSync"); + +// recommended size of each group of messages per download +static const uint32_t kDefaultGroupSize = 50U * 1024U /* 50K */; + +nsDefaultAutoSyncMsgStrategy::nsDefaultAutoSyncMsgStrategy() {} + +nsDefaultAutoSyncMsgStrategy::~nsDefaultAutoSyncMsgStrategy() {} + +NS_IMETHODIMP nsDefaultAutoSyncMsgStrategy::Sort( + nsIMsgFolder* aFolder, nsIMsgDBHdr* aMsgHdr1, nsIMsgDBHdr* aMsgHdr2, + nsAutoSyncStrategyDecisionType* aDecision) { + NS_ENSURE_ARG_POINTER(aDecision); + + uint32_t msgSize1 = 0, msgSize2 = 0; + PRTime msgDate1 = 0, msgDate2 = 0; + + if (!aMsgHdr1 || !aMsgHdr2) { + *aDecision = nsAutoSyncStrategyDecisions::Same; + return NS_OK; + } + + aMsgHdr1->GetMessageSize(&msgSize1); + aMsgHdr1->GetDate(&msgDate1); + + aMsgHdr2->GetMessageSize(&msgSize2); + aMsgHdr2->GetDate(&msgDate2); + + // Special case: if message size is larger than a + // certain size, then place it to the bottom of the q + if (msgSize2 > kFirstPassMessageSize && msgSize1 > kFirstPassMessageSize) + *aDecision = msgSize2 > msgSize1 ? nsAutoSyncStrategyDecisions::Lower + : nsAutoSyncStrategyDecisions::Higher; + else if (msgSize2 > kFirstPassMessageSize) + *aDecision = nsAutoSyncStrategyDecisions::Lower; + else if (msgSize1 > kFirstPassMessageSize) + *aDecision = nsAutoSyncStrategyDecisions::Higher; + else { + // Most recent and smallest first + if (msgDate1 < msgDate2) + *aDecision = nsAutoSyncStrategyDecisions::Higher; + else if (msgDate1 > msgDate2) + *aDecision = nsAutoSyncStrategyDecisions::Lower; + else { + if (msgSize1 > msgSize2) + *aDecision = nsAutoSyncStrategyDecisions::Higher; + else if (msgSize1 < msgSize2) + *aDecision = nsAutoSyncStrategyDecisions::Lower; + else + *aDecision = nsAutoSyncStrategyDecisions::Same; + } + } + return NS_OK; +} + +NS_IMETHODIMP nsDefaultAutoSyncMsgStrategy::IsExcluded(nsIMsgFolder* aFolder, + nsIMsgDBHdr* aMsgHdr, + bool* aDecision) { + NS_ENSURE_ARG_POINTER(aDecision); + NS_ENSURE_ARG_POINTER(aMsgHdr); + NS_ENSURE_ARG_POINTER(aFolder); + nsCOMPtr<nsIMsgIncomingServer> server; + + nsresult rv = aFolder->GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIImapIncomingServer> imapServer(do_QueryInterface(server, &rv)); + int32_t offlineMsgAgeLimit = -1; + imapServer->GetAutoSyncMaxAgeDays(&offlineMsgAgeLimit); + NS_ENSURE_SUCCESS(rv, rv); + PRTime msgDate; + aMsgHdr->GetDate(&msgDate); + *aDecision = offlineMsgAgeLimit > 0 && + msgDate < MsgConvertAgeInDaysToCutoffDate(offlineMsgAgeLimit); + return NS_OK; +} + +NS_IMPL_ISUPPORTS(nsDefaultAutoSyncFolderStrategy, nsIAutoSyncFolderStrategy) + +nsDefaultAutoSyncFolderStrategy::nsDefaultAutoSyncFolderStrategy() {} + +nsDefaultAutoSyncFolderStrategy::~nsDefaultAutoSyncFolderStrategy() {} + +NS_IMETHODIMP nsDefaultAutoSyncFolderStrategy::Sort( + nsIMsgFolder* aFolderA, nsIMsgFolder* aFolderB, + nsAutoSyncStrategyDecisionType* aDecision) { + NS_ENSURE_ARG_POINTER(aDecision); + + if (!aFolderA || !aFolderB) { + *aDecision = nsAutoSyncStrategyDecisions::Same; + return NS_OK; + } + + bool isInbox1, isInbox2, isDrafts1, isDrafts2, isTrash1, isTrash2; + aFolderA->GetFlag(nsMsgFolderFlags::Inbox, &isInbox1); + aFolderB->GetFlag(nsMsgFolderFlags::Inbox, &isInbox2); + // + aFolderA->GetFlag(nsMsgFolderFlags::Drafts, &isDrafts1); + aFolderB->GetFlag(nsMsgFolderFlags::Drafts, &isDrafts2); + // + aFolderA->GetFlag(nsMsgFolderFlags::Trash, &isTrash1); + aFolderB->GetFlag(nsMsgFolderFlags::Trash, &isTrash2); + + // Follow this order; + // INBOX > DRAFTS > SUBFOLDERS > TRASH + + // test whether the folder is opened by the user. + // we give high priority to the folders explicitly opened by + // the user. + nsresult rv; + bool folderAOpen = false; + bool folderBOpen = false; + nsCOMPtr<nsIMsgMailSession> session = + do_GetService("@mozilla.org/messenger/services/session;1", &rv); + if (NS_SUCCEEDED(rv) && session) { + session->IsFolderOpenInWindow(aFolderA, &folderAOpen); + session->IsFolderOpenInWindow(aFolderB, &folderBOpen); + } + + if (folderAOpen == folderBOpen) { + // if both of them or none of them are opened by the user + // make your decision based on the folder type + if (isInbox2 || (isDrafts2 && !isInbox1) || isTrash1) + *aDecision = nsAutoSyncStrategyDecisions::Higher; + else if (isInbox1 || (isDrafts1 && !isDrafts2) || isTrash2) + *aDecision = nsAutoSyncStrategyDecisions::Lower; + else + *aDecision = nsAutoSyncStrategyDecisions::Same; + } else { + // otherwise give higher priority to opened one + *aDecision = folderBOpen ? nsAutoSyncStrategyDecisions::Higher + : nsAutoSyncStrategyDecisions::Lower; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsDefaultAutoSyncFolderStrategy::IsExcluded(nsIMsgFolder* aFolder, + bool* aDecision) { + NS_ENSURE_ARG_POINTER(aDecision); + NS_ENSURE_ARG_POINTER(aFolder); + uint32_t folderFlags; + aFolder->GetFlags(&folderFlags); + // exclude saved search + *aDecision = (folderFlags & nsMsgFolderFlags::Virtual); + if (!*aDecision) { + // Exclude orphans + nsCOMPtr<nsIMsgFolder> parent; + aFolder->GetParent(getter_AddRefs(parent)); + if (!parent) *aDecision = true; + } + return NS_OK; +} + +#define NOTIFY_LISTENERS_STATIC(obj_, propertyfunc_, params_) \ + PR_BEGIN_MACRO \ + nsTObserverArray<nsCOMPtr<nsIAutoSyncMgrListener>>::ForwardIterator iter( \ + obj_->mListeners); \ + nsCOMPtr<nsIAutoSyncMgrListener> listener; \ + while (iter.HasMore()) { \ + listener = iter.GetNext(); \ + listener->propertyfunc_ params_; \ + } \ + PR_END_MACRO + +#define NOTIFY_LISTENERS(propertyfunc_, params_) \ + NOTIFY_LISTENERS_STATIC(this, propertyfunc_, params_) + +nsAutoSyncManager::nsAutoSyncManager() { + mGroupSize = kDefaultGroupSize; + + mIdleState = notIdle; + mStartupDone = false; + mDownloadModel = dmChained; + mUpdateInProgress = false; + mPaused = false; + + nsresult rv; + mIdleService = do_GetService("@mozilla.org/widget/useridleservice;1", &rv); + if (mIdleService) mIdleService->AddIdleObserver(this, kIdleTimeInSec); + + // Observe xpcom-shutdown event and app-idle changes + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + + rv = observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false); + observerService->AddObserver(this, kAppIdleNotification, false); + observerService->AddObserver(this, NS_IOSERVICE_OFFLINE_STATUS_TOPIC, false); + observerService->AddObserver(this, NS_IOSERVICE_GOING_OFFLINE_TOPIC, false); + observerService->AddObserver(this, kStartupDoneNotification, false); +} + +nsAutoSyncManager::~nsAutoSyncManager() {} + +void nsAutoSyncManager::InitTimer() { + if (!mTimer) { + nsresult rv = NS_NewTimerWithFuncCallback( + getter_AddRefs(mTimer), TimerCallback, (void*)this, kTimerIntervalInMs, + nsITimer::TYPE_REPEATING_SLACK, "nsAutoSyncManager::TimerCallback", + nullptr); + if (NS_FAILED(rv)) { + NS_WARNING("Could not start nsAutoSyncManager timer"); + } + } +} + +void nsAutoSyncManager::StopTimer() { + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } +} + +void nsAutoSyncManager::StartTimerIfNeeded() { + if ((mUpdateQ.Count() > 0 || mDiscoveryQ.Count() > 0) && !mTimer) InitTimer(); +} + +void nsAutoSyncManager::TimerCallback(nsITimer* aTimer, void* aClosure) { + if (!aClosure) return; + + nsAutoSyncManager* autoSyncMgr = static_cast<nsAutoSyncManager*>(aClosure); + if (autoSyncMgr->GetIdleState() == notIdle || + (autoSyncMgr->mDiscoveryQ.Count() <= 0 && + autoSyncMgr->mUpdateQ.Count() <= 0)) { + // Idle will create a new timer automatically if discovery Q or update Q is + // not empty + autoSyncMgr->StopTimer(); + } + + // process a folder in the discovery queue + if (autoSyncMgr->mDiscoveryQ.Count() > 0) { + nsCOMPtr<nsIAutoSyncState> autoSyncStateObj(autoSyncMgr->mDiscoveryQ[0]); + if (autoSyncStateObj) { + uint32_t leftToProcess; + nsresult rv = autoSyncStateObj->ProcessExistingHeaders( + kNumberOfHeadersToProcess, &leftToProcess); + + nsCOMPtr<nsIMsgFolder> folder; + autoSyncStateObj->GetOwnerFolder(getter_AddRefs(folder)); + if (folder) + NOTIFY_LISTENERS_STATIC( + autoSyncMgr, OnDiscoveryQProcessed, + (folder, kNumberOfHeadersToProcess, leftToProcess)); + + if (NS_SUCCEEDED(rv) && 0 == leftToProcess) { + autoSyncMgr->mDiscoveryQ.RemoveObjectAt(0); + if (folder) + NOTIFY_LISTENERS_STATIC( + autoSyncMgr, OnFolderRemovedFromQ, + (nsIAutoSyncMgrListener::DiscoveryQueue, folder)); + } + if (MOZ_LOG_TEST(gAutoSyncLog, LogLevel::Debug)) { + nsCString folderName; + folder->GetURI(folderName); + MOZ_LOG(gAutoSyncLog, LogLevel::Debug, + ("%s: processed discovery q for folder=%s, " + "msgs left to process in folder=%d", + __func__, folderName.get(), leftToProcess)); + } + } + } + + if (autoSyncMgr->mUpdateQ.Count() > 0) { + if (!autoSyncMgr->mUpdateInProgress) // Avoids possible overlap of updates + { + nsCOMPtr<nsIAutoSyncState> autoSyncStateObj(autoSyncMgr->mUpdateQ[0]); + if (autoSyncStateObj) { + int32_t state; + nsresult rv = autoSyncStateObj->GetState(&state); + if (NS_SUCCEEDED(rv) && (state == nsAutoSyncState::stCompletedIdle || + state == nsAutoSyncState::stUpdateNeeded)) { + nsCOMPtr<nsIMsgFolder> folder; + autoSyncStateObj->GetOwnerFolder(getter_AddRefs(folder)); + if (folder) { + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = + do_QueryInterface(folder, &rv); + NS_ENSURE_SUCCESS_VOID(rv); + rv = imapFolder->InitiateAutoSync(autoSyncMgr); + if (NS_SUCCEEDED(rv)) { + autoSyncMgr->mUpdateInProgress = true; + NOTIFY_LISTENERS_STATIC(autoSyncMgr, OnAutoSyncInitiated, + (folder)); + } + } + if (MOZ_LOG_TEST(gAutoSyncLog, LogLevel::Debug)) { + nsCString folderName; + folder->GetURI(folderName); + MOZ_LOG(gAutoSyncLog, LogLevel::Debug, + ("%s: process update q for folder=%s", __func__, + folderName.get())); + } + } + } + } + // if initiation is not successful for some reason, or + // if there is an on going download for this folder, + // remove it from q and continue with the next one + if (!autoSyncMgr->mUpdateInProgress) { + nsCOMPtr<nsIMsgFolder> folder; + autoSyncMgr->mUpdateQ[0]->GetOwnerFolder(getter_AddRefs(folder)); + + autoSyncMgr->mUpdateQ.RemoveObjectAt(0); + + if (folder) + NOTIFY_LISTENERS_STATIC(autoSyncMgr, OnFolderRemovedFromQ, + (nsIAutoSyncMgrListener::UpdateQueue, folder)); + if (MOZ_LOG_TEST(gAutoSyncLog, LogLevel::Error)) { + nsCString folderName; + folder->GetURI(folderName); + MOZ_LOG(gAutoSyncLog, LogLevel::Error, + ("%s: update q init failed for folder=%s", __func__, + folderName.get())); + } + } + + } // endif +} + +/** + * Populates aChainedQ with the auto-sync state objects that are not owned by + * the same imap server. + * Assumes that aChainedQ initially empty. + */ +void nsAutoSyncManager::ChainFoldersInQ( + const nsCOMArray<nsIAutoSyncState>& aQueue, + nsCOMArray<nsIAutoSyncState>& aChainedQ) { + if (aQueue.Count() > 0) aChainedQ.AppendObject(aQueue[0]); + + int32_t pqElemCount = aQueue.Count(); + for (int32_t pqidx = 1; pqidx < pqElemCount; pqidx++) { + bool chained = false; + int32_t needToBeReplacedWith = -1; + int32_t elemCount = aChainedQ.Count(); + for (int32_t idx = 0; idx < elemCount; idx++) { + bool isSibling; + nsresult rv = aChainedQ[idx]->IsSibling(aQueue[pqidx], &isSibling); + + if (NS_SUCCEEDED(rv) && isSibling) { + // this prevent us to overwrite a lower priority sibling in + // download-in-progress state with a higher priority one. + // we have to wait until its download is completed before + // switching to new one. + int32_t state; + aQueue[pqidx]->GetState(&state); + if (aQueue[pqidx] != aChainedQ[idx] && + state == nsAutoSyncState::stDownloadInProgress) + needToBeReplacedWith = idx; + else + chained = true; + + break; + } + } // endfor + + if (needToBeReplacedWith > -1) + aChainedQ.ReplaceObjectAt(aQueue[pqidx], needToBeReplacedWith); + else if (!chained) + aChainedQ.AppendObject(aQueue[pqidx]); + + } // endfor +} + +/** + * Searches the given queue for another folder owned by the same imap server. + */ +nsIAutoSyncState* nsAutoSyncManager::SearchQForSibling( + const nsCOMArray<nsIAutoSyncState>& aQueue, + nsIAutoSyncState* aAutoSyncStateObj, int32_t aStartIdx, int32_t* aIndex) { + if (aIndex) *aIndex = -1; + + if (aAutoSyncStateObj) { + bool isSibling; + int32_t elemCount = aQueue.Count(); + for (int32_t idx = aStartIdx; idx < elemCount; idx++) { + nsresult rv = aAutoSyncStateObj->IsSibling(aQueue[idx], &isSibling); + + if (NS_SUCCEEDED(rv) && isSibling && aAutoSyncStateObj != aQueue[idx]) { + if (aIndex) *aIndex = idx; + + return aQueue[idx]; + } + } + } + return nullptr; +} + +/** + * Searches for the next folder owned by the same imap server in the given + * queue, starting from the index of the given folder. + */ +nsIAutoSyncState* nsAutoSyncManager::GetNextSibling( + const nsCOMArray<nsIAutoSyncState>& aQueue, + nsIAutoSyncState* aAutoSyncStateObj, int32_t* aIndex) { + if (aIndex) *aIndex = -1; + + if (aAutoSyncStateObj) { + bool located = false; + bool isSibling; + int32_t elemCount = aQueue.Count(); + for (int32_t idx = 0; idx < elemCount; idx++) { + if (!located) { + located = (aAutoSyncStateObj == aQueue[idx]); + continue; + } + + nsresult rv = aAutoSyncStateObj->IsSibling(aQueue[idx], &isSibling); + if (NS_SUCCEEDED(rv) && isSibling) { + if (aIndex) *aIndex = idx; + + return aQueue[idx]; + } + } + } + return nullptr; +} + +/** + * Checks whether there is another folder in the given q that is owned + * by the same imap server or not. + * + * @param aQueue the queue that will be searched for a sibling + * @param aAutoSyncStateObj the auto-sync state object that we are looking + * a sibling for + * @param aState the state of the sibling. -1 means "any state" + * @param aIndex [out] the index of the found sibling, if it is provided by the + * caller (not null) + * @return true if found, false otherwise + */ +bool nsAutoSyncManager::DoesQContainAnySiblingOf( + const nsCOMArray<nsIAutoSyncState>& aQueue, + nsIAutoSyncState* aAutoSyncStateObj, const int32_t aState, + int32_t* aIndex) { + if (aState == -1) + return (nullptr != SearchQForSibling(aQueue, aAutoSyncStateObj, 0, aIndex)); + + int32_t offset = 0; + nsIAutoSyncState* autoSyncState; + while ((autoSyncState = + SearchQForSibling(aQueue, aAutoSyncStateObj, offset, &offset))) { + int32_t state; + nsresult rv = autoSyncState->GetState(&state); + if (NS_SUCCEEDED(rv) && aState == state) break; + + offset++; + } + if (aIndex) *aIndex = offset; + + return (nullptr != autoSyncState); +} + +/** + * Searches the given queue for the highest priority folder owned by the + * same imap server. + */ +nsIAutoSyncState* nsAutoSyncManager::GetHighestPrioSibling( + const nsCOMArray<nsIAutoSyncState>& aQueue, + nsIAutoSyncState* aAutoSyncStateObj, int32_t* aIndex) { + return SearchQForSibling(aQueue, aAutoSyncStateObj, 0, aIndex); +} + +// to chain update folder actions +NS_IMETHODIMP nsAutoSyncManager::OnStartRunningUrl(nsIURI* aUrl) { + return NS_OK; +} + +/** + * This is called when an update folder URL finishes. It is also called by + * nsAutoSyncState::OnStopRunningUrl when a folder status URL finishes. + */ +NS_IMETHODIMP nsAutoSyncManager::OnStopRunningUrl(nsIURI* aUrl, + nsresult aExitCode) { + if (MOZ_LOG_TEST(gAutoSyncLog, LogLevel::Debug)) { + nsCString uri; + if (aUrl) uri = aUrl->GetSpecOrDefault(); + MOZ_LOG(gAutoSyncLog, LogLevel::Debug, + ("nsAutoSyncManager::%s, count=%d, url=%s", __func__, + mUpdateQ.Count(), uri.get())); + } + mUpdateInProgress = false; // Set false to allow next folder to update + if (mUpdateQ.Count() > 0) mUpdateQ.RemoveObjectAt(0); + + return aExitCode; +} + +/** + * This occurs on system sleep, hibernate or when TB is set offline or shutdown. + */ +NS_IMETHODIMP nsAutoSyncManager::Pause() { + StopTimer(); + mPaused = true; + MOZ_LOG(gAutoSyncLog, LogLevel::Debug, ("autosync paused")); + return NS_OK; +} + +/** + * This occurs on wakeup from sleep or hibernate and when TB is returned online. + */ +NS_IMETHODIMP nsAutoSyncManager::Resume() { + mPaused = false; + StartTimerIfNeeded(); + // If mUpdateInProgress was true on resume it needs to be reset back to false + // to avoid inhibiting autosync until a restart. OnStopRunningUrl(), where it + // is normally reset, may not occur depending on timing and autosync will + // never be initiated in TimerCallback() for any folder. + MOZ_LOG(gAutoSyncLog, LogLevel::Debug, + ("autosync resumed, mUpdateInProgress=%d(bool)", mUpdateInProgress)); + mUpdateInProgress = false; // May already be false, that's OK + return NS_OK; +} + +NS_IMETHODIMP nsAutoSyncManager::Observe(nsISupports*, const char* aTopic, + const char16_t* aSomeData) { + if (!PL_strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (observerService) { + observerService->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); + observerService->RemoveObserver(this, kAppIdleNotification); + observerService->RemoveObserver(this, NS_IOSERVICE_OFFLINE_STATUS_TOPIC); + observerService->RemoveObserver(this, NS_IOSERVICE_GOING_OFFLINE_TOPIC); + observerService->RemoveObserver(this, kStartupDoneNotification); + } + + // cancel and release the timer + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } + // unsubscribe from idle service + if (mIdleService) mIdleService->RemoveIdleObserver(this, kIdleTimeInSec); + + return NS_OK; + } + + if (!PL_strcmp(aTopic, kStartupDoneNotification)) { + mStartupDone = true; + } else if (!PL_strcmp(aTopic, kAppIdleNotification)) { + if (nsDependentString(aSomeData).EqualsLiteral("idle")) { + IdleState prevIdleState = GetIdleState(); + + // we were already idle (either system or app), so + // just remember that we're app idle and return. + SetIdleState(appIdle); + if (prevIdleState != notIdle) return NS_OK; + + MOZ_LOG(gAutoSyncLog, LogLevel::Debug, ("%s: in app idle", __func__)); + return StartIdleProcessing(); + } + + // we're back from appIdle - if already notIdle, just return; + if (GetIdleState() == notIdle) return NS_OK; + MOZ_LOG(gAutoSyncLog, LogLevel::Debug, ("%s: out of app idle", __func__)); + + SetIdleState(notIdle); + NOTIFY_LISTENERS(OnStateChanged, (false)); + return NS_OK; + } else if (!PL_strcmp(aTopic, NS_IOSERVICE_OFFLINE_STATUS_TOPIC)) { + if (nsDependentString(aSomeData).EqualsLiteral(NS_IOSERVICE_ONLINE)) + Resume(); + } else if (!PL_strcmp(aTopic, NS_IOSERVICE_GOING_OFFLINE_TOPIC)) { + Pause(); + } + // we're back from system idle + else if (!PL_strcmp(aTopic, "back")) { + // if we're app idle when we get back from system idle, we ignore + // it, since we'll keep doing our idle stuff. + if (GetIdleState() != appIdle) { + SetIdleState(notIdle); + NOTIFY_LISTENERS(OnStateChanged, (false)); + MOZ_LOG(gAutoSyncLog, LogLevel::Debug, ("%s: out of idle", __func__)); + } + return NS_OK; + } else // we've gone system idle + { + // Check if we were already idle. We may have gotten + // multiple system idle notificatons. In that case, + // just remember that we're systemIdle and return; + if (GetIdleState() != notIdle) return NS_OK; + + // we might want to remember if we were app idle, because + // coming back from system idle while app idle shouldn't stop + // app indexing. But I think it's OK for now just leave ourselves + // in appIdle state. + if (GetIdleState() != appIdle) SetIdleState(systemIdle); + if (WeAreOffline()) return NS_OK; + MOZ_LOG(gAutoSyncLog, LogLevel::Debug, ("%s: in sys idle", __func__)); + return StartIdleProcessing(); + } + return NS_OK; +} + +nsresult nsAutoSyncManager::StartIdleProcessing() { + if (mPaused) return NS_OK; + + MOZ_LOG(gAutoSyncLog, LogLevel::Debug, ("enter %s", __func__)); + StartTimerIfNeeded(); + + // Ignore idle events sent during the startup + if (!mStartupDone) return NS_OK; + + // notify listeners that auto-sync is running + NOTIFY_LISTENERS(OnStateChanged, (true)); + + nsCOMArray<nsIAutoSyncState> chainedQ; + nsCOMArray<nsIAutoSyncState>* queue = &mPriorityQ; + if (mDownloadModel == dmChained) { + ChainFoldersInQ(mPriorityQ, chainedQ); + queue = &chainedQ; + } + + // to store the folders that should be removed from the priority + // queue at the end of the iteration. + nsCOMArray<nsIAutoSyncState> foldersToBeRemoved; + + // process folders in the priority queue + int32_t elemCount = queue->Count(); + for (int32_t idx = 0; idx < elemCount; idx++) { + nsCOMPtr<nsIAutoSyncState> autoSyncStateObj((*queue)[idx]); + if (!autoSyncStateObj) continue; + + int32_t state; + autoSyncStateObj->GetState(&state); + + // TODO: Test cached-connection availability in parallel mode + // and do not exceed (cached-connection count - 1) + + if (state != nsAutoSyncState::stReadyToDownload) continue; + + nsresult rv = DownloadMessagesForOffline(autoSyncStateObj); + if (NS_FAILED(rv)) { + // special case: this folder does not have any message to download + // (see bug 457342), remove it explicitly from the queue when iteration + // is over. + // Note that in normal execution flow, folders are removed from priority + // queue only in OnDownloadCompleted when all messages are downloaded + // successfully. This is the only place we change this flow. + if (NS_ERROR_NOT_AVAILABLE == rv) + foldersToBeRemoved.AppendObject(autoSyncStateObj); + + HandleDownloadErrorFor(autoSyncStateObj, rv); + } // endif + } // endfor + + // remove folders with no pending messages from the priority queue + elemCount = foldersToBeRemoved.Count(); + for (int32_t idx = 0; idx < elemCount; idx++) { + nsCOMPtr<nsIAutoSyncState> autoSyncStateObj(foldersToBeRemoved[idx]); + if (!autoSyncStateObj) continue; + + nsCOMPtr<nsIMsgFolder> folder; + autoSyncStateObj->GetOwnerFolder(getter_AddRefs(folder)); + if (folder) NOTIFY_LISTENERS(OnDownloadCompleted, (folder)); + + if (MOZ_LOG_TEST(gAutoSyncLog, LogLevel::Debug)) { + nsCString folderName; + folder->GetURI(folderName); + MOZ_LOG(gAutoSyncLog, LogLevel::Debug, + ("%s: folder=%s has no pending msgs, " + "remove from priority q", + __func__, folderName.get())); + } + autoSyncStateObj->SetState(nsAutoSyncState::stCompletedIdle); + + if (mPriorityQ.RemoveObject(autoSyncStateObj)) + NOTIFY_LISTENERS(OnFolderRemovedFromQ, + (nsIAutoSyncMgrListener::PriorityQueue, folder)); + } + + return AutoUpdateFolders(); +} + +/** + * Updates offline imap folders that are not synchronized recently. This is + * called whenever we're idle. + */ +nsresult nsAutoSyncManager::AutoUpdateFolders() { + nsresult rv; + + // iterate through each imap account and update offline folders automatically + MOZ_LOG(gAutoSyncLog, LogLevel::Debug, ("enter %s", __func__)); + + nsCOMPtr<nsIMsgAccountManager> accountManager = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsTArray<RefPtr<nsIMsgAccount>> accounts; + rv = accountManager->GetAccounts(accounts); + NS_ENSURE_SUCCESS(rv, rv); + + for (auto account : accounts) { + if (!account) continue; + + nsCOMPtr<nsIMsgIncomingServer> incomingServer; + rv = account->GetIncomingServer(getter_AddRefs(incomingServer)); + if (!incomingServer) continue; + + nsCString type; + rv = incomingServer->GetType(type); + + if (!type.EqualsLiteral("imap")) continue; + + // If we haven't logged onto this server yet during this session or if the + // password has been removed from cache (see + // nsImapIncomingServer::ForgetSessionPassword) then skip autosync for + // this account. + bool notLoggedIn; + incomingServer->GetServerRequiresPasswordForBiff(¬LoggedIn); + if (notLoggedIn) { + if (MOZ_LOG_TEST(gAutoSyncLog, LogLevel::Debug)) { + nsCString serverName; + incomingServer->GetHostName(serverName); + MOZ_LOG(gAutoSyncLog, LogLevel::Debug, + ("%s: server |%s| don't autosync; not yet logged in", __func__, + serverName.get())); + } + continue; + } + + nsCOMPtr<nsIMsgFolder> rootFolder; + + rv = incomingServer->GetRootFolder(getter_AddRefs(rootFolder)); + if (rootFolder) { + if (NS_FAILED(rv)) continue; + + nsTArray<RefPtr<nsIMsgFolder>> allDescendants; + rv = rootFolder->GetDescendants(allDescendants); + + // Get the update time in minutes for each folder of this account/server. + // It will be the user configured biff time for server even if user has + // disabled "Check for new messages every X minutes" for the account. + // Update time will default to 10 minutes if an invalid value is set or + // if there are errors obtaining it. + // Specifically, the value used here is mail.server.serverX.check_time + // or the default mail.server.default.check_time. + int32_t updateMinutes = -1; + rv = incomingServer->GetBiffMinutes(&updateMinutes); + if (NS_FAILED(rv) || updateMinutes < 1) + updateMinutes = kDefaultUpdateInterval; + PRTime span = updateMinutes * (PR_USEC_PER_SEC * 60UL); + if (MOZ_LOG_TEST(gAutoSyncLog, LogLevel::Debug)) { + nsCString serverName; + incomingServer->GetHostName(serverName); + MOZ_LOG(gAutoSyncLog, LogLevel::Debug, + ("%s: Update time set to |%d| minutes for " + "folders in account |%s|", + __func__, updateMinutes, serverName.get())); + } + + for (auto folder : allDescendants) { + uint32_t folderFlags; + rv = folder->GetFlags(&folderFlags); + // Skip this folder if not offline or is a saved search or is no select. + if (NS_FAILED(rv) || !(folderFlags & nsMsgFolderFlags::Offline) || + folderFlags & + (nsMsgFolderFlags::Virtual | nsMsgFolderFlags::ImapNoselect)) + continue; + + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = + do_QueryInterface(folder, &rv); + if (NS_FAILED(rv)) continue; + + nsCOMPtr<nsIImapIncomingServer> imapServer; + rv = imapFolder->GetImapIncomingServer(getter_AddRefs(imapServer)); + if (imapServer) { + bool autoSyncOfflineStores = false; + rv = imapServer->GetAutoSyncOfflineStores(&autoSyncOfflineStores); + + // skip if AutoSyncOfflineStores pref is not set for this folder + if (NS_FAILED(rv) || !autoSyncOfflineStores) continue; + } + + nsCOMPtr<nsIAutoSyncState> autoSyncState; + rv = imapFolder->GetAutoSyncStateObj(getter_AddRefs(autoSyncState)); + NS_ASSERTION( + autoSyncState, + "*** nsAutoSyncState shouldn't be NULL, check owner folder"); + + // shouldn't happen but let's be defensive here + if (!autoSyncState) continue; + + int32_t state; + rv = autoSyncState->GetState(&state); + nsCString folderName; + if (MOZ_LOG_TEST(gAutoSyncLog, LogLevel::Debug)) { + folder->GetURI(folderName); + MOZ_LOG( + gAutoSyncLog, LogLevel::Debug, + ("%s: folder=%s, state=%d", __func__, folderName.get(), state)); + } + if (state == nsAutoSyncState::stCompletedIdle || + state == nsAutoSyncState::stUpdateNeeded || + state == nsAutoSyncState::stUpdateIssued) { + // Ensure that we wait for at least the "span" time set above before + // queuing an update of the same folder. + PRTime lastUpdateTime; + rv = autoSyncState->GetLastUpdateTime(&lastUpdateTime); + if (NS_SUCCEEDED(rv) && ((lastUpdateTime + span) < PR_Now())) { + int32_t idx = mUpdateQ.IndexOf(autoSyncState); + if (state == nsAutoSyncState::stUpdateIssued) { + // Handle the case where an update is triggered but nothing is + // found to download. This can happen after messages are copied + // or moved between offline folders of the same server or if imap + // "folderstatus" URL triggers an update but no new messages + // are detected. + bool downloadQEmpty; + autoSyncState->IsDownloadQEmpty(&downloadQEmpty); + if (downloadQEmpty) { + MOZ_LOG(gAutoSyncLog, LogLevel::Debug, + ("%s: nothing to download for folder %s, " + "set state to stCompletedIdle, updateQ idx=%d", + __func__, folderName.get(), idx)); + autoSyncState->SetState(nsAutoSyncState::stCompletedIdle); + + // This should already be done by + // nsAutoSyncManager::OnStopRunningUrl() but set update state to + // completed and remove folder state object from update queue in + // case OnStopRunningUrl never occurred. + mUpdateInProgress = false; + if (idx > -1) { + mUpdateQ.RemoveObjectAt(idx); + idx = -1; // re-q below + } + } else { + MOZ_LOG(gAutoSyncLog, LogLevel::Debug, + ("%s: downloadQ not empty. Why? updateQ idx=%d", + __func__, idx)); + if (idx > -1) { + // Download q not empty and folder still on update q, maybe it + // just needs more time so leave update q as it is to update + // on next "span" interval. (Never seen this happen.) + idx = 0; + } + } + } + // Now q or re-q the update for this folder unless it's still q'd. + if (idx < 0) { + mUpdateQ.AppendObject(autoSyncState); + MOZ_LOG(gAutoSyncLog, LogLevel::Debug, + ("%s: folder=%s added to update q", __func__, + folderName.get())); + if (folder) + NOTIFY_LISTENERS(OnFolderAddedIntoQ, + (nsIAutoSyncMgrListener::UpdateQueue, folder)); + } + } + } + + // Check if time to add folder to discovery q on kAutoSyncFreq (1 hour) + // time base. + PRTime lastSyncTime; + rv = autoSyncState->GetLastSyncTime(&lastSyncTime); + if (NS_SUCCEEDED(rv) && ((lastSyncTime + kAutoSyncFreq) < PR_Now())) { + // add this folder into discovery queue to process existing headers + // and discover messages not downloaded yet + if (mDiscoveryQ.IndexOf(autoSyncState) == -1) { + mDiscoveryQ.AppendObject(autoSyncState); + MOZ_LOG(gAutoSyncLog, LogLevel::Debug, + ("%s: folder=%s added to discovery q", __func__, + folderName.get())); + if (folder) + NOTIFY_LISTENERS( + OnFolderAddedIntoQ, + (nsIAutoSyncMgrListener::DiscoveryQueue, folder)); + } + } + } // endfor + } // endif + } // endfor + + // lazily create the timer if there is something to process in the queue + // when timer is done, it will self destruct + StartTimerIfNeeded(); + + return rv; +} + +/** + * Places the given folder into the priority queue based on active + * strategy function. + */ +void nsAutoSyncManager::ScheduleFolderForOfflineDownload( + nsIAutoSyncState* aAutoSyncStateObj) { + if (aAutoSyncStateObj && (mPriorityQ.IndexOf(aAutoSyncStateObj) == -1)) { + nsCOMPtr<nsIAutoSyncFolderStrategy> folStrategy; + GetFolderStrategy(getter_AddRefs(folStrategy)); + + if (mPriorityQ.Count() <= 0) { + // make sure that we don't insert a folder excluded by the given strategy + nsCOMPtr<nsIMsgFolder> folder; + aAutoSyncStateObj->GetOwnerFolder(getter_AddRefs(folder)); + if (folder) { + bool excluded = false; + if (folStrategy) folStrategy->IsExcluded(folder, &excluded); + + if (!excluded) { + mPriorityQ.AppendObject( + aAutoSyncStateObj); // insert into the first spot + NOTIFY_LISTENERS(OnFolderAddedIntoQ, + (nsIAutoSyncMgrListener::PriorityQueue, folder)); + } + } + } else { + // find the right spot for the given folder + uint32_t qidx = mPriorityQ.Count(); + while (qidx > 0) { + --qidx; + + nsCOMPtr<nsIMsgFolder> folderA, folderB; + mPriorityQ[qidx]->GetOwnerFolder(getter_AddRefs(folderA)); + aAutoSyncStateObj->GetOwnerFolder(getter_AddRefs(folderB)); + + bool excluded = false; + if (folderB && folStrategy) folStrategy->IsExcluded(folderB, &excluded); + + if (excluded) break; + + nsAutoSyncStrategyDecisionType decision = + nsAutoSyncStrategyDecisions::Same; + if (folderA && folderB && folStrategy) + folStrategy->Sort(folderA, folderB, &decision); + + if (decision == nsAutoSyncStrategyDecisions::Higher && 0 == qidx) + mPriorityQ.InsertObjectAt(aAutoSyncStateObj, 0); + else if (decision == nsAutoSyncStrategyDecisions::Higher) + continue; + else if (decision == nsAutoSyncStrategyDecisions::Lower) + mPriorityQ.InsertObjectAt(aAutoSyncStateObj, qidx + 1); + else // decision == nsAutoSyncStrategyDecisions::Same + mPriorityQ.InsertObjectAt(aAutoSyncStateObj, qidx); + + NOTIFY_LISTENERS(OnFolderAddedIntoQ, + (nsIAutoSyncMgrListener::PriorityQueue, folderB)); + break; + } // end while + } + } // endif +} + +/** + * Zero aSizeLimit means no limit + */ +nsresult nsAutoSyncManager::DownloadMessagesForOffline( + nsIAutoSyncState* aAutoSyncStateObj, uint32_t aSizeLimit) { + if (!aAutoSyncStateObj) return NS_ERROR_INVALID_ARG; + + int32_t count; + nsresult rv = aAutoSyncStateObj->GetPendingMessageCount(&count); + NS_ENSURE_SUCCESS(rv, rv); + + // special case: no more message to download for this folder: + // see HandleDownloadErrorFor for recovery policy + if (!count) return NS_ERROR_NOT_AVAILABLE; + + nsTArray<RefPtr<nsIMsgDBHdr>> messagesToDownload; + uint32_t totalSize = 0; + rv = aAutoSyncStateObj->GetNextGroupOfMessages(mGroupSize, &totalSize, + messagesToDownload); + NS_ENSURE_SUCCESS(rv, rv); + + // there are pending messages but the cumulative size is zero: + // treat as special case. + // Note that although it shouldn't happen, we know that sometimes + // imap servers manifest messages as zero length. By returning + // NS_ERROR_NOT_AVAILABLE we cause this folder to be removed from + // the priority queue temporarily (until the next idle or next update) + // in an effort to prevent it blocking other folders of the same account + // being synced. + if (!totalSize) return NS_ERROR_NOT_AVAILABLE; + + // ensure that we don't exceed the given size limit for this particular group + if (aSizeLimit && aSizeLimit < totalSize) return NS_ERROR_FAILURE; + + if (!messagesToDownload.IsEmpty()) { + rv = aAutoSyncStateObj->DownloadMessagesForOffline(messagesToDownload); + + int32_t totalCount; + (void)aAutoSyncStateObj->GetTotalMessageCount(&totalCount); + + nsCOMPtr<nsIMsgFolder> folder; + aAutoSyncStateObj->GetOwnerFolder(getter_AddRefs(folder)); + if (NS_SUCCEEDED(rv) && folder) + NOTIFY_LISTENERS(OnDownloadStarted, + (folder, messagesToDownload.Length(), totalCount)); + } + + return rv; +} + +// clang-format off +/** + * Assuming that the download operation on the given folder has been failed at + * least once, execute these steps: + * - put the auto-sync state into ready-to-download mode + * - rollback the message offset so we can try the same group again (unless the + * retry count is reached to the given limit) + * - if parallel model is active, wait to be resumed by the next idle + * - if chained model is active, search the priority queue to find a sibling to + * continue with. + */ +// clang-format on +nsresult nsAutoSyncManager::HandleDownloadErrorFor( + nsIAutoSyncState* aAutoSyncStateObj, const nsresult error) { + if (!aAutoSyncStateObj) return NS_ERROR_INVALID_ARG; + + // ensure that an error occurred + if (NS_SUCCEEDED(error)) return NS_OK; + + // NS_ERROR_NOT_AVAILABLE is a special case/error happens when the queued + // folder doesn't have any message to download (see bug 457342). In such case + // we shouldn't retry the current message group, nor notify listeners. Simply + // continuing with the next sibling in the priority queue would suffice. + + if (NS_ERROR_NOT_AVAILABLE != error) { + // force the auto-sync state to try downloading the same group at least + // kGroupRetryCount times before it moves to the next one + aAutoSyncStateObj->TryCurrentGroupAgain(kGroupRetryCount); + + nsCOMPtr<nsIMsgFolder> folder; + aAutoSyncStateObj->GetOwnerFolder(getter_AddRefs(folder)); + if (folder) NOTIFY_LISTENERS(OnDownloadError, (folder)); + } + + // if parallel model, don't do anything else + + if (mDownloadModel == dmChained) { + // switch to the next folder in the chain and continue downloading + nsIAutoSyncState* autoSyncStateObj = aAutoSyncStateObj; + nsIAutoSyncState* nextAutoSyncStateObj = nullptr; + while ( + (nextAutoSyncStateObj = GetNextSibling(mPriorityQ, autoSyncStateObj))) { + autoSyncStateObj = nextAutoSyncStateObj; + nsresult rv = DownloadMessagesForOffline(autoSyncStateObj); + if (NS_SUCCEEDED(rv)) break; + if (rv == NS_ERROR_NOT_AVAILABLE) + // next folder in the chain also doesn't have any message to download + // switch to next one if any + continue; + autoSyncStateObj->TryCurrentGroupAgain(kGroupRetryCount); + } + } + + return NS_OK; +} + +NS_IMETHODIMP nsAutoSyncManager::GetGroupSize(uint32_t* aGroupSize) { + NS_ENSURE_ARG_POINTER(aGroupSize); + *aGroupSize = mGroupSize; + return NS_OK; +} +NS_IMETHODIMP nsAutoSyncManager::SetGroupSize(uint32_t aGroupSize) { + mGroupSize = aGroupSize ? aGroupSize : kDefaultGroupSize; + return NS_OK; +} + +NS_IMETHODIMP nsAutoSyncManager::GetMsgStrategy( + nsIAutoSyncMsgStrategy** aMsgStrategy) { + NS_ENSURE_ARG_POINTER(aMsgStrategy); + + // lazily create if it is not done already + if (!mMsgStrategyImpl) { + mMsgStrategyImpl = new nsDefaultAutoSyncMsgStrategy; + if (!mMsgStrategyImpl) return NS_ERROR_OUT_OF_MEMORY; + } + + NS_IF_ADDREF(*aMsgStrategy = mMsgStrategyImpl); + return NS_OK; +} +NS_IMETHODIMP nsAutoSyncManager::SetMsgStrategy( + nsIAutoSyncMsgStrategy* aMsgStrategy) { + mMsgStrategyImpl = aMsgStrategy; + return NS_OK; +} + +NS_IMETHODIMP nsAutoSyncManager::GetFolderStrategy( + nsIAutoSyncFolderStrategy** aFolderStrategy) { + NS_ENSURE_ARG_POINTER(aFolderStrategy); + + // lazily create if it is not done already + if (!mFolderStrategyImpl) { + mFolderStrategyImpl = new nsDefaultAutoSyncFolderStrategy; + if (!mFolderStrategyImpl) return NS_ERROR_OUT_OF_MEMORY; + } + + NS_IF_ADDREF(*aFolderStrategy = mFolderStrategyImpl); + return NS_OK; +} +NS_IMETHODIMP nsAutoSyncManager::SetFolderStrategy( + nsIAutoSyncFolderStrategy* aFolderStrategy) { + mFolderStrategyImpl = aFolderStrategy; + return NS_OK; +} + +NS_IMETHODIMP +nsAutoSyncManager::DoesMsgFitDownloadCriteria(nsIMsgDBHdr* aMsgHdr, + bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + uint32_t msgFlags = 0; + aMsgHdr->GetFlags(&msgFlags); + + // check whether this message is marked imap deleted or not + *aResult = !(msgFlags & nsMsgMessageFlags::IMAPDeleted); + if (!(*aResult)) return NS_OK; + + bool shouldStoreMsgOffline = true; + nsCOMPtr<nsIMsgFolder> folder; + aMsgHdr->GetFolder(getter_AddRefs(folder)); + if (folder) { + nsMsgKey msgKey; + nsresult rv = aMsgHdr->GetMessageKey(&msgKey); + // a cheap way to get the size limit for this folder and make + // sure that we don't have this message offline already + if (NS_SUCCEEDED(rv)) + folder->ShouldStoreMsgOffline(msgKey, &shouldStoreMsgOffline); + } + + *aResult &= shouldStoreMsgOffline; + + return NS_OK; +} + +NS_IMETHODIMP nsAutoSyncManager::OnDownloadQChanged( + nsIAutoSyncState* aAutoSyncStateObj) { + nsCOMPtr<nsIAutoSyncState> autoSyncStateObj(aAutoSyncStateObj); + if (!autoSyncStateObj) return NS_ERROR_INVALID_ARG; + + if (mPaused) return NS_OK; + // We want to start downloading immediately unless the folder is excluded. + bool excluded = false; + nsCOMPtr<nsIAutoSyncFolderStrategy> folStrategy; + nsCOMPtr<nsIMsgFolder> folder; + + GetFolderStrategy(getter_AddRefs(folStrategy)); + autoSyncStateObj->GetOwnerFolder(getter_AddRefs(folder)); + + if (folder && folStrategy) folStrategy->IsExcluded(folder, &excluded); + + nsresult rv = NS_OK; + + if (!excluded) { + // Add this folder into the priority queue. + autoSyncStateObj->SetState(nsAutoSyncState::stReadyToDownload); + ScheduleFolderForOfflineDownload(autoSyncStateObj); + + // If we operate in parallel mode or if there is no sibling downloading + // messages at the moment, we can download the first group of the messages + // for this folder + if (mDownloadModel == dmParallel || + !DoesQContainAnySiblingOf(mPriorityQ, autoSyncStateObj, + nsAutoSyncState::stDownloadInProgress)) { + // this will download the first group of messages immediately; + // to ensure that we don't end up downloading a large single message in + // not-idle time, we enforce a limit. If there is no message fits into + // this limit we postpone the download until the next idle. + if (GetIdleState() == notIdle) + rv = DownloadMessagesForOffline(autoSyncStateObj, kFirstGroupSizeLimit); + else + rv = DownloadMessagesForOffline(autoSyncStateObj); + + if (NS_FAILED(rv)) + autoSyncStateObj->TryCurrentGroupAgain(kGroupRetryCount); + } + } + return rv; +} + +NS_IMETHODIMP +nsAutoSyncManager::OnDownloadStarted(nsIAutoSyncState* aAutoSyncStateObj, + nsresult aStartCode) { + nsCOMPtr<nsIAutoSyncState> autoSyncStateObj(aAutoSyncStateObj); + if (!autoSyncStateObj) return NS_ERROR_INVALID_ARG; + + // resume downloads during next idle time + if (NS_FAILED(aStartCode)) + autoSyncStateObj->SetState(nsAutoSyncState::stReadyToDownload); + + return aStartCode; +} + +NS_IMETHODIMP +nsAutoSyncManager::OnDownloadCompleted(nsIAutoSyncState* aAutoSyncStateObj, + nsresult aExitCode) { + nsCOMPtr<nsIAutoSyncState> autoSyncStateObj(aAutoSyncStateObj); + if (!autoSyncStateObj) return NS_ERROR_INVALID_ARG; + + nsresult rv = aExitCode; + + if (NS_FAILED(aExitCode)) { + // retry the same group kGroupRetryCount times + // try again if TB still idle, otherwise wait for the next idle time + autoSyncStateObj->TryCurrentGroupAgain(kGroupRetryCount); + if (GetIdleState() != notIdle) { + rv = DownloadMessagesForOffline(autoSyncStateObj); + if (NS_FAILED(rv)) rv = HandleDownloadErrorFor(autoSyncStateObj, rv); + } + return rv; + } + + // download is successful, reset the retry counter of the folder + autoSyncStateObj->ResetRetryCounter(); + + nsCOMPtr<nsIMsgFolder> folder; + aAutoSyncStateObj->GetOwnerFolder(getter_AddRefs(folder)); + if (folder) NOTIFY_LISTENERS(OnDownloadCompleted, (folder)); + + int32_t count; + rv = autoSyncStateObj->GetPendingMessageCount(&count); + NS_ENSURE_SUCCESS(rv, rv); + + nsIAutoSyncState* nextFolderToDownload = nullptr; + if (count > 0) { + autoSyncStateObj->SetState(nsAutoSyncState::stReadyToDownload); + + // in parallel model, we continue downloading the same folder as long as it + // has more pending messages + nextFolderToDownload = autoSyncStateObj; + + // in chained model, ensure that we are always downloading the highest + // priority folder first + if (mDownloadModel == dmChained) { + // switch to higher priority folder and continue to download, + // if any added recently + int32_t myIndex = mPriorityQ.IndexOf(autoSyncStateObj); + + int32_t siblingIndex; + nsIAutoSyncState* sibling = + GetHighestPrioSibling(mPriorityQ, autoSyncStateObj, &siblingIndex); + + // lesser index = higher priority + if (sibling && myIndex > -1 && siblingIndex < myIndex) + nextFolderToDownload = sibling; + } + } else { + autoSyncStateObj->SetState(nsAutoSyncState::stCompletedIdle); + + nsCOMPtr<nsIMsgFolder> folder; + nsresult rv = autoSyncStateObj->GetOwnerFolder(getter_AddRefs(folder)); + + if (NS_SUCCEEDED(rv) && mPriorityQ.RemoveObject(autoSyncStateObj)) + NOTIFY_LISTENERS(OnFolderRemovedFromQ, + (nsIAutoSyncMgrListener::PriorityQueue, folder)); + + // find the next folder owned by the same server in the queue and continue + // downloading + if (mDownloadModel == dmChained) + nextFolderToDownload = + GetHighestPrioSibling(mPriorityQ, autoSyncStateObj); + + } // endif + + // continue downloading if TB is still in idle state + if (nextFolderToDownload && GetIdleState() != notIdle) { + rv = DownloadMessagesForOffline(nextFolderToDownload); + if (NS_FAILED(rv)) rv = HandleDownloadErrorFor(nextFolderToDownload, rv); + } + + return rv; +} + +NS_IMETHODIMP nsAutoSyncManager::GetDownloadModel(int32_t* aDownloadModel) { + NS_ENSURE_ARG_POINTER(aDownloadModel); + *aDownloadModel = mDownloadModel; + return NS_OK; +} +NS_IMETHODIMP nsAutoSyncManager::SetDownloadModel(int32_t aDownloadModel) { + mDownloadModel = aDownloadModel; + return NS_OK; +} + +NS_IMETHODIMP nsAutoSyncManager::AddListener( + nsIAutoSyncMgrListener* aListener) { + NS_ENSURE_ARG_POINTER(aListener); + mListeners.AppendElementUnlessExists(aListener); + return NS_OK; +} + +NS_IMETHODIMP nsAutoSyncManager::RemoveListener( + nsIAutoSyncMgrListener* aListener) { + NS_ENSURE_ARG_POINTER(aListener); + mListeners.RemoveElement(aListener); + return NS_OK; +} + +/* readonly attribute unsigned long discoveryQLength; */ +NS_IMETHODIMP nsAutoSyncManager::GetDiscoveryQLength( + uint32_t* aDiscoveryQLength) { + NS_ENSURE_ARG_POINTER(aDiscoveryQLength); + *aDiscoveryQLength = mDiscoveryQ.Count(); + return NS_OK; +} + +/* readonly attribute unsigned long uploadQLength; */ +NS_IMETHODIMP nsAutoSyncManager::GetUpdateQLength(uint32_t* aUpdateQLength) { + NS_ENSURE_ARG_POINTER(aUpdateQLength); + *aUpdateQLength = mUpdateQ.Count(); + return NS_OK; +} + +/* readonly attribute unsigned long downloadQLength; */ +NS_IMETHODIMP nsAutoSyncManager::GetDownloadQLength( + uint32_t* aDownloadQLength) { + NS_ENSURE_ARG_POINTER(aDownloadQLength); + *aDownloadQLength = mPriorityQ.Count(); + return NS_OK; +} + +NS_IMETHODIMP +nsAutoSyncManager::OnFolderHasPendingMsgs(nsIAutoSyncState* aAutoSyncStateObj) { + NS_ENSURE_ARG_POINTER(aAutoSyncStateObj); + if (mUpdateQ.IndexOf(aAutoSyncStateObj) == -1) { + nsCOMPtr<nsIMsgFolder> folder; + aAutoSyncStateObj->GetOwnerFolder(getter_AddRefs(folder)); + // If this folder isn't the trash, add it to the update q. + if (folder) { + bool isTrash; + folder->GetFlag(nsMsgFolderFlags::Trash, &isTrash); + if (!isTrash) { + bool isSentOrArchive; + folder->IsSpecialFolder( + nsMsgFolderFlags::SentMail | nsMsgFolderFlags::Archive, true, + &isSentOrArchive); + // Sent or archive folders go to the q front, the rest to the end. + if (isSentOrArchive) + mUpdateQ.InsertObjectAt(aAutoSyncStateObj, 0); + else + mUpdateQ.AppendObject(aAutoSyncStateObj); + aAutoSyncStateObj->SetState(nsAutoSyncState::stUpdateNeeded); + NOTIFY_LISTENERS(OnFolderAddedIntoQ, + (nsIAutoSyncMgrListener::UpdateQueue, folder)); + } + } + } + return NS_OK; +} + +void nsAutoSyncManager::SetIdleState(IdleState st) { mIdleState = st; } + +nsAutoSyncManager::IdleState nsAutoSyncManager::GetIdleState() const { + return mIdleState; +} + +NS_IMPL_ISUPPORTS(nsAutoSyncManager, nsIObserver, nsIUrlListener, + nsIAutoSyncManager) diff --git a/comm/mailnews/imap/src/nsAutoSyncManager.h b/comm/mailnews/imap/src/nsAutoSyncManager.h new file mode 100644 index 0000000000..fb77b66a77 --- /dev/null +++ b/comm/mailnews/imap/src/nsAutoSyncManager.h @@ -0,0 +1,265 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsAutoSyncManager_h__ +# define nsAutoSyncManager_h__ + +# include "nsString.h" +# include "nsCOMArray.h" +# include "nsIObserver.h" +# include "nsIUrlListener.h" +# include "nsITimer.h" +# include "nsTObserverArray.h" +# include "nsIAutoSyncManager.h" +# include "nsIAutoSyncMsgStrategy.h" +# include "nsIAutoSyncFolderStrategy.h" +# include "nsIUserIdleService.h" +# include "prtime.h" + +// clang-format off +/* Auto-Sync + * + * Background: + * it works only with offline imap folders. "autosync_offline_stores" pref + * enables/disables auto-sync mechanism. Note that setting "autosync_offline_stores" + * to false, or setting folder to not-offline doesn't stop synchronization + * process for already queued folders. + * + * Auto-Sync policy: + * o It kicks in during system idle time, and tries to download as much messages + * as possible based on given folder and message prioritization strategies/rules. + * Default folder prioritization strategy dictates to sort the folders based on the + * following order: INBOX > DRAFTS > SUBFOLDERS > TRASH. + * Similarly, default message prioritization strategy dictates to download the most + * recent and smallest message first. Also, by sorting the messages by size in the + * queue, it tries to maximize the number of messages downloaded. + * o It downloads the messages in groups. Default groups size is defined by |kDefaultGroupSize|. + * o It downloads the messages larger than the group size one-by-one. + * o If new messages arrive when not idle, it downloads the messages that do fit into + * |kFirstGroupSizeLimit| size limit immediately, without waiting for idle time, unless there is + * a sibling (a folder owned by the same imap server) in stDownloadInProgress state in the q + * o If new messages arrive when idle, it downloads all the messages without any restriction. + * o If new messages arrive into a folder while auto-sync is downloading other messages of the + * same folder, it simply puts the new messages into the folder's download queue, and + * re-prioritize the messages. That behavior makes sure that the high priority + * (defined by the message strategy) get downloaded first always. + * o If new messages arrive into a folder while auto-sync is downloading messages of a lower + * priority folder, auto-sync switches the folders in the queue and starts downloading the + * messages of the higher priority folder next time it downloads a message group. + * o Currently there is no way to stop/pause/cancel a message download. The smallest + * granularity is the message group size. + * o Auto-Sync manager periodically (kAutoSyncFreq) checks folder for existing messages + * w/o bodies. It persists the last time the folder is checked in the local database of the + * folder. We call this process 'Discovery'. This process is asynchronous and processes + * |kNumberOfHeadersToProcess| number of headers at each cycle. Since it works on local data, + * it doesn't consume lots of system resources, it does its job fast. + * o Discovery is necessary especially when the user makes a transition from not-offline + * to offline mode. + * o Update frequency is defined by nsMsgIncomingServer::BiffMinutes. + * + * Error Handling: + * o if the user moves/deletes/filters all messages of a folder already queued, auto-sync + * deals with that situation by skipping the folder in question, and continuing with the + * next in chain. + * o If the message size is zero, auto-sync ignores the message. + * o If the download of the message group fails for some reason, auto-sync tries to + * download the same group |kGroupRetryCount| times. If it still fails, continues with the + * next group of messages. + * + * Download Model: + * Parallel model should be used with the imap servers that do not have any "max number of sessions + * per IP" limit, and when the bandwidth is significantly large. + * + * How it really works: + * The AutoSyncManager gets an idle notification. First it processes any + * folders in the discovery queue (which means it schedules message download + * for any messages it previously determined it should download). Then it sets + * a timer, and in the timer callback, it processes the update q, by calling + * InitiateAutoSync on the first folder in the update q. + * + * See additional info near the bottom of this file. + */ +// clang-format on + +/** + * Default strategy implementation to prioritize messages in the download queue. + */ +class nsDefaultAutoSyncMsgStrategy final : public nsIAutoSyncMsgStrategy { + static const uint32_t kFirstPassMessageSize = 60U * 1024U; // 60K + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIAUTOSYNCMSGSTRATEGY + + nsDefaultAutoSyncMsgStrategy(); + + private: + ~nsDefaultAutoSyncMsgStrategy(); +}; + +/** + * Default strategy implementation to prioritize folders in the download queue. + */ +class nsDefaultAutoSyncFolderStrategy final : public nsIAutoSyncFolderStrategy { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIAUTOSYNCFOLDERSTRATEGY + + nsDefaultAutoSyncFolderStrategy(); + + private: + ~nsDefaultAutoSyncFolderStrategy(); +}; + +// see the end of the page for auto-sync internals + +/** + * Manages background message download operations for offline imap folders. + */ +class nsAutoSyncManager final : public nsIObserver, + public nsIUrlListener, + public nsIAutoSyncManager { + static const PRTime kAutoSyncFreq = 60UL * (PR_USEC_PER_SEC * 60UL); // 1hr + static const uint32_t kDefaultUpdateInterval = 10UL; // 10min + static const int32_t kTimerIntervalInMs = 400; + static const uint32_t kNumberOfHeadersToProcess = 250U; + // enforced size of the first group that will be downloaded before idle time + static const uint32_t kFirstGroupSizeLimit = 60U * 1024U /* 60K */; + static const int32_t kIdleTimeInSec = 10; + static const uint32_t kGroupRetryCount = 3; + + enum IdleState { systemIdle, appIdle, notIdle }; + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + NS_DECL_NSIURLLISTENER + NS_DECL_NSIAUTOSYNCMANAGER + + nsAutoSyncManager(); + + private: + ~nsAutoSyncManager(); + + void SetIdleState(IdleState st); + IdleState GetIdleState() const; + nsresult StartIdleProcessing(); + nsresult AutoUpdateFolders(); + void ScheduleFolderForOfflineDownload(nsIAutoSyncState* aAutoSyncStateObj); + nsresult DownloadMessagesForOffline(nsIAutoSyncState* aAutoSyncStateObj, + uint32_t aSizeLimit = 0); + nsresult HandleDownloadErrorFor(nsIAutoSyncState* aAutoSyncStateObj, + const nsresult error); + + // Helper methods for priority Q operations + static void ChainFoldersInQ(const nsCOMArray<nsIAutoSyncState>& aQueue, + nsCOMArray<nsIAutoSyncState>& aChainedQ); + static nsIAutoSyncState* SearchQForSibling( + const nsCOMArray<nsIAutoSyncState>& aQueue, + nsIAutoSyncState* aAutoSyncStateObj, int32_t aStartIdx, + int32_t* aIndex = nullptr); + static bool DoesQContainAnySiblingOf( + const nsCOMArray<nsIAutoSyncState>& aQueue, + nsIAutoSyncState* aAutoSyncStateObj, const int32_t aState, + int32_t* aIndex = nullptr); + static nsIAutoSyncState* GetNextSibling( + const nsCOMArray<nsIAutoSyncState>& aQueue, + nsIAutoSyncState* aAutoSyncStateObj, int32_t* aIndex = nullptr); + static nsIAutoSyncState* GetHighestPrioSibling( + const nsCOMArray<nsIAutoSyncState>& aQueue, + nsIAutoSyncState* aAutoSyncStateObj, int32_t* aIndex = nullptr); + + /// timer to process existing keys and updates + void InitTimer(); + static void TimerCallback(nsITimer* aTimer, void* aClosure); + void StopTimer(); + void StartTimerIfNeeded(); + + protected: + nsCOMPtr<nsIAutoSyncMsgStrategy> mMsgStrategyImpl; + nsCOMPtr<nsIAutoSyncFolderStrategy> mFolderStrategyImpl; + // contains the folders that will be downloaded on background + nsCOMArray<nsIAutoSyncState> mPriorityQ; + // contains the folders that will be examined for existing headers and + // adds the headers we don't have offline into the autosyncState + // object's download queue. + nsCOMArray<nsIAutoSyncState> mDiscoveryQ; + // contains the folders that will be checked for new messages with STATUS, + // and if there are any, we'll call UpdateFolder on them. + nsCOMArray<nsIAutoSyncState> mUpdateQ; + // this is set true when autosync is initiated for a single folder. Its + // purpose is ensure that update for a folder finishes before the next one + // starts. + bool mUpdateInProgress; + + // This is set if auto sync has been completely paused. + bool mPaused; + // This is set if we've finished startup and should start + // paying attention to idle notifications. + bool mStartupDone; + + private: + uint32_t mGroupSize; + IdleState mIdleState; + int32_t mDownloadModel; + nsCOMPtr<nsIUserIdleService> mIdleService; + nsCOMPtr<nsITimer> mTimer; + nsTObserverArray<nsCOMPtr<nsIAutoSyncMgrListener> > mListeners; +}; + +#endif + +/* +How queues inter-relate: + +nsAutoSyncState has an internal priority queue to store messages waiting to be +downloaded. nsAutoSyncMsgStrategy object determines the order in this queue, +nsAutoSyncManager uses this queue to manage downloads. Two events cause a +change in this queue: + +1) nsImapMailFolder::HeaderFetchCompleted: is triggered when TB notices that +there are pending messages on the server -- via IDLE command from the server, +via explicit select from the user, or via automatic Update during idle time. If +it turns out that there are pending messages on the server, it adds them into +nsAutoSyncState's download queue. + +2) nsAutoSyncState::ProcessExistingHeaders: is triggered for every imap folder +every hour or so (see kAutoSyncFreq). nsAutoSyncManager uses an internal queue +called Discovery queue to keep track of this task. The purpose of +ProcessExistingHeaders() method is to check existing headers of a given folder +in batches and discover the messages without bodies, in asynchronous fashion. +This process is sequential, one and only one folder at any given time, very +similar to indexing. Again, if it turns out that the folder in hand has messages +w/o bodies, ProcessExistingHeaders adds them into nsAutoSyncState's download +queue. + +Any change in nsAutoSyncState's download queue, notifies nsAutoSyncManager and +nsAutoSyncManager puts the requesting nsAutoSyncState into its internal +priority queue (called mPriorityQ) -- if the folder is not already there. +nsAutoSyncFolderStrategy object determines the order in this queue. This queue +is processed in two modes: chained and parallel. + +i) Chained: One folder per imap server any given time. Folders owned by +different imap servers are simultaneous. + +ii) Parallel: All folders at the same time, using all cached-connections - +a.k.a 'Folders gone wild' mode. + +Note: The "Chained" mode is currently in use: mDownloadModel = dmChained; + +The order the folders are added into the mPriorityQ doesn't matter since every +time a batch completed for an imap server, nsAutoSyncManager adjusts the order. +So, lets say that updating a sub-folder starts downloading message immediately, +when an higher priority folder is added into the queue, nsAutoSyncManager +switches to this higher priority folder instead of processing the next group of +messages of the lower priority one. Setting group size too high might delay +this switch at worst. + +And finally, Update queue helps nsAutoSyncManager to keep track of folders +waiting to be updated. With the latest change, we update one and only one +folder at any given time. Default frequency of updating is 10 min +(kDefaultUpdateInterval). We add folders into the update queue during idle time, +if they are not in mPriorityQ already. + +*/ diff --git a/comm/mailnews/imap/src/nsAutoSyncState.cpp b/comm/mailnews/imap/src/nsAutoSyncState.cpp new file mode 100644 index 0000000000..db3216419a --- /dev/null +++ b/comm/mailnews/imap/src/nsAutoSyncState.cpp @@ -0,0 +1,782 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsAutoSyncState.h" +#include "nsImapMailFolder.h" +#include "nsIImapService.h" +#include "nsIMsgMailNewsUrl.h" +#include "nsIMsgMailSession.h" +#include "nsMsgFolderFlags.h" +#include "nsIAutoSyncManager.h" +#include "nsIAutoSyncMsgStrategy.h" +#include "nsServiceManagerUtils.h" +#include "nsComponentManagerUtils.h" +#include "mozilla/Logging.h" + +using namespace mozilla; + +extern LazyLogModule gAutoSyncLog; // defined in nsAutoSyncManager.cpp + +MsgStrategyComparatorAdaptor::MsgStrategyComparatorAdaptor( + nsIAutoSyncMsgStrategy* aStrategy, nsIMsgFolder* aFolder, + nsIMsgDatabase* aDatabase) + : mStrategy(aStrategy), mFolder(aFolder), mDatabase(aDatabase) {} + +/** @return True if the elements are equals; false otherwise. */ +bool MsgStrategyComparatorAdaptor::Equals(const nsMsgKey& a, + const nsMsgKey& b) const { + nsCOMPtr<nsIMsgDBHdr> hdrA; + nsCOMPtr<nsIMsgDBHdr> hdrB; + + mDatabase->GetMsgHdrForKey(a, getter_AddRefs(hdrA)); + mDatabase->GetMsgHdrForKey(b, getter_AddRefs(hdrB)); + + if (hdrA && hdrB) { + nsresult rv = NS_OK; + nsAutoSyncStrategyDecisionType decision = nsAutoSyncStrategyDecisions::Same; + + if (mStrategy) rv = mStrategy->Sort(mFolder, hdrA, hdrB, &decision); + + if (NS_SUCCEEDED(rv)) + return (decision == nsAutoSyncStrategyDecisions::Same); + } + + return false; +} + +/** @return True if (a < b); false otherwise. */ +bool MsgStrategyComparatorAdaptor::LessThan(const nsMsgKey& a, + const nsMsgKey& b) const { + nsCOMPtr<nsIMsgDBHdr> hdrA; + nsCOMPtr<nsIMsgDBHdr> hdrB; + + mDatabase->GetMsgHdrForKey(a, getter_AddRefs(hdrA)); + mDatabase->GetMsgHdrForKey(b, getter_AddRefs(hdrB)); + + if (hdrA && hdrB) { + nsresult rv = NS_OK; + nsAutoSyncStrategyDecisionType decision = nsAutoSyncStrategyDecisions::Same; + + if (mStrategy) rv = mStrategy->Sort(mFolder, hdrA, hdrB, &decision); + + if (NS_SUCCEEDED(rv)) + return (decision == nsAutoSyncStrategyDecisions::Lower); + } + + return false; +} + +nsAutoSyncState::nsAutoSyncState(nsImapMailFolder* aOwnerFolder, + PRTime aLastSyncTime) + : mSyncState(stCompletedIdle), + mOffset(0U), + mLastOffset(0U), + mLastServerTotal(0), + mLastServerRecent(0), + mLastServerUnseen(0), + mLastNextUID(0), + mLastSyncTime(aLastSyncTime), + mLastUpdateTime(0UL), + mProcessPointer(0U), + mIsDownloadQChanged(false), + mRetryCounter(0U) { + mOwnerFolder = + do_GetWeakReference(static_cast<nsIMsgImapMailFolder*>(aOwnerFolder)); + mHaveAStatusResponse = false; +} + +nsAutoSyncState::~nsAutoSyncState() {} + +// TODO:XXXemre should be implemented when we start +// doing space management +nsresult nsAutoSyncState::ManageStorageSpace() { return NS_OK; } + +nsresult nsAutoSyncState::PlaceIntoDownloadQ( + const nsTArray<nsMsgKey>& aMsgKeyList) { + nsresult rv = NS_OK; + if (!aMsgKeyList.IsEmpty()) { + nsCOMPtr<nsIMsgFolder> folder = do_QueryReferent(mOwnerFolder, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgDatabase> database; + rv = folder->GetMsgDatabase(getter_AddRefs(database)); + if (!database) return NS_ERROR_FAILURE; + + nsCOMPtr<nsIAutoSyncManager> autoSyncMgr = + do_GetService(NS_AUTOSYNCMANAGER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIAutoSyncMsgStrategy> msgStrategy; + autoSyncMgr->GetMsgStrategy(getter_AddRefs(msgStrategy)); + + // increase the array size + mDownloadQ.SetCapacity(mDownloadQ.Length() + aMsgKeyList.Length()); + + // remove excluded messages + int32_t elemCount = aMsgKeyList.Length(); + for (int32_t idx = 0; idx < elemCount; idx++) { + nsCOMPtr<nsIMsgDBHdr> hdr; + bool containsKey; + database->ContainsKey(aMsgKeyList[idx], &containsKey); + if (!containsKey) continue; + rv = database->GetMsgHdrForKey(aMsgKeyList[idx], getter_AddRefs(hdr)); + if (!hdr) + continue; // can't get message header, continue with the next one + + bool doesFit = true; + rv = autoSyncMgr->DoesMsgFitDownloadCriteria(hdr, &doesFit); + if (NS_SUCCEEDED(rv) && !mDownloadSet.Contains(aMsgKeyList[idx]) && + doesFit) { + bool excluded = false; + if (msgStrategy) { + rv = msgStrategy->IsExcluded(folder, hdr, &excluded); + + if (NS_SUCCEEDED(rv) && !excluded) { + mIsDownloadQChanged = true; + mDownloadSet.PutEntry(aMsgKeyList[idx]); + mDownloadQ.AppendElement(aMsgKeyList[idx]); + } + } + } + } // endfor + + if (mIsDownloadQChanged) { + LogOwnerFolderName("Download Q is created for "); + LogQWithSize(mDownloadQ, 0); + rv = autoSyncMgr->OnDownloadQChanged(this); + } + } + return rv; +} + +nsresult nsAutoSyncState::SortQueueBasedOnStrategy(nsTArray<nsMsgKey>& aQueue) { + nsresult rv; + nsCOMPtr<nsIMsgFolder> folder = do_QueryReferent(mOwnerFolder, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgDatabase> database; + rv = folder->GetMsgDatabase(getter_AddRefs(database)); + if (!database) return NS_ERROR_FAILURE; + + nsCOMPtr<nsIAutoSyncManager> autoSyncMgr = + do_GetService(NS_AUTOSYNCMANAGER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIAutoSyncMsgStrategy> msgStrategy; + rv = autoSyncMgr->GetMsgStrategy(getter_AddRefs(msgStrategy)); + NS_ENSURE_SUCCESS(rv, rv); + + MsgStrategyComparatorAdaptor strategyComp(msgStrategy, folder, database); + aQueue.Sort(strategyComp); + + return rv; +} + +// This method is a hack to prioritize newly inserted messages, +// without changing the size of the queue. It is required since +// we cannot sort ranges in nsTArray. +nsresult nsAutoSyncState::SortSubQueueBasedOnStrategy( + nsTArray<nsMsgKey>& aQueue, uint32_t aStartingOffset) { + NS_ASSERTION(aStartingOffset < aQueue.Length(), + "*** Starting offset is out of range"); + + // Copy already downloaded messages into a temporary queue, + // we want to exclude them from the sort. + nsTArray<nsMsgKey> tmpQ; + tmpQ.AppendElements(aQueue.Elements(), aStartingOffset); + + // Remove already downloaded messages and sort the resulting queue + aQueue.RemoveElementsAt(0, aStartingOffset); + + nsresult rv = SortQueueBasedOnStrategy(aQueue); + + // copy excluded messages back + aQueue.InsertElementsAt(0, tmpQ); + + return rv; +} + +NS_IMETHODIMP nsAutoSyncState::GetNextGroupOfMessages( + uint32_t aSuggestedGroupSizeLimit, uint32_t* aActualGroupSize, + nsTArray<RefPtr<nsIMsgDBHdr>>& aMessages) { + NS_ENSURE_ARG_POINTER(aActualGroupSize); + + aMessages.Clear(); + *aActualGroupSize = 0; + + nsresult rv; + nsCOMPtr<nsIMsgFolder> folder = do_QueryReferent(mOwnerFolder, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgDatabase> database; + folder->GetMsgDatabase(getter_AddRefs(database)); + + if (database) { + if (!mDownloadQ.IsEmpty()) { + // sort the download queue if new items are added since the last time + if (mIsDownloadQChanged) { + // we want to sort only pending messages. mOffset is + // the position of the first pending message in the download queue + rv = (mOffset > 0) ? SortSubQueueBasedOnStrategy(mDownloadQ, mOffset) + : SortQueueBasedOnStrategy(mDownloadQ); + + if (NS_SUCCEEDED(rv)) mIsDownloadQChanged = false; + } + + nsCOMPtr<nsIAutoSyncManager> autoSyncMgr = + do_GetService(NS_AUTOSYNCMANAGER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t msgCount = mDownloadQ.Length(); + uint32_t idx = mOffset; + + nsCOMPtr<nsIAutoSyncMsgStrategy> msgStrategy; + autoSyncMgr->GetMsgStrategy(getter_AddRefs(msgStrategy)); + + for (; idx < msgCount; idx++) { + bool containsKey = false; + database->ContainsKey(mDownloadQ[idx], &containsKey); + if (!containsKey) { + mDownloadSet.RemoveEntry(mDownloadQ[idx]); + mDownloadQ.RemoveElementAt(idx--); + msgCount--; + continue; + } + nsCOMPtr<nsIMsgDBHdr> qhdr; + database->GetMsgHdrForKey(mDownloadQ[idx], getter_AddRefs(qhdr)); + if (!qhdr) continue; // maybe deleted, skip it! + + // ensure that we don't have this message body offline already, + // possible if the user explicitly selects this message prior + // to auto-sync kicks in + bool hasMessageOffline; + folder->HasMsgOffline(mDownloadQ[idx], &hasMessageOffline); + if (hasMessageOffline) continue; + + // this check point allows msg strategy function + // to do last minute decisions based on the current + // state of TB such as the size of the message store etc. + if (msgStrategy) { + bool excluded = false; + if (NS_SUCCEEDED(msgStrategy->IsExcluded(folder, qhdr, &excluded)) && + excluded) + continue; + } + + uint32_t msgSize; + qhdr->GetMessageSize(&msgSize); + // ignore 0 byte messages; the imap parser asserts when we try + // to download them, and there's no point anyway. + if (!msgSize) continue; + + if (!*aActualGroupSize && msgSize >= aSuggestedGroupSizeLimit) { + *aActualGroupSize = msgSize; + aMessages.AppendElement(qhdr); + idx++; + break; + } + if ((*aActualGroupSize) + msgSize > aSuggestedGroupSizeLimit) break; + + aMessages.AppendElement(qhdr); + *aActualGroupSize += msgSize; + } // endfor + + mLastOffset = mOffset; + mOffset = idx; + } + + LogOwnerFolderName("Next group of messages to be downloaded."); + LogQWithSize(aMessages, 0); + } // endif + + return NS_OK; +} + +/** + * Called by nsAutoSyncManager::TimerCallback to process message headers for a + * folder in the discovery queue. The queue is created on the kAutoSyncFreq + * time base (1 hour). Headers lacking offline store are placed in download + * queue. + */ +NS_IMETHODIMP nsAutoSyncState::ProcessExistingHeaders( + uint32_t aNumOfHdrsToProcess, uint32_t* aLeftToProcess) { + NS_ENSURE_ARG_POINTER(aLeftToProcess); + + nsresult rv; + nsCOMPtr<nsIMsgFolder> folder = do_QueryReferent(mOwnerFolder, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgDatabase> database; + rv = folder->GetMsgDatabase(getter_AddRefs(database)); + if (!database) return NS_ERROR_FAILURE; + + // create a queue to process existing headers for the first time + if (mExistingHeadersQ.IsEmpty()) { + nsTArray<nsMsgKey> keys; + rv = database->ListAllKeys(keys); + NS_ENSURE_SUCCESS(rv, rv); + keys.Sort(); + mExistingHeadersQ.AppendElements(keys); + mProcessPointer = 0; + } + + // process the existing headers and find the messages not downloaded yet + uint32_t lastIdx = mProcessPointer; + nsTArray<nsMsgKey> msgKeys; + uint32_t keyCount = mExistingHeadersQ.Length(); + for (; mProcessPointer < (lastIdx + aNumOfHdrsToProcess) && + mProcessPointer < keyCount; + mProcessPointer++) { + bool hasMessageOffline; + folder->HasMsgOffline(mExistingHeadersQ[mProcessPointer], + &hasMessageOffline); + if (!hasMessageOffline) + msgKeys.AppendElement(mExistingHeadersQ[mProcessPointer]); + } + if (!msgKeys.IsEmpty()) { + nsCString folderName; + folder->GetURI(folderName); + MOZ_LOG( + gAutoSyncLog, LogLevel::Debug, + ("%s: %zu messages will be added into the download q of folder %s\n", + __func__, msgKeys.Length(), folderName.get())); + + rv = PlaceIntoDownloadQ(msgKeys); + if (NS_FAILED(rv)) mProcessPointer = lastIdx; + } + + *aLeftToProcess = keyCount - mProcessPointer; + + // cleanup if we are done processing + if (0 == *aLeftToProcess) { + mLastSyncTime = PR_Now(); + mExistingHeadersQ.Clear(); + mProcessPointer = 0; + folder->SetMsgDatabase(nullptr); + } + + return rv; +} + +void nsAutoSyncState::OnNewHeaderFetchCompleted( + const nsTArray<nsMsgKey>& aMsgKeyList) { + SetLastUpdateTime(PR_Now()); + if (!aMsgKeyList.IsEmpty()) PlaceIntoDownloadQ(aMsgKeyList); + MOZ_LOG( + gAutoSyncLog, LogLevel::Debug, + ("%s: %zu msg keys put into download q", __func__, aMsgKeyList.Length())); +} + +NS_IMETHODIMP nsAutoSyncState::UpdateFolder() { + nsresult rv; + nsCOMPtr<nsIAutoSyncManager> autoSyncMgr = + do_GetService(NS_AUTOSYNCMANAGER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIUrlListener> autoSyncMgrListener = + do_QueryInterface(autoSyncMgr, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = + do_QueryReferent(mOwnerFolder, &rv); + SetState(nsAutoSyncState::stUpdateIssued); + return imapFolder->UpdateFolderWithListener(nullptr, autoSyncMgrListener); +} + +NS_IMETHODIMP nsAutoSyncState::OnStartRunningUrl(nsIURI* aUrl) { + nsresult rv = NS_OK; + + // if there is a problem to start the download, set rv with the + // corresponding error code. In that case, AutoSyncManager is going to + // set the autosync state to nsAutoSyncState::stReadyToDownload + // to resume downloading another time + + // TODO: is there a way to make sure that download started without + // problem through nsIURI interface? + + nsCOMPtr<nsIAutoSyncManager> autoSyncMgr = + do_GetService(NS_AUTOSYNCMANAGER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + return autoSyncMgr->OnDownloadStarted(this, rv); +} + +/** + * This is called when a folder status URL finishes. It is also called when + * needed message downloads (imap fetch) for a folder completes. + */ +NS_IMETHODIMP nsAutoSyncState::OnStopRunningUrl(nsIURI* aUrl, + nsresult aExitCode) { + nsresult rv; + nsCOMPtr<nsIMsgFolder> ownerFolder = do_QueryReferent(mOwnerFolder, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIAutoSyncManager> autoSyncMgr = + do_GetService(NS_AUTOSYNCMANAGER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIUrlListener> autoSyncMgrListener = + do_QueryInterface(autoSyncMgr, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + if (mSyncState == stStatusIssued) { + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = + do_QueryReferent(mOwnerFolder, &rv); + NS_ENSURE_SUCCESS(rv, rv); + int32_t serverTotal, serverUnseen, serverRecent, serverNextUID; + imapFolder->GetServerTotal(&serverTotal); + imapFolder->GetServerUnseen(&serverUnseen); + imapFolder->GetServerRecent(&serverRecent); + imapFolder->GetServerNextUID(&serverNextUID); + // Note: UNSEEN often shows a change when nothing else changes. This is + // because UNSEEN produced by SELECT is not the number of unseen messages. + // So ignore change to UNSEEN to avoid spurious folder updates. Commented + // out below. + MOZ_LOG(gAutoSyncLog, LogLevel::Debug, + ("%s: serverUnseen=%d lastServerUnseen=%d", __func__, serverUnseen, + mLastServerUnseen)); + if (serverNextUID != mLastNextUID || serverTotal != mLastServerTotal || + serverRecent != mLastServerRecent //|| + /*(serverUnseen != mLastServerUnseen)*/) { + if (MOZ_LOG_TEST(gAutoSyncLog, LogLevel::Debug)) { + nsCString folderName; + ownerFolder->GetURI(folderName); + MOZ_LOG(gAutoSyncLog, LogLevel::Debug, + ("%s: folder %s status changed serverNextUID=%d lastNextUID=%d", + __func__, folderName.get(), serverNextUID, mLastNextUID)); + MOZ_LOG(gAutoSyncLog, LogLevel::Debug, + ("%s: serverTotal = %d lastServerTotal = %d serverRecent = %d " + "lastServerRecent = %d\n", + __func__, serverTotal, mLastServerTotal, serverRecent, + mLastServerRecent)); + } + SetServerCounts(serverTotal, serverRecent, serverUnseen, serverNextUID); + SetState(nsAutoSyncState::stUpdateIssued); + rv = imapFolder->UpdateFolderWithListener(nullptr, autoSyncMgrListener); + } else // folderstatus detected no change + { + if (MOZ_LOG_TEST(gAutoSyncLog, LogLevel::Debug)) { + nsCString folderName; + ownerFolder->GetURI(folderName); + MOZ_LOG(gAutoSyncLog, LogLevel::Debug, + ("%s: folder %s status or noop issued, no change", __func__, + folderName.get())); + } + // Status detected no change. This may be due to an previously deleted and + // now empty database so change compares above could be invalid. If so, + // force an update which will re-populate the database (.msf) and download + // all the message to mbox/maildir store. This check is only done on the + // first imap STATUS response after start-up and if the server response + // reports that the folder is not empty. + if (!mHaveAStatusResponse && serverTotal != 0) { + nsCOMPtr<nsIMsgDatabase> database; + ownerFolder->GetMsgDatabase(getter_AddRefs(database)); + bool hasHeader = false; + if (database) { + nsCOMPtr<nsIMsgEnumerator> hdrs; + database->EnumerateMessages(getter_AddRefs(hdrs)); + if (hdrs) hdrs->HasMoreElements(&hasHeader); + } + if (!hasHeader) { + if (MOZ_LOG_TEST(gAutoSyncLog, LogLevel::Debug)) { + nsCString folderName; + ownerFolder->GetURI(folderName); + MOZ_LOG(gAutoSyncLog, LogLevel::Debug, + ("%s: folder %s has empty DB, force an update", __func__, + folderName.get())); + } + SetServerCounts(serverTotal, serverRecent, serverUnseen, + serverNextUID); + SetState(nsAutoSyncState::stUpdateIssued); + rv = imapFolder->UpdateFolderWithListener(nullptr, + autoSyncMgrListener); + } + } + if (mSyncState == stStatusIssued) { + // Didn't force an update above so transition back to stCompletedIdle + ownerFolder->SetMsgDatabase(nullptr); + // nothing more to do. + SetState(nsAutoSyncState::stCompletedIdle); + // autoSyncMgr needs this notification, so manufacture it. + rv = autoSyncMgrListener->OnStopRunningUrl(aUrl, NS_OK); + } + } // end no change detected + mHaveAStatusResponse = true; + } else // URL not folderstatus but FETCH of message body + { + // XXXemre how we recover from this error? + rv = ownerFolder->ReleaseSemaphore(ownerFolder); + NS_ASSERTION(NS_SUCCEEDED(rv), "*** Cannot release folder semaphore"); + + nsCOMPtr<nsIMsgMailNewsUrl> mailUrl = do_QueryInterface(aUrl); + if (mailUrl) rv = mailUrl->UnRegisterListener(this); + + if (MOZ_LOG_TEST(gAutoSyncLog, LogLevel::Debug)) { + nsCString folderName; + ownerFolder->GetURI(folderName); + MOZ_LOG(gAutoSyncLog, LogLevel::Debug, + ("%s: URL for FETCH of msg body/bodies complete, folder %s", + __func__, folderName.get())); + } + rv = autoSyncMgr->OnDownloadCompleted(this, aExitCode); + } + return rv; +} + +NS_IMETHODIMP nsAutoSyncState::GetState(int32_t* aState) { + NS_ENSURE_ARG_POINTER(aState); + *aState = mSyncState; + return NS_OK; +} + +// clang-format off +const char* stateStrings[] = { + "stCompletedIdle:0", // Initial state + "stStatusIssued:1", // Imap STATUS or NOOP to occur to detect new msgs + "stUpdateNeeded:2", // Imap SELECT to occur due to "pending" msgs + "stUpdateIssued:3", // Imap SELECT to occur then fetch new headers + "stReadyToDownload:4", // Ready to download a group of new messages + "stDownloadInProgress:5" // Download, go to 4 if more msgs then 0 when all done +}; +// clang-format on + +NS_IMETHODIMP nsAutoSyncState::SetState(int32_t aState) { + mSyncState = aState; + if (aState == stCompletedIdle) { + ResetDownloadQ(); + // tell folder to let go of its cached msg db pointer + nsresult rv; + nsCOMPtr<nsIMsgMailSession> session = + do_GetService("@mozilla.org/messenger/services/session;1", &rv); + if (NS_SUCCEEDED(rv) && session) { + nsCOMPtr<nsIMsgFolder> ownerFolder = do_QueryReferent(mOwnerFolder, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + bool folderOpen; + uint32_t folderFlags; + ownerFolder->GetFlags(&folderFlags); + session->IsFolderOpenInWindow(ownerFolder, &folderOpen); + if (!folderOpen && !(folderFlags & nsMsgFolderFlags::Inbox)) + ownerFolder->SetMsgDatabase(nullptr); + } + } + nsCString logStr("Sync State set to |"); + logStr.Append(stateStrings[aState]); + logStr.AppendLiteral("| for "); + LogOwnerFolderName(logStr.get()); + return NS_OK; +} + +NS_IMETHODIMP nsAutoSyncState::TryCurrentGroupAgain(uint32_t aRetryCount) { + SetState(stReadyToDownload); + + nsresult rv; + if (++mRetryCounter > aRetryCount) { + ResetRetryCounter(); + rv = NS_ERROR_FAILURE; + } else + rv = Rollback(); + + return rv; +} + +NS_IMETHODIMP nsAutoSyncState::ResetRetryCounter() { + mRetryCounter = 0; + return NS_OK; +} + +NS_IMETHODIMP nsAutoSyncState::GetPendingMessageCount(int32_t* aMsgCount) { + NS_ENSURE_ARG_POINTER(aMsgCount); + *aMsgCount = mDownloadQ.Length() - mOffset; + return NS_OK; +} + +NS_IMETHODIMP nsAutoSyncState::GetTotalMessageCount(int32_t* aMsgCount) { + NS_ENSURE_ARG_POINTER(aMsgCount); + *aMsgCount = mDownloadQ.Length(); + return NS_OK; +} + +NS_IMETHODIMP nsAutoSyncState::GetOwnerFolder(nsIMsgFolder** aFolder) { + NS_ENSURE_ARG_POINTER(aFolder); + + nsresult rv; + nsCOMPtr<nsIMsgFolder> ownerFolder = do_QueryReferent(mOwnerFolder, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + ownerFolder.forget(aFolder); + return NS_OK; +} + +NS_IMETHODIMP nsAutoSyncState::Rollback() { + mOffset = mLastOffset; + return NS_OK; +} + +NS_IMETHODIMP nsAutoSyncState::ResetDownloadQ() { + mOffset = mLastOffset = 0; + mDownloadSet.Clear(); + mDownloadQ.Clear(); + mDownloadQ.Compact(); + + return NS_OK; +} + +/** + * Tests whether the given folder is owned by the same imap server + * or not. + */ +NS_IMETHODIMP nsAutoSyncState::IsSibling(nsIAutoSyncState* aAnotherStateObj, + bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + *aResult = false; + + nsresult rv; + nsCOMPtr<nsIMsgFolder> folderA, folderB; + + rv = GetOwnerFolder(getter_AddRefs(folderA)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aAnotherStateObj->GetOwnerFolder(getter_AddRefs(folderB)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgIncomingServer> serverA, serverB; + rv = folderA->GetServer(getter_AddRefs(serverA)); + NS_ENSURE_SUCCESS(rv, rv); + rv = folderB->GetServer(getter_AddRefs(serverB)); + NS_ENSURE_SUCCESS(rv, rv); + + bool isSibling; + rv = serverA->Equals(serverB, &isSibling); + + if (NS_SUCCEEDED(rv)) *aResult = isSibling; + + return rv; +} + +/** + * Test whether the download queue is empty. + */ +NS_IMETHODIMP nsAutoSyncState::IsDownloadQEmpty(bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + *aResult = mDownloadQ.IsEmpty(); + return NS_OK; +} + +NS_IMETHODIMP nsAutoSyncState::DownloadMessagesForOffline( + nsTArray<RefPtr<nsIMsgDBHdr>> const& messages) { + nsresult rv; + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString messageIds; + nsTArray<nsMsgKey> msgKeys; + + rv = nsImapMailFolder::BuildIdsAndKeyArray(messages, messageIds, msgKeys); + if (NS_FAILED(rv) || messageIds.IsEmpty()) return rv; + + // acquire semaphore for offline store. If it fails, we won't download + nsCOMPtr<nsIMsgFolder> folder = do_QueryReferent(mOwnerFolder, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = folder->AcquireSemaphore(folder); + NS_ENSURE_SUCCESS(rv, rv); + + if (MOZ_LOG_TEST(gAutoSyncLog, LogLevel::Debug)) { + nsCString folderName; + folder->GetURI(folderName); + MOZ_LOG(gAutoSyncLog, LogLevel::Debug, + ("%s: downloading UIDs %s for folder %s", __func__, + messageIds.get(), folderName.get())); + } + // start downloading + rv = imapService->DownloadMessagesForOffline(messageIds, folder, this, + nullptr); + if (NS_SUCCEEDED(rv)) SetState(stDownloadInProgress); + + return rv; +} + +NS_IMETHODIMP nsAutoSyncState::GetLastSyncTime(PRTime* aLastSyncTime) { + NS_ENSURE_ARG_POINTER(aLastSyncTime); + *aLastSyncTime = mLastSyncTime; + return NS_OK; +} + +void nsAutoSyncState::SetLastSyncTimeInSec(int32_t aLastSyncTime) { + mLastSyncTime = ((PRTime)aLastSyncTime * PR_USEC_PER_SEC); +} + +NS_IMETHODIMP nsAutoSyncState::GetLastUpdateTime(PRTime* aLastUpdateTime) { + NS_ENSURE_ARG_POINTER(aLastUpdateTime); + *aLastUpdateTime = mLastUpdateTime; + return NS_OK; +} + +NS_IMETHODIMP nsAutoSyncState::SetLastUpdateTime(PRTime aLastUpdateTime) { + mLastUpdateTime = aLastUpdateTime; + return NS_OK; +} + +void nsAutoSyncState::SetServerCounts(int32_t total, int32_t recent, + int32_t unseen, int32_t nextUID) { + mLastServerTotal = total; + mLastServerRecent = recent; + mLastServerUnseen = unseen; + mLastNextUID = nextUID; +} + +NS_IMPL_ISUPPORTS(nsAutoSyncState, nsIAutoSyncState, nsIUrlListener) + +void nsAutoSyncState::LogQWithSize(nsTArray<nsMsgKey>& q, uint32_t toOffset) { + nsCOMPtr<nsIMsgFolder> ownerFolder = do_QueryReferent(mOwnerFolder); + if (ownerFolder) { + nsCOMPtr<nsIMsgDatabase> database; + ownerFolder->GetMsgDatabase(getter_AddRefs(database)); + + uint32_t x = q.Length(); + while (x > toOffset && database) { + x--; + nsCOMPtr<nsIMsgDBHdr> h; + database->GetMsgHdrForKey(q[x], getter_AddRefs(h)); + uint32_t s; + if (h) { + h->GetMessageSize(&s); + MOZ_LOG(gAutoSyncLog, LogLevel::Debug, + ("Elem #%d, size: %u bytes\n", x + 1, s)); + } else + MOZ_LOG(gAutoSyncLog, LogLevel::Debug, + ("unable to get header for key %ul", q[x])); + } + } +} + +void nsAutoSyncState::LogQWithSize(nsTArray<RefPtr<nsIMsgDBHdr>> const& q, + uint32_t toOffset) { + nsCOMPtr<nsIMsgFolder> ownerFolder = do_QueryReferent(mOwnerFolder); + if (ownerFolder) { + nsCOMPtr<nsIMsgDatabase> database; + ownerFolder->GetMsgDatabase(getter_AddRefs(database)); + + uint32_t x = q.Length(); + while (x > toOffset && database) { + x--; + if (q[x]) { + uint32_t s; + q[x]->GetMessageSize(&s); + MOZ_LOG(gAutoSyncLog, LogLevel::Debug, + ("Elem #%d, size: %u bytes\n", x + 1, s)); + } else + MOZ_LOG(gAutoSyncLog, LogLevel::Debug, + ("null header in q at index %ul", x)); + } + } +} + +void nsAutoSyncState::LogOwnerFolderName(const char* s) { + nsCOMPtr<nsIMsgFolder> ownerFolder = do_QueryReferent(mOwnerFolder); + if (ownerFolder) { + nsCString folderName; + ownerFolder->GetURI(folderName); + MOZ_LOG(gAutoSyncLog, LogLevel::Debug, + ("*** %s Folder: %s ***\n", s, folderName.get())); + } +} diff --git a/comm/mailnews/imap/src/nsAutoSyncState.h b/comm/mailnews/imap/src/nsAutoSyncState.h new file mode 100644 index 0000000000..d04ad016c2 --- /dev/null +++ b/comm/mailnews/imap/src/nsAutoSyncState.h @@ -0,0 +1,106 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsAutoSyncState_h__ +#define nsAutoSyncState_h__ + +#include "MailNewsTypes.h" +#include "nsIAutoSyncState.h" +#include "nsIUrlListener.h" +#include "nsIWeakReferenceUtils.h" +#include "nsTHashtable.h" +#include "nsHashKeys.h" +#include "nsTArray.h" +#include "prlog.h" + +class nsImapMailFolder; +class nsIAutoSyncMsgStrategy; +class nsIMsgDatabase; + +/** + * An adaptor class to make msg strategy nsTArray.Sort() + * compatible. + */ +class MsgStrategyComparatorAdaptor { + public: + MsgStrategyComparatorAdaptor(nsIAutoSyncMsgStrategy* aStrategy, + nsIMsgFolder* aFolder, + nsIMsgDatabase* aDatabase); + + /** @return True if the elements are equals; false otherwise. */ + bool Equals(const nsMsgKey& a, const nsMsgKey& b) const; + + /** @return True if (a < b); false otherwise. */ + bool LessThan(const nsMsgKey& a, const nsMsgKey& b) const; + + private: + MsgStrategyComparatorAdaptor(); + + private: + nsIAutoSyncMsgStrategy* mStrategy; + nsIMsgFolder* mFolder; + nsIMsgDatabase* mDatabase; +}; + +/** + * Facilitates auto-sync capabilities for imap folders. + */ +class nsAutoSyncState final : public nsIAutoSyncState, public nsIUrlListener { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIAUTOSYNCSTATE + NS_DECL_NSIURLLISTENER + + explicit nsAutoSyncState(nsImapMailFolder* aOwnerFolder, + PRTime aLastSyncTime = 0UL); + + /// Called by owner folder when new headers are fetched from the server + void OnNewHeaderFetchCompleted(const nsTArray<nsMsgKey>& aMsgKeyList); + + /// Sets the last sync time in lower precision (seconds) + void SetLastSyncTimeInSec(int32_t aLastSyncTime); + + /// Manages storage space for auto-sync operations + nsresult ManageStorageSpace(); + + void SetServerCounts(int32_t total, int32_t recent, int32_t unseen, + int32_t nextUID); + + private: + ~nsAutoSyncState(); + + nsresult PlaceIntoDownloadQ(const nsTArray<nsMsgKey>& aMsgKeyList); + nsresult SortQueueBasedOnStrategy(nsTArray<nsMsgKey>& aQueue); + nsresult SortSubQueueBasedOnStrategy(nsTArray<nsMsgKey>& aQueue, + uint32_t aStartingOffset); + + void LogOwnerFolderName(const char* s); + void LogQWithSize(nsTArray<nsMsgKey>& q, uint32_t toOffset = 0); + void LogQWithSize(nsTArray<RefPtr<nsIMsgDBHdr>> const& q, + uint32_t toOffset = 0); + + private: + int32_t mSyncState; + nsWeakPtr mOwnerFolder; + uint32_t mOffset; + uint32_t mLastOffset; + + // used to tell if the Server counts have changed. + int32_t mLastServerTotal; + int32_t mLastServerRecent; + int32_t mLastServerUnseen; + int32_t mLastNextUID; + + PRTime mLastSyncTime; + PRTime mLastUpdateTime; + uint32_t mProcessPointer; + bool mIsDownloadQChanged; + uint32_t mRetryCounter; + nsTHashtable<nsUint32HashKey> mDownloadSet; + nsTArray<nsMsgKey> mDownloadQ; + nsTArray<nsMsgKey> mExistingHeadersQ; + bool mHaveAStatusResponse; +}; + +#endif diff --git a/comm/mailnews/imap/src/nsImapBodyShell.cpp b/comm/mailnews/imap/src/nsImapBodyShell.cpp new file mode 100644 index 0000000000..fecd4230b7 --- /dev/null +++ b/comm/mailnews/imap/src/nsImapBodyShell.cpp @@ -0,0 +1,1060 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" +#include "nsImapBodyShell.h" +#include "nsImapProtocol.h" +#include "nsMimeTypes.h" +#include "nsServiceManagerUtils.h" +#include "mozilla/Logging.h" + +// need to talk to Rich about this... +#define IMAP_EXTERNAL_CONTENT_HEADER "X-Mozilla-IMAP-Part" + +using namespace mozilla; +extern LazyLogModule IMAPCache; // defined in nsImapProtocol.cpp + +// imapbody.cpp +// Implementation of the nsImapBodyShell and associated classes +// These are used to parse IMAP BODYSTRUCTURE responses, and intelligently (?) +// figure out what parts we need to display inline. + +/* + Create a nsImapBodyShell from a full BODYSTRUCUTRE response from the + parser. + + The body shell represents a single, top-level object, the message. The + message body might be treated as either a container or a leaf (just like any + arbitrary part). + + Steps for creating a part: + 1. Pull out the paren grouping for the part + 2. Create a generic part object with that buffer + 3. The factory will return either a leaf or container, depending on what + it really is. + 4. It is responsible for parsing its children, if there are any +*/ + +///////////// nsImapBodyShell //////////////////////////////////// + +NS_IMPL_ISUPPORTS0(nsImapBodyShell) + +nsImapBodyShell::nsImapBodyShell(nsIMAPBodypartMessage* message, uint32_t UID, + uint32_t UIDValidity, const char* folderName, + bool showAttachmentsInline) + : m_message(message), + m_isValid(false), + m_folderName(folderName), + m_generatingPart(nullptr), + m_isBeingGenerated(false), + m_cached(false), + m_generatingWholeMessage(false), + m_contentModified(IMAP_ContentModifiedType::IMAP_CONTENT_NOT_MODIFIED) { + m_UID = ""; + m_UID.AppendInt(UID); + m_UID_validity = m_UID; + m_UID_validity.AppendInt(UIDValidity); + + m_contentModified = showAttachmentsInline + ? IMAP_CONTENT_MODIFIED_VIEW_INLINE + : IMAP_CONTENT_MODIFIED_VIEW_AS_LINKS; + + SetIsValid(m_message != nullptr); +} + +nsImapBodyShell::~nsImapBodyShell() { + delete m_message; + m_prefetchQueue.Clear(); +} + +void nsImapBodyShell::SetIsValid(bool valid) { m_isValid = valid; } + +// Fills in buffer (and adopts storage) for header object +void nsImapBodyShell::AdoptMessageHeaders(char* headers, const char* partNum) { + if (!GetIsValid()) return; + + if (!partNum) partNum = "0"; + + // we are going to say that a message header object only has + // part data, and no header data. + nsIMAPBodypart* foundPart = m_message->FindPartWithNumber(partNum); + if (foundPart) { + nsIMAPBodypartMessage* messageObj = foundPart->GetnsIMAPBodypartMessage(); + if (messageObj) { + messageObj->AdoptMessageHeaders(headers); + if (!messageObj->GetIsValid()) SetIsValid(false); + } else { + // We were filling in message headers for a given part number. + // We looked up that part number, found an object, but it + // wasn't of type message/rfc822. + // Something's wrong. + NS_ASSERTION(false, "object not of type message rfc822"); + } + } else + SetIsValid(false); +} + +// Fills in buffer (and adopts storage) for MIME headers in appropriate object. +// If object can't be found, sets isValid to false. +void nsImapBodyShell::AdoptMimeHeader(const char* partNum, char* mimeHeader) { + if (!GetIsValid()) return; + + NS_ASSERTION(partNum, "null partnum in body shell"); + + nsIMAPBodypart* foundPart = m_message->FindPartWithNumber(partNum); + + if (foundPart) { + foundPart->AdoptHeaderDataBuffer(mimeHeader); + if (!foundPart->GetIsValid()) SetIsValid(false); + } else { + SetIsValid(false); + } +} + +void nsImapBodyShell::AddPrefetchToQueue(nsIMAPeFetchFields fields, + const char* partNumber) { + nsIMAPMessagePartID newPart(fields, partNumber); + m_prefetchQueue.AppendElement(newPart); +} + +// Requires that the shell is valid when called +// Performs a preflight check on all message parts to see if they are all +// inline. Returns true if all parts are inline, false otherwise. +bool nsImapBodyShell::PreflightCheckAllInline() { + bool rv = m_message->PreflightCheckAllInline(this); + // if (rv) + // MOZ_LOG(IMAP, out, ("BODYSHELL: All parts inline. Reverting to whole + // message download.")); + return rv; +} + +// When partNum is NULL, Generates a whole message and intelligently +// leaves out parts that are not inline. + +// When partNum is not NULL, Generates a MIME part that hasn't been downloaded +// yet Ok, here's how we're going to do this. Essentially, this will be the +// mirror image of the "normal" generation. All parts will be left out except a +// single part which is explicitly specified. All relevant headers will be +// included. Libmime will extract only the part of interest, so we don't have to +// worry about the other parts. This also has the advantage that it looks like +// it will be more workable for nested parts. For instance, if a user clicks on +// a link to a forwarded message, then that forwarded message may be generated +// along with any images that the forwarded message contains, for instance. + +int32_t nsImapBodyShell::Generate(nsImapProtocol* conn, char* partNum) { + // Hold the connection in existence for the duration. + RefPtr<nsImapProtocol> kungFuDeathGrip(conn); + + m_isBeingGenerated = true; + m_generatingPart = partNum; + int32_t contentLength = 0; + + if (!GetIsValid() || PreflightCheckAllInline()) { + // We don't have a valid shell, or all parts are going to be inline anyway. + // Fall back to fetching the whole message. +#ifdef DEBUG_chrisf + NS_ASSERTION(GetIsValid()); +#endif + m_generatingWholeMessage = true; + uint32_t messageSize = conn->GetMessageSize(GetUID()); + MOZ_LOG(IMAPCache, LogLevel::Debug, + ("Generate(): Set IMAP_CONTENT_NOT MODIFIED")); + // So that when we cache it, we know we have the whole message. + conn->SetContentModified(IMAP_CONTENT_NOT_MODIFIED); + if (!conn->DeathSignalReceived()) + conn->FallbackToFetchWholeMsg(GetUID(), messageSize); + contentLength = (int32_t)messageSize; // ugh + } else { + // We have a valid shell. + bool streamCreated = false; + m_generatingWholeMessage = false; + + ////// PASS 1 : PREFETCH /////// + // First, prefetch any additional headers/data that we need + if (!conn->GetPseudoInterrupted()) + m_message->Generate( + this, conn, false, + true); // This queues up everything we need to prefetch + // Now, run a single pipelined prefetch (neato!) + conn->PipelinedFetchMessageParts(GetUID(), m_prefetchQueue); + m_prefetchQueue.Clear(); + + ////// PASS 2 : COMPUTE STREAM SIZE /////// + // Next, figure out the size from the parts that we're going to fill in, + // plus all of the MIME headers, plus the message header itself + if (!conn->GetPseudoInterrupted()) + contentLength = m_message->Generate(this, conn, false, false); + + // Setup the stream + if (!conn->GetPseudoInterrupted() && !conn->DeathSignalReceived()) { + nsresult rv = conn->BeginMessageDownLoad(contentLength, MESSAGE_RFC822); + if (NS_FAILED(rv)) { + m_generatingPart = nullptr; + conn->AbortMessageDownLoad(); + return 0; + } + streamCreated = true; + } + + ////// PASS 3 : GENERATE /////// + // Generate the message + if (!conn->GetPseudoInterrupted() && !conn->DeathSignalReceived()) + m_message->Generate(this, conn, true, false); + + // Close the stream here - normal. If pseudointerrupted, the connection + // will abort the download stream + if (!conn->GetPseudoInterrupted() && !conn->DeathSignalReceived()) + conn->NormalMessageEndDownload(); + else if (streamCreated) + conn->AbortMessageDownLoad(); + + m_generatingPart = NULL; + } + + m_isBeingGenerated = false; + return contentLength; +} + +///////////// nsIMAPBodypart //////////////////////////////////// + +nsIMAPBodypart::nsIMAPBodypart(char* partNumber, nsIMAPBodypart* parentPart) { + SetIsValid(true); + m_parentPart = parentPart; + m_partNumberString = partNumber; // storage adopted + m_partData = NULL; + m_headerData = NULL; + m_boundaryData = NULL; // initialize from parsed BODYSTRUCTURE + m_contentLength = 0; + m_partLength = 0; + + m_contentType = NULL; + m_bodyType = NULL; + m_bodySubType = NULL; + m_bodyID = NULL; + m_bodyDescription = NULL; + m_bodyEncoding = NULL; +} + +nsIMAPBodypart::~nsIMAPBodypart() { + PR_FREEIF(m_partNumberString); + PR_FREEIF(m_contentType); + PR_FREEIF(m_bodyType); + PR_FREEIF(m_bodySubType); + PR_FREEIF(m_bodyID); + PR_FREEIF(m_bodyDescription); + PR_FREEIF(m_bodyEncoding); + PR_FREEIF(m_partData); + PR_FREEIF(m_headerData); + PR_FREEIF(m_boundaryData); +} + +void nsIMAPBodypart::SetIsValid(bool valid) { + m_isValid = valid; + if (!m_isValid) { + // MOZ_LOG(IMAP, out, ("BODYSHELL: Part is invalid. Part Number: %s + // Content-Type: %s", m_partNumberString, m_contentType)); + } +} + +// Adopts storage for part data buffer. If NULL, sets isValid to false. +void nsIMAPBodypart::AdoptPartDataBuffer(char* buf) { + m_partData = buf; + if (!m_partData) { + SetIsValid(false); + } +} + +// Adopts storage for header data buffer. If NULL, sets isValid to false. +void nsIMAPBodypart::AdoptHeaderDataBuffer(char* buf) { + m_headerData = buf; + if (!m_headerData) { + SetIsValid(false); + } +} + +// Finds the part with given part number +// Returns a nsIMAPBodystructure of the matched part if it is this +// or one of its children. Returns NULL otherwise. +nsIMAPBodypart* nsIMAPBodypart::FindPartWithNumber(const char* partNum) { + // either brute force, or do it the smart way - look at the number. + // (the parts should be ordered, and hopefully indexed by their number) + + if (m_partNumberString && !PL_strcasecmp(partNum, m_partNumberString)) + return this; + + // if (!m_partNumberString && !PL_strcasecmp(partNum, "1")) + // return this; + + return NULL; +} + +void nsIMAPBodypart::QueuePrefetchMIMEHeader(nsImapBodyShell* aShell) { + aShell->AddPrefetchToQueue(kMIMEHeader, m_partNumberString); +} + +int32_t nsIMAPBodypart::GenerateMIMEHeader(nsImapBodyShell* aShell, + nsImapProtocol* conn, bool stream, + bool prefetch) { + if (prefetch && !m_headerData) { + QueuePrefetchMIMEHeader(aShell); + return 0; + } + if (m_headerData) { + int32_t mimeHeaderLength = 0; + + if (!ShouldFetchInline(aShell)) { + // if this part isn't inline, add the X-Mozilla-IMAP-Part header + char* xPartHeader = PR_smprintf("%s: %s", IMAP_EXTERNAL_CONTENT_HEADER, + m_partNumberString); + if (xPartHeader) { + if (stream) { + conn->Log("SHELL", "GENERATE-XHeader", m_partNumberString); + conn->HandleMessageDownLoadLine(xPartHeader, false); + } + mimeHeaderLength += PL_strlen(xPartHeader); + PR_Free(xPartHeader); + } + } + + mimeHeaderLength += PL_strlen(m_headerData); + if (stream) { + conn->Log("SHELL", "GENERATE-MIMEHeader", m_partNumberString); + conn->HandleMessageDownLoadLine(m_headerData, + false); // all one line? Can we do that? + } + + return mimeHeaderLength; + } + + SetIsValid(false); // prefetch didn't adopt a MIME header + return 0; +} + +int32_t nsIMAPBodypart::GeneratePart(nsImapBodyShell* aShell, + nsImapProtocol* conn, bool stream, + bool prefetch) { + if (prefetch) return 0; // don't need to prefetch anything + + if (m_partData) // we have prefetched the part data + { + if (stream) { + conn->Log("SHELL", "GENERATE-Part-Prefetched", m_partNumberString); + conn->HandleMessageDownLoadLine(m_partData, false); + } + return PL_strlen(m_partData); + } + + // we are fetching and streaming this part's body as we go + if (stream && !conn->DeathSignalReceived()) { + char* generatingPart = aShell->GetGeneratingPart(); + bool fetchingSpecificPart = + (generatingPart && !PL_strcmp(generatingPart, m_partNumberString)); + + conn->Log("SHELL", "GENERATE-Part-Inline", m_partNumberString); + MOZ_LOG(IMAPCache, LogLevel::Debug, + ("GeneratePart(): Call FetchTryChunking() part length=%" PRIi32 + ", part number=%s", + m_partLength, m_partNumberString)); + conn->FetchTryChunking(aShell->GetUID(), kMIMEPart, true, + m_partNumberString, m_partLength, + !fetchingSpecificPart); + } + return m_partLength; // the part length has been filled in from the + // BODYSTRUCTURE response +} + +int32_t nsIMAPBodypart::GenerateBoundary(nsImapBodyShell* aShell, + nsImapProtocol* conn, bool stream, + bool prefetch, bool lastBoundary) { + if (prefetch) return 0; // don't need to prefetch anything + + if (m_boundaryData) { + if (!lastBoundary) { + if (stream) { + conn->Log("SHELL", "GENERATE-Boundary", m_partNumberString); + conn->HandleMessageDownLoadLine(m_boundaryData, false); + } + return PL_strlen(m_boundaryData); + } + + // the last boundary + char* lastBoundaryData = PR_smprintf("%s--", m_boundaryData); + if (lastBoundaryData) { + if (stream) { + conn->Log("SHELL", "GENERATE-Boundary-Last", m_partNumberString); + conn->HandleMessageDownLoadLine(lastBoundaryData, false); + } + int32_t rv = PL_strlen(lastBoundaryData); + PR_Free(lastBoundaryData); + return rv; + } + // HandleMemoryFailure(); + return 0; + } + return 0; +} + +int32_t nsIMAPBodypart::GenerateEmptyFilling(nsImapBodyShell* aShell, + nsImapProtocol* conn, bool stream, + bool prefetch) { + if (prefetch) return 0; // don't need to prefetch anything + + const nsString& emptyString = conn->GetEmptyMimePartString(); + if (!emptyString.IsEmpty()) { + if (stream) { + conn->Log("SHELL", "GENERATE-Filling", m_partNumberString); + conn->HandleMessageDownLoadLine(NS_ConvertUTF16toUTF8(emptyString).get(), + false); + } + return emptyString.Length(); + } + return 0; +} + +// Returns true if the prefs say that this content type should +// explicitly be kept in when filling in the shell +bool nsIMAPBodypart::ShouldExplicitlyFetchInline() { return false; } + +// Returns true if the prefs say that this content type should +// explicitly be left out when filling in the shell +bool nsIMAPBodypart::ShouldExplicitlyNotFetchInline() { return false; } + +///////////// nsIMAPBodypartLeaf ///////////////////////////// + +nsIMAPBodypartLeaf::nsIMAPBodypartLeaf(char* partNum, + nsIMAPBodypart* parentPart, + char* bodyType, char* bodySubType, + char* bodyID, char* bodyDescription, + char* bodyEncoding, int32_t partLength, + bool preferPlainText) + : nsIMAPBodypart(partNum, parentPart), mPreferPlainText(preferPlainText) { + m_bodyType = bodyType; + m_bodySubType = bodySubType; + m_bodyID = bodyID; + m_bodyDescription = bodyDescription; + m_bodyEncoding = bodyEncoding; + m_partLength = partLength; + if (m_bodyType && m_bodySubType) { + m_contentType = PR_smprintf("%s/%s", m_bodyType, m_bodySubType); + } + SetIsValid(true); +} + +nsIMAPBodypartType nsIMAPBodypartLeaf::GetType() { return IMAP_BODY_LEAF; } + +int32_t nsIMAPBodypartLeaf::Generate(nsImapBodyShell* aShell, + nsImapProtocol* conn, bool stream, + bool prefetch) { + int32_t len = 0; + + if (GetIsValid()) { + if (stream && !prefetch) + conn->Log("SHELL", "GENERATE-Leaf", m_partNumberString); + + // Stream out the MIME part boundary + // GenerateBoundary(); + NS_ASSERTION(m_parentPart, "part has no parent"); + // nsIMAPBodypartMessage *parentMessage = m_parentPart ? + // m_parentPart->GetnsIMAPBodypartMessage() : NULL; + + // Stream out the MIME header of this part, if this isn't the only body part + // of a message + // if (parentMessage ? !parentMessage->GetIsTopLevelMessage() : true) + if ((m_parentPart->GetType() != IMAP_BODY_MESSAGE_RFC822) && + !conn->GetPseudoInterrupted()) + len += GenerateMIMEHeader(aShell, conn, stream, prefetch); + + if (!conn->GetPseudoInterrupted()) { + if (ShouldFetchInline(aShell)) { + // Fetch and stream the content of this part + len += GeneratePart(aShell, conn, stream, prefetch); + } else { + // fill in the filling within the empty part + len += GenerateEmptyFilling(aShell, conn, stream, prefetch); + } + } + } + m_contentLength = len; + return m_contentLength; +} + +// returns true if this part should be fetched inline for generation. +bool nsIMAPBodypartLeaf::ShouldFetchInline(nsImapBodyShell* aShell) { + char* generatingPart = aShell->GetGeneratingPart(); + if (generatingPart) { + // If we are generating a specific part + if (!PL_strcmp(generatingPart, m_partNumberString)) { + // This is the part we're generating + return true; + } + + // If this is the only body part of a message, and that + // message is the part being generated, then this leaf should + // be inline as well. + if ((m_parentPart->GetType() == IMAP_BODY_MESSAGE_RFC822) && + (!PL_strcmp(m_parentPart->GetPartNumberString(), generatingPart))) + return true; + + // The parent of this part is a multipart + if (m_parentPart->GetType() == IMAP_BODY_MULTIPART) { + // This is the first text part of a forwarded message + // with a multipart body, and that message is being generated, + // then generate this part. + nsIMAPBodypart* grandParent = m_parentPart->GetParentPart(); + // grandParent must exist, since multiparts need parents + NS_ASSERTION(grandParent, "grandparent doesn't exist for multi-part alt"); + if (grandParent && (grandParent->GetType() == IMAP_BODY_MESSAGE_RFC822) && + (!PL_strcmp(grandParent->GetPartNumberString(), generatingPart)) && + (m_partNumberString[PL_strlen(m_partNumberString) - 1] == '1') && + !PL_strcasecmp(m_bodyType, "text")) + return true; // we're downloading it inline + + // This is a child of a multipart/appledouble attachment, + // and that multipart/appledouble attachment is being generated + if (!PL_strcasecmp(m_parentPart->GetBodySubType(), "appledouble") && + !PL_strcmp(m_parentPart->GetPartNumberString(), generatingPart)) + return true; // we're downloading it inline + } + + // Leave out all other leaves if this isn't the one + // we're generating. + // Maybe change later to check parents, etc. + return false; + } + + // We are generating the whole message, possibly (hopefully) + // leaving out non-inline parts + if (ShouldExplicitlyFetchInline()) return true; + if (ShouldExplicitlyNotFetchInline()) return false; + + // If the parent is a message (this is the only body part of that + // message), and that message should be inline, then its body + // should inherit the inline characteristics of that message + if (m_parentPart->GetType() == IMAP_BODY_MESSAGE_RFC822) + return m_parentPart->ShouldFetchInline(aShell); + + // View Attachments As Links is on. + if (!(aShell->GetContentModified() == IMAP_CONTENT_MODIFIED_VIEW_INLINE)) { + // The last text part is still displayed inline, + // even if View Attachments As Links is on. + nsIMAPBodypart* grandParentPart = m_parentPart->GetParentPart(); + if ((mPreferPlainText || + !PL_strcasecmp(m_parentPart->GetBodySubType(), "mixed")) && + !PL_strcmp(m_partNumberString, "1") && + !PL_strcasecmp(m_bodyType, "text")) + return true; // we're downloading it inline + + if ((!PL_strcasecmp(m_parentPart->GetBodySubType(), "alternative") || + (grandParentPart && + !PL_strcasecmp(grandParentPart->GetBodySubType(), "alternative"))) && + !PL_strcasecmp(m_bodyType, "text") && + ((!PL_strcasecmp(m_bodySubType, "plain") && mPreferPlainText) || + (!PL_strcasecmp(m_bodySubType, "html") && !mPreferPlainText))) + return true; + + // This is the first text part of a top-level multipart. + // For instance, a message with multipart body, where the first + // part is multipart, and this is the first leaf of that first part. + if (m_parentPart->GetType() == IMAP_BODY_MULTIPART && + (PL_strlen(m_partNumberString) >= 2) && + !PL_strcmp(m_partNumberString + PL_strlen(m_partNumberString) - 2, + ".1") && // this is the first text type on this level + (!PL_strcmp(m_parentPart->GetPartNumberString(), "1") || + !PL_strcmp(m_parentPart->GetPartNumberString(), "2")) && + !PL_strcasecmp(m_bodyType, "text")) + return true; + // This is the first text part of a top-level multipart of the + // toplevelmessage This 'assumes' the text body is first leaf. This is not + // required for valid email. The only other way is to get + // content-disposition = attachment and exclude those text parts. + if (m_parentPart->GetType() == IMAP_BODY_MULTIPART && + !PL_strcasecmp(m_bodyType, "text") && + !PL_strcmp(m_parentPart->GetPartNumberString(), "0") && + !PL_strcmp(m_partNumberString, "1")) + return true; + + // we may have future problems needing tests here + + return false; // we can leave it on the server + } +#ifdef XP_MACOSX + // If it is either applesingle, or a resource fork for appledouble + if (!PL_strcasecmp(m_contentType, "application/applefile")) { + // if it is appledouble + if (m_parentPart->GetType() == IMAP_BODY_MULTIPART && + !PL_strcasecmp(m_parentPart->GetBodySubType(), "appledouble")) { + // This is the resource fork of a multipart/appledouble. + // We inherit the inline attributes of the parent, + // which was derived from its OTHER child. (The data fork.) + return m_parentPart->ShouldFetchInline(aShell); + } + // it is applesingle + return false; // we can leave it on the server + } +#endif // XP_MACOSX + + // Fetch type APPLICATION now if the subtype is a signature or if it's an + // octet-stream. Otherwise, fetch on demand. + if (!PL_strcasecmp(m_bodyType, "APPLICATION") && + PL_strncasecmp(m_bodySubType, "x-pkcs7", 7) && + PL_strcasecmp(m_bodySubType, "octet-stream")) + return false; // we can leave it on the server + if (!PL_strcasecmp(m_bodyType, "AUDIO")) return false; + // Here's where we can add some more intelligence -- let's leave out + // any other parts that we know we can't display inline. + return true; // we're downloading it inline +} + +bool nsIMAPBodypartMultipart::IsLastTextPart(const char* partNumberString) { + // iterate backwards over the parent's part list and if the part is + // text, compare it to the part number string + for (int i = m_partList.Length() - 1; i >= 0; i--) { + nsIMAPBodypart* part = m_partList[i]; + if (!PL_strcasecmp(part->GetBodyType(), "text")) + return !PL_strcasecmp(part->GetPartNumberString(), partNumberString); + } + return false; +} + +bool nsIMAPBodypartLeaf::PreflightCheckAllInline(nsImapBodyShell* aShell) { + // only need to check this part, since it has no children. + return ShouldFetchInline(aShell); +} + +///////////// nsIMAPBodypartMessage //////////////////////// + +nsIMAPBodypartMessage::nsIMAPBodypartMessage( + char* partNum, nsIMAPBodypart* parentPart, bool topLevelMessage, + char* bodyType, char* bodySubType, char* bodyID, char* bodyDescription, + char* bodyEncoding, int32_t partLength, bool preferPlainText) + : nsIMAPBodypartLeaf(partNum, parentPart, bodyType, bodySubType, bodyID, + bodyDescription, bodyEncoding, partLength, + preferPlainText) { + m_topLevelMessage = topLevelMessage; + if (m_topLevelMessage) { + m_partNumberString = PR_smprintf("0"); + if (!m_partNumberString) { + SetIsValid(false); + return; + } + } + m_body = NULL; + m_headers = new nsIMAPMessageHeaders( + m_partNumberString, this); // We always have a Headers object + if (!m_headers || !m_headers->GetIsValid()) { + SetIsValid(false); + return; + } + SetIsValid(true); +} + +void nsIMAPBodypartMessage::SetBody(nsIMAPBodypart* body) { + if (m_body) delete m_body; + m_body = body; +} + +nsIMAPBodypartType nsIMAPBodypartMessage::GetType() { + return IMAP_BODY_MESSAGE_RFC822; +} + +nsIMAPBodypartMessage::~nsIMAPBodypartMessage() { + delete m_headers; + delete m_body; +} + +int32_t nsIMAPBodypartMessage::Generate(nsImapBodyShell* aShell, + nsImapProtocol* conn, bool stream, + bool prefetch) { + if (!GetIsValid()) return 0; + + m_contentLength = 0; + + if (stream && !prefetch) + conn->Log("SHELL", "GENERATE-MessageRFC822", m_partNumberString); + + if (!m_topLevelMessage && + !conn->GetPseudoInterrupted()) // not the top-level message - we need + // the MIME header as well as the + // message header + { + // but we don't need the MIME headers of a message/rfc822 part if this + // content type is in (part of) the main msg header. In other words, we + // still need these MIME headers if this message/rfc822 body part is + // enclosed in the msg body (most likely as a body part of a multipart/mixed + // msg). + // Don't fetch (bug 128888) Do fetch (bug 168097) + // ---------------------------------- ----------------------------------- + // message/rfc822 (parent part) message/rfc822 + // message/rfc822 <<<--- multipart/mixed (parent part) + // multipart/mixed message/rfc822 <<<--- + // text/html (body text) multipart/mixed + // text/plain (attachment) text/html (body text) + // application/msword (attachment) text/plain (attachment) + // application/msword (attachment) + // "<<<---" points to the part we're examining here. + if (PL_strcasecmp(m_bodyType, "message") || + PL_strcasecmp(m_bodySubType, "rfc822") || + PL_strcasecmp(m_parentPart->GetBodyType(), "message") || + PL_strcasecmp(m_parentPart->GetBodySubType(), "rfc822")) + m_contentLength += GenerateMIMEHeader(aShell, conn, stream, prefetch); + } + + if (!conn->GetPseudoInterrupted()) + m_contentLength += m_headers->Generate(aShell, conn, stream, prefetch); + if (!conn->GetPseudoInterrupted()) + m_contentLength += m_body->Generate(aShell, conn, stream, prefetch); + + return m_contentLength; +} + +bool nsIMAPBodypartMessage::ShouldFetchInline(nsImapBodyShell* aShell) { + if (m_topLevelMessage) // the main message should always be defined as + // "inline" + return true; + + char* generatingPart = aShell->GetGeneratingPart(); + if (generatingPart) { + // If we are generating a specific part + // Always generate containers (just don't fill them in) + // because it is low cost (everything is cached) + // and it gives the message its full MIME structure, + // to avoid any potential mishap. + return true; + } + + // Generating whole message + if (ShouldExplicitlyFetchInline()) return true; + if (ShouldExplicitlyNotFetchInline()) return false; + + // Message types are inline, by default. + return true; +} + +bool nsIMAPBodypartMessage::PreflightCheckAllInline(nsImapBodyShell* aShell) { + if (!ShouldFetchInline(aShell)) return false; + + return m_body->PreflightCheckAllInline(aShell); +} + +// Fills in buffer (and adopts storage) for header object +void nsIMAPBodypartMessage::AdoptMessageHeaders(char* headers) { + if (!GetIsValid()) return; + + // we are going to say that the message headers only have + // part data, and no header data. + m_headers->AdoptPartDataBuffer(headers); + if (!m_headers->GetIsValid()) SetIsValid(false); +} + +// Finds the part with given part number +// Returns a nsIMAPBodystructure of the matched part if it is this +// or one of its children. Returns NULL otherwise. +nsIMAPBodypart* nsIMAPBodypartMessage::FindPartWithNumber(const char* partNum) { + // either brute force, or do it the smart way - look at the number. + // (the parts should be ordered, and hopefully indexed by their number) + + if (!PL_strcasecmp(partNum, m_partNumberString)) return this; + + return m_body->FindPartWithNumber(partNum); +} + +///////////// nsIMAPBodypartMultipart //////////////////////// + +nsIMAPBodypartMultipart::nsIMAPBodypartMultipart(char* partNum, + nsIMAPBodypart* parentPart) + : nsIMAPBodypart(partNum, parentPart) { + if (!m_parentPart || (m_parentPart->GetType() == IMAP_BODY_MESSAGE_RFC822)) { + // the multipart (this) will inherit the part number of its parent + PR_FREEIF(m_partNumberString); + if (!m_parentPart) { + m_partNumberString = PR_smprintf("0"); + } else + m_partNumberString = NS_xstrdup(m_parentPart->GetPartNumberString()); + } + m_bodyType = NS_xstrdup("multipart"); + if (m_parentPart && m_bodyType) + SetIsValid(true); + else + SetIsValid(false); +} + +nsIMAPBodypartType nsIMAPBodypartMultipart::GetType() { + return IMAP_BODY_MULTIPART; +} + +nsIMAPBodypartMultipart::~nsIMAPBodypartMultipart() { + for (int i = m_partList.Length() - 1; i >= 0; i--) { + delete m_partList[i]; + } +} + +void nsIMAPBodypartMultipart::SetBodySubType(char* bodySubType) { + PR_FREEIF(m_bodySubType); + PR_FREEIF(m_contentType); + m_bodySubType = bodySubType; + if (m_bodyType && m_bodySubType) + m_contentType = PR_smprintf("%s/%s", m_bodyType, m_bodySubType); +} + +int32_t nsIMAPBodypartMultipart::Generate(nsImapBodyShell* aShell, + nsImapProtocol* conn, bool stream, + bool prefetch) { + int32_t len = 0; + + if (GetIsValid()) { + if (stream && !prefetch) + conn->Log("SHELL", "GENERATE-Multipart", m_partNumberString); + + // Stream out the MIME header of this part + + bool parentIsMessageType = + GetParentPart() + ? (GetParentPart()->GetType() == IMAP_BODY_MESSAGE_RFC822) + : true; + + // If this is multipart/signed, then we always want to generate the MIME + // headers of this multipart. Otherwise, we only want to do it if the parent + // is not of type "message" + bool needMIMEHeader = + !parentIsMessageType; // !PL_strcasecmp(m_bodySubType, "signed") ? true + // : !parentIsMessageType; + if (needMIMEHeader && + !conn->GetPseudoInterrupted()) // not a message body's type + { + len += GenerateMIMEHeader(aShell, conn, stream, prefetch); + } + + if (ShouldFetchInline(aShell)) { + for (auto part : m_partList) { + if (!conn->GetPseudoInterrupted()) + len += GenerateBoundary(aShell, conn, stream, prefetch, false); + if (!conn->GetPseudoInterrupted()) + len += part->Generate(aShell, conn, stream, prefetch); + } + if (!conn->GetPseudoInterrupted()) + len += GenerateBoundary(aShell, conn, stream, prefetch, true); + } else { + // fill in the filling within the empty part + if (!conn->GetPseudoInterrupted()) + len += GenerateEmptyFilling(aShell, conn, stream, prefetch); + } + } + m_contentLength = len; + return m_contentLength; +} + +bool nsIMAPBodypartMultipart::ShouldFetchInline(nsImapBodyShell* aShell) { + char* generatingPart = aShell->GetGeneratingPart(); + if (generatingPart) { + // If we are generating a specific part + // Always generate containers (just don't fill them in) + // because it is low cost (everything is cached) + // and it gives the message its full MIME structure, + // to avoid any potential mishap. + return true; + } + + // Generating whole message + if (ShouldExplicitlyFetchInline()) return true; + if (ShouldExplicitlyNotFetchInline()) return false; + + if (!PL_strcasecmp(m_bodySubType, "alternative")) return true; + + nsIMAPBodypart* grandparentPart = m_parentPart->GetParentPart(); + + // if we're a multipart sub-part of multipart alternative, we need to + // be fetched because mime will always display us. + if (!PL_strcasecmp(m_parentPart->GetBodySubType(), "alternative") && + GetType() == IMAP_BODY_MULTIPART) + return true; + // If "Show Attachments as Links" is on, and + // the parent of this multipart is not a message, + // then it's not inline. + if (!(aShell->GetContentModified() == IMAP_CONTENT_MODIFIED_VIEW_INLINE) && + (m_parentPart->GetType() != IMAP_BODY_MESSAGE_RFC822) && + (m_parentPart->GetType() == IMAP_BODY_MULTIPART + ? (grandparentPart + ? grandparentPart->GetType() != IMAP_BODY_MESSAGE_RFC822 + : true) + : true)) + return false; + + // multiparts are always inline (even multipart/appledouble) + // (their children might not be, though) + return true; +} + +bool nsIMAPBodypartMultipart::PreflightCheckAllInline(nsImapBodyShell* aShell) { + bool rv = ShouldFetchInline(aShell); + + size_t i = 0; + while (rv && (i < m_partList.Length())) { + rv = m_partList[i]->PreflightCheckAllInline(aShell); + i++; + } + + return rv; +} + +nsIMAPBodypart* nsIMAPBodypartMultipart::FindPartWithNumber( + const char* partNum) { + NS_ASSERTION(partNum, "null part passed into FindPartWithNumber"); + + // check this + if (!PL_strcmp(partNum, m_partNumberString)) return this; + + // check children + for (int i = m_partList.Length() - 1; i >= 0; i--) { + nsIMAPBodypart* foundPart = m_partList[i]->FindPartWithNumber(partNum); + if (foundPart) return foundPart; + } + + // not this, or any of this's children + return NULL; +} + +///////////// nsIMAPMessageHeaders //////////////////////////////////// + +nsIMAPMessageHeaders::nsIMAPMessageHeaders(char* partNum, + nsIMAPBodypart* parentPart) + : nsIMAPBodypart(partNum, parentPart) { + if (!partNum) { + SetIsValid(false); + return; + } + m_partNumberString = NS_xstrdup(partNum); + if (!m_partNumberString) { + SetIsValid(false); + return; + } + if (!m_parentPart || !m_parentPart->GetnsIMAPBodypartMessage()) { + // Message headers created without a valid Message parent + NS_ASSERTION(false, "creating message headers with invalid message parent"); + SetIsValid(false); + } +} + +nsIMAPBodypartType nsIMAPMessageHeaders::GetType() { + return IMAP_BODY_MESSAGE_HEADER; +} + +void nsIMAPMessageHeaders::QueuePrefetchMessageHeaders( + nsImapBodyShell* aShell) { + if (!m_parentPart->GetnsIMAPBodypartMessage() + ->GetIsTopLevelMessage()) // not top-level headers + aShell->AddPrefetchToQueue(kRFC822HeadersOnly, m_partNumberString); + else + aShell->AddPrefetchToQueue(kRFC822HeadersOnly, NULL); +} + +int32_t nsIMAPMessageHeaders::Generate(nsImapBodyShell* aShell, + nsImapProtocol* conn, bool stream, + bool prefetch) { + // prefetch the header + if (prefetch && !m_partData && !conn->DeathSignalReceived()) { + QueuePrefetchMessageHeaders(aShell); + } + + if (stream && !prefetch) + conn->Log("SHELL", "GENERATE-MessageHeaders", m_partNumberString); + + // stream out the part data + if (ShouldFetchInline(aShell)) { + if (!conn->GetPseudoInterrupted()) + m_contentLength = GeneratePart(aShell, conn, stream, prefetch); + } else { + m_contentLength = 0; // don't fill in any filling for the headers + } + return m_contentLength; +} + +bool nsIMAPMessageHeaders::ShouldFetchInline(nsImapBodyShell* aShell) { + return m_parentPart->ShouldFetchInline(aShell); +} + +///////////// nsImapBodyShellCache //////////////////////////////////// + +nsImapBodyShellCache::nsImapBodyShellCache() + : m_shellList(kMaxEntries), m_shellHash(kMaxEntries) {} + +// We'll use an LRU scheme here. +// We will add shells in numerical order, so the +// least recently used one will be in slot 0. +void nsImapBodyShellCache::EjectEntry() { + MOZ_ASSERT(!m_shellList.IsEmpty()); + + nsImapBodyShell* removedShell = m_shellList.ElementAt(0); + + m_shellList.RemoveElementAt(0); + m_shellHash.Remove(removedShell->GetUID()); +} + +void nsImapBodyShellCache::Clear() { + m_shellList.ClearAndRetainStorage(); + m_shellHash.Clear(); +} + +void nsImapBodyShellCache::AddShellToCache(nsImapBodyShell* shell) { + // If it's already in the cache, then just return. + // This has the side-effect of re-ordering the LRU list + // to put this at the top, which is good, because it's what we want. + if (FindShellForUID(shell->GetUID_validity(), shell->GetFolderName(), + shell->GetContentModified())) { + return; + } + + // OK, so it's not in the cache currently. + + // First, for safety sake, remove any entry with the given UID, + // just in case we have a collision between two messages in different + // folders with the same UID. + RefPtr<nsImapBodyShell> foundShell; + m_shellHash.Get(shell->GetUID_validity(), getter_AddRefs(foundShell)); + if (foundShell) { + m_shellHash.Remove(foundShell->GetUID_validity()); + m_shellList.RemoveElement(foundShell); + } + + // Make sure there's enough room + while (m_shellList.Length() > (kMaxEntries - 1)) { + EjectEntry(); + } + + // Add the new one to the cache + m_shellList.AppendElement(shell); + + m_shellHash.InsertOrUpdate(shell->GetUID_validity(), RefPtr{shell}); + shell->SetIsCached(true); +} + +nsImapBodyShell* nsImapBodyShellCache::FindShellForUID( + nsACString const& UID, nsACString const& mailboxName, + IMAP_ContentModifiedType modType) { + RefPtr<nsImapBodyShell> foundShell; + m_shellHash.Get(UID, getter_AddRefs(foundShell)); + if (!foundShell) return nullptr; + // Make sure the content-modified types are compatible. + // This allows us to work seamlessly while people switch between + // View Attachments Inline and View Attachments As Links. + // Enforce the invariant that any cached shell we use + // match the current content-modified settings. + if (modType != foundShell->GetContentModified()) return nullptr; + + // mailbox names must match also. + if (!mailboxName.Equals(foundShell->GetFolderName())) return nullptr; + + // adjust the LRU stuff. This defeats the performance gain of the hash if + // it actually is found since this is linear. + m_shellList.RemoveElement(foundShell); + m_shellList.AppendElement(foundShell); // Adds to end + + return foundShell; +} + +///////////// nsIMAPMessagePartID //////////////////////////////////// + +nsIMAPMessagePartID::nsIMAPMessagePartID(nsIMAPeFetchFields fields, + const char* partNumberString) + : m_partNumberString(partNumberString), m_fields(fields) {} diff --git a/comm/mailnews/imap/src/nsImapBodyShell.h b/comm/mailnews/imap/src/nsImapBodyShell.h new file mode 100644 index 0000000000..c2ab8c5898 --- /dev/null +++ b/comm/mailnews/imap/src/nsImapBodyShell.h @@ -0,0 +1,357 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* +nsImapBodyShell and associated classes +*/ + +#ifndef IMAPBODY_H +#define IMAPBODY_H + +#include "mozilla/Attributes.h" +#include "nsImapCore.h" +#include "nsString.h" +#include "nsRefPtrHashtable.h" +#include "nsTArray.h" + +class nsImapProtocol; + +typedef enum _nsIMAPBodypartType { + IMAP_BODY_MESSAGE_RFC822, + IMAP_BODY_MESSAGE_HEADER, + IMAP_BODY_LEAF, + IMAP_BODY_MULTIPART +} nsIMAPBodypartType; + +class nsImapBodyShell; +class nsIMAPBodypartMessage; + +class nsIMAPBodypart { + public: + // Construction + virtual bool GetIsValid() { return m_isValid; } + virtual void SetIsValid(bool valid); + virtual nsIMAPBodypartType GetType() = 0; + + // Generation + // Generates an HTML representation of this part. Returns content length + // generated, -1 if failed. + virtual int32_t Generate(nsImapBodyShell* aShell, nsImapProtocol* conn, + bool /*stream*/, bool /* prefetch */) { + return -1; + } + virtual void AdoptPartDataBuffer( + char* buf); // Adopts storage for part data buffer. If NULL, sets + // isValid to false. + virtual void AdoptHeaderDataBuffer( + char* buf); // Adopts storage for header data buffer. If NULL, sets + // isValid to false. + virtual bool ShouldFetchInline(nsImapBodyShell* aShell) { + return true; + } // returns true if this part should be fetched inline for generation. + virtual bool PreflightCheckAllInline(nsImapBodyShell* aShell) { return true; } + + virtual bool ShouldExplicitlyFetchInline(); + virtual bool ShouldExplicitlyNotFetchInline(); + virtual bool IsLastTextPart(const char* partNumberString) { return true; } + + protected: + // If stream is false, simply returns the content length that will be + // generated the body of the part itself + virtual int32_t GeneratePart(nsImapBodyShell* aShell, nsImapProtocol* conn, + bool stream, bool prefetch); + // the MIME headers of the part + virtual int32_t GenerateMIMEHeader(nsImapBodyShell* aShell, + nsImapProtocol* conn, bool stream, + bool prefetch); + // Generates the MIME boundary wrapper for this part. + virtual int32_t GenerateBoundary(nsImapBodyShell* aShell, + nsImapProtocol* conn, bool stream, + bool prefetch, bool lastBoundary); + // lastBoundary indicates whether or not this should be the boundary for the + // final MIME part of the multipart message. + // Generates (possibly empty) filling for a part that won't be filled in + // inline. + virtual int32_t GenerateEmptyFilling(nsImapBodyShell* aShell, + nsImapProtocol* conn, bool stream, + bool prefetch); + + // Part Numbers / Hierarchy + public: + virtual char* GetPartNumberString() { return m_partNumberString; } + virtual nsIMAPBodypart* FindPartWithNumber( + const char* partNum); // Returns the part object with the given number + virtual nsIMAPBodypart* GetParentPart() { + return m_parentPart; + } // Returns the parent of this part. + // We will define a part of type message/rfc822 to be the + // parent of its body and header. + // A multipart is a parent of its child parts. + // All other leafs do not have children. + + // Other / Helpers + public: + virtual ~nsIMAPBodypart(); + virtual nsIMAPBodypartMessage* GetnsIMAPBodypartMessage() { return NULL; } + + const char* GetBodyType() { return m_bodyType; } + const char* GetBodySubType() { return m_bodySubType; } + void SetBoundaryData(char* boundaryData) { m_boundaryData = boundaryData; } + + protected: + virtual void QueuePrefetchMIMEHeader(nsImapBodyShell* aShell); + // virtual void PrefetchMIMEHeader(); // Initiates a prefetch for the MIME + // header of this part. + nsIMAPBodypart(char* partNumber, nsIMAPBodypart* parentPart); + + protected: + bool m_isValid; // If this part is valid. + char* m_partNumberString; // string representation of this part's + // full-hierarchy number. Define 0 to be the + // top-level message + char* m_partData; // data for this part. NULL if not filled in yet. + char* m_headerData; // data for this part's MIME header. NULL if not filled + // in yet. + char* m_boundaryData; // MIME boundary for this part + int32_t m_partLength; + int32_t m_contentLength; // Total content length which will be Generate()'d. + // -1 if not filled in yet. + nsIMAPBodypart* m_parentPart; // Parent of this part + + // Fields - Filled in from parsed BODYSTRUCTURE response (as well as others) + char* m_contentType; // constructed from m_bodyType and m_bodySubType + char* m_bodyType; + char* m_bodySubType; + char* m_bodyID; + char* m_bodyDescription; + char* m_bodyEncoding; + // we ignore extension data for now +}; + +// Message headers +// A special type of nsIMAPBodypart +// These may be headers for the top-level message, +// or any body part of type message/rfc822. +class nsIMAPMessageHeaders : public nsIMAPBodypart { + public: + nsIMAPMessageHeaders(char* partNum, nsIMAPBodypart* parentPart); + virtual nsIMAPBodypartType GetType() override; + // Generates an HTML representation of this part. Returns content length + // generated, -1 if failed. + virtual int32_t Generate(nsImapBodyShell* aShell, nsImapProtocol* conn, + bool stream, bool prefetch) override; + virtual bool ShouldFetchInline(nsImapBodyShell* aShell) override; + virtual void QueuePrefetchMessageHeaders(nsImapBodyShell* aShell); +}; + +class nsIMAPBodypartMultipart : public nsIMAPBodypart { + public: + nsIMAPBodypartMultipart(char* partNum, nsIMAPBodypart* parentPart); + virtual nsIMAPBodypartType GetType() override; + virtual ~nsIMAPBodypartMultipart(); + virtual bool ShouldFetchInline(nsImapBodyShell* aShell) override; + virtual bool PreflightCheckAllInline(nsImapBodyShell* aShell) override; + // Generates an HTML representation of this part. Returns content length + // generated, -1 if failed. + virtual int32_t Generate(nsImapBodyShell* aShell, nsImapProtocol* conn, + bool stream, bool prefetch) override; + // Returns the part object with the given number + virtual nsIMAPBodypart* FindPartWithNumber(const char* partNum) override; + virtual bool IsLastTextPart(const char* partNumberString) override; + void AppendPart(nsIMAPBodypart* part) { m_partList.AppendElement(part); } + void SetBodySubType(char* bodySubType); + + protected: + // An ordered list of top-level body parts for this shell + nsTArray<nsIMAPBodypart*> m_partList; +}; + +// The name "leaf" is somewhat misleading, since a part of type message/rfc822 +// is technically a leaf, even though it can contain other parts within it. +class nsIMAPBodypartLeaf : public nsIMAPBodypart { + public: + nsIMAPBodypartLeaf(char* partNum, nsIMAPBodypart* parentPart, char* bodyType, + char* bodySubType, char* bodyID, char* bodyDescription, + char* bodyEncoding, int32_t partLength, + bool preferPlainText); + virtual nsIMAPBodypartType GetType() override; + // Generates an HTML representation of this part. Returns content length + // generated, -1 if failed. + virtual int32_t Generate(nsImapBodyShell* aShell, nsImapProtocol* conn, + bool stream, bool prefetch) override; + // returns true if this part should be fetched inline for generation. + virtual bool ShouldFetchInline(nsImapBodyShell* aShell) override; + virtual bool PreflightCheckAllInline(nsImapBodyShell* aShell) override; + + private: + bool mPreferPlainText; +}; + +class nsIMAPBodypartMessage : public nsIMAPBodypartLeaf { + public: + nsIMAPBodypartMessage(char* partNum, nsIMAPBodypart* parentPart, + bool topLevelMessage, char* bodyType, char* bodySubType, + char* bodyID, char* bodyDescription, char* bodyEncoding, + int32_t partLength, bool preferPlainText); + void SetBody(nsIMAPBodypart* body); + virtual nsIMAPBodypartType GetType() override; + virtual ~nsIMAPBodypartMessage(); + virtual int32_t Generate(nsImapBodyShell* aShell, nsImapProtocol* conn, + bool stream, bool prefetch) override; + virtual bool ShouldFetchInline(nsImapBodyShell* aShell) override; + virtual bool PreflightCheckAllInline(nsImapBodyShell* aShell) override; + // Returns the part object with the given number + virtual nsIMAPBodypart* FindPartWithNumber(const char* partNum) override; + void AdoptMessageHeaders( + char* headers); // Fills in buffer (and adopts storage) for header object + // partNum specifies the message part number to which the + // headers correspond. NULL indicates the top-level + // message + virtual nsIMAPBodypartMessage* GetnsIMAPBodypartMessage() override { + return this; + } + virtual bool GetIsTopLevelMessage() { return m_topLevelMessage; } + + protected: + nsIMAPMessageHeaders* m_headers; // Every body shell should have headers + nsIMAPBodypart* m_body; + bool m_topLevelMessage; // Whether or not this is the top-level message +}; + +// MessagePartID and an array of them are used for pipelining prefetches. + +class nsIMAPMessagePartID { + public: + nsIMAPMessagePartID(nsIMAPeFetchFields fields, const char* partNumberString); + nsIMAPeFetchFields GetFields() { return m_fields; } + const char* GetPartNumberString() { return m_partNumberString; } + + protected: + const char* m_partNumberString; + nsIMAPeFetchFields m_fields; +}; + +// We will refer to a Body "Shell" as a hierarchical object representation of a +// parsed BODYSTRUCTURE response. A shell contains representations of Shell +// "Parts." A Body Shell can undergo essentially two operations: Construction +// and Generation. Shell Construction occurs from a parsed a BODYSTRUCTURE +// response, split into empty parts. Shell Generation generates a "MIME Shell" +// of the message and streams it to libmime for display. The MIME Shell has +// selected (inline) parts filled in, and leaves all others for on-demand +// retrieval through explicit part fetches. + +class nsImapBodyShell : public nsISupports { + public: + NS_DECL_THREADSAFE_ISUPPORTS + nsImapBodyShell(nsIMAPBodypartMessage* message, uint32_t UID, + uint32_t UIDValidity, const char* folderName, + bool showAttachmentsInline); + // To be used after a shell is uncached + bool GetIsValid() { return m_isValid; } + void SetIsValid(bool valid); + + // Prefetch + // Adds a message body part to the queue to be prefetched + // in a single, pipelined command + void AddPrefetchToQueue(nsIMAPeFetchFields, const char* partNum); + // Fills in buffer (and adopts storage) for header object + // partNum specifies the message part number to which the + // headers correspond. NULL indicates the top-level message + void AdoptMessageHeaders(char* headers, const char* partNum); + // Fills in buffer (and adopts storage) for MIME headers in appropriate + // object. If object can't be found, sets isValid to false. + void AdoptMimeHeader(const char* partNum, char* mimeHeader); + + // Generation + // Streams out an HTML representation of this IMAP message, going along and + // fetching parts it thinks it needs, and leaving empty shells for the parts + // it doesn't. + // Returns number of bytes generated, or -1 if invalid. + // If partNum is not NULL, then this works to generates a MIME part that + // hasn't been downloaded yet and leaves out all other parts. By default, to + // generate a normal message, partNum should be NULL. + int32_t Generate(nsImapProtocol* conn, char* partNum); + + // Returns TRUE if the user has the pref "Show Attachments Inline" set. + // Returns FALSE if the setting is "Show Attachments as Links" + bool GetShowAttachmentsInline(); + // Returns true if all parts are inline, false otherwise. Does not generate + // anything. + bool PreflightCheckAllInline(); + + // Helpers + nsCString& GetUID() { return m_UID; } + nsCString& GetUID_validity() { return m_UID_validity; } + nsCString const& GetFolderName() const { return m_folderName; } + char* GetGeneratingPart() { return m_generatingPart; } + // Returns true if this is in the process of being generated, + // so we don't re-enter + bool IsBeingGenerated() { return m_isBeingGenerated; } + bool IsShellCached() { return m_cached; } + void SetIsCached(bool isCached) { m_cached = isCached; } + bool GetGeneratingWholeMessage() { return m_generatingWholeMessage; } + IMAP_ContentModifiedType GetContentModified() { return m_contentModified; } + + protected: + virtual ~nsImapBodyShell(); + + nsIMAPBodypartMessage* m_message; + + // Array of pipelined part prefetches. + nsTArray<nsIMAPMessagePartID> m_prefetchQueue; + + bool m_isValid; + nsCString m_UID; // UID of this message + nsCString m_UID_validity; // appended UID and UID-validity of this message + nsCString m_folderName; // folder that contains this message + char* m_generatingPart; // If a specific part is being generated, this is it. + // Otherwise, NULL. + bool m_isBeingGenerated; // true if this body shell is in the process of + // being generated + bool m_cached; // Whether or not this shell is cached + bool m_generatingWholeMessage; // whether or not we are generating the whole + // (non-MPOD) message Set to false if we are + // generating by parts + // under what conditions the content has been modified. + // Either IMAP_CONTENT_MODIFIED_VIEW_INLINE or + // IMAP_CONTENT_MODIFIED_VIEW_AS_LINKS + IMAP_ContentModifiedType m_contentModified; +}; + +// This class caches shells, so we don't have to always go and re-fetch them. +// This does not cache any of the filled-in inline parts; those are cached +// individually in the libnet memory cache. (ugh, how will we do that?) Since +// we'll only be retrieving shells for messages over a given size, and since the +// shells themselves won't be very large, this cache will not grow very big +// (relatively) and should handle most common usage scenarios. + +// A body cache is associated with a given host, spanning folders, so +// it uses both UID and UIDVALIDITY . + +class nsImapBodyShellCache { + public: + nsImapBodyShellCache(); + + // Adds shell to cache, possibly ejecting + // another entry based on scheme in EjectEntry(). + void AddShellToCache(nsImapBodyShell* shell); + // Looks up a shell in the cache given the message's UID. + nsImapBodyShell* FindShellForUID(nsACString const& UID, + nsACString const& mailboxName, + IMAP_ContentModifiedType modType); + void Clear(); + + protected: + static constexpr int kMaxEntries = 20; + // Chooses an entry to eject; deletes that entry; and ejects it from the + // cache, clearing up a new space. + void EjectEntry(); + + nsTArray<nsImapBodyShell*> m_shellList; + // For quick lookup based on UID + nsRefPtrHashtable<nsCStringHashKey, nsImapBodyShell> m_shellHash; +}; + +#endif // IMAPBODY_H diff --git a/comm/mailnews/imap/src/nsImapCore.h b/comm/mailnews/imap/src/nsImapCore.h new file mode 100644 index 0000000000..4c4e213d45 --- /dev/null +++ b/comm/mailnews/imap/src/nsImapCore.h @@ -0,0 +1,191 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// clang-format off +#ifndef _nsImapCore_H_ +#define _nsImapCore_H_ + +#include "MailNewsTypes.h" +#include "nsString.h" + +/* imap message flags */ +typedef uint16_t imapMessageFlagsType; + +/* used for communication between imap thread and event sinks */ +#define kNoFlags 0x00 /* RFC flags */ +#define kMarked 0x01 +#define kUnmarked 0x02 +#define kNoinferiors 0x04 +#define kNoselect 0x08 +#define kImapTrash 0x10 /* Navigator flag */ +#define kJustExpunged 0x20 /* This update is a post expunge url update. */ +#define kPersonalMailbox 0x40 /* this mailbox is in the personal namespace */ +#define kPublicMailbox 0x80 /* this mailbox is in the public namespace */ +#define kOtherUsersMailbox 0x100 /* this mailbox is in the other users' namespace */ +#define kNameSpace 0x200 /* this mailbox IS a namespace */ +#define kNewlyCreatedFolder 0x400 /* this folder was just created */ +#define kImapDrafts 0x800 /* XLIST says this is the drafts folder */ +#define kImapSpam 0x1000 /* XLIST says this is the spam folder */ +#define kImapSent 0x2000 /* XLIST says this is the sent folder */ +#define kImapInbox 0x4000 /* XLIST says this is the INBOX folder */ +#define kImapAllMail 0x8000 /* XLIST says this is AllMail (GMail) */ +#define kImapXListTrash 0x10000 /* XLIST says this is the trash */ +#define kNonExistent 0x20000 /* RFC 5258, LIST-EXTENDED */ +#define kSubscribed 0x40000 /* RFC 5258, LIST-EXTENDED */ +#define kRemote 0x80000 /* RFC 5258, LIST-EXTENDED */ +#define kHasChildren 0x100000 /* RFC 5258, LIST-EXTENDED */ +#define kHasNoChildren 0x200000 /* RFC 5258, LIST-EXTENDED */ +#define kImapArchive 0x400000 /* RFC 5258, LIST-EXTENDED */ + +/* flags for individual messages */ +/* currently the ui only offers \Seen and \Flagged */ +#define kNoImapMsgFlag 0x0000 +#define kImapMsgSeenFlag 0x0001 +#define kImapMsgAnsweredFlag 0x0002 +#define kImapMsgFlaggedFlag 0x0004 +#define kImapMsgDeletedFlag 0x0008 +#define kImapMsgDraftFlag 0x0010 +#define kImapMsgRecentFlag 0x0020 +#define kImapMsgForwardedFlag 0x0040 /* Not always supported, check mailbox folder */ +#define kImapMsgMDNSentFlag 0x0080 /* Not always supported. check mailbox folder */ +#define kImapMsgCustomKeywordFlag 0x0100 /* this msg has a custom keyword */ +#define kImapMsgSupportMDNSentFlag 0x2000 +#define kImapMsgSupportForwardedFlag 0x4000 +/** + * We use a separate xlist trash flag so we can prefer the GMail trash + * over an existing Trash folder we may have created. + */ +#define kImapMsgSupportUserFlag 0x8000 +/* This seems to be the most cost effective way of +* piggying back the server support user flag info. +*/ + +/* if a url creator does not know the hierarchyDelimiter, use this */ +#define kOnlineHierarchySeparatorUnknown '^' +#define kOnlineHierarchySeparatorNil '|' + +#define IMAP_URL_TOKEN_SEPARATOR ">" +#define kUidUnknown -1 +// Special initial value meaning ACLs need to be loaded from DB. +#define kAclInvalid ((uint32_t) -1) + +// this has to do with Mime Parts on Demand. It used to live in net.h +// I'm not sure where this will live, but here is OK temporarily +typedef enum { + IMAP_CONTENT_NOT_MODIFIED = 0, + IMAP_CONTENT_MODIFIED_VIEW_INLINE, + IMAP_CONTENT_MODIFIED_VIEW_AS_LINKS, + IMAP_CONTENT_FORCE_CONTENT_NOT_MODIFIED +} IMAP_ContentModifiedType; + +// I think this should really go in an imap.h equivalent file +typedef enum { + kPersonalNamespace = 0, + kOtherUsersNamespace, + kPublicNamespace, + kDefaultNamespace, + kUnknownNamespace +} EIMAPNamespaceType; + + +/** + * IMAP server feature, mostly CAPABILITY responses + * + * one of the cap flags below + */ +typedef uint64_t eIMAPCapabilityFlag; +/** + * IMAP server features, mostly CAPABILITY responses + * + * any set of the cap flags below, i.e. + * i.e. 0, 1 or more |eIMAPCapabilityFlag|. + */ +typedef uint64_t eIMAPCapabilityFlags; + +const eIMAPCapabilityFlag kCapabilityUndefined = 0x00000000; +const eIMAPCapabilityFlag kCapabilityDefined = 0x00000001; +const eIMAPCapabilityFlag kHasAuthLoginCapability = 0x00000002; /* AUTH LOGIN (not the same as kHasAuthOldLoginCapability) */ +const eIMAPCapabilityFlag kHasAuthOldLoginCapability = 0x00000004; /* original IMAP login method */ +const eIMAPCapabilityFlag kHasXSenderCapability = 0x00000008; +const eIMAPCapabilityFlag kIMAP4Capability = 0x00000010; /* RFC1734 */ +const eIMAPCapabilityFlag kIMAP4rev1Capability = 0x00000020; /* RFC2060 */ +const eIMAPCapabilityFlag kIMAP4other = 0x00000040; /* future rev?? */ +const eIMAPCapabilityFlag kNoHierarchyRename = 0x00000080; /* no hierarchy rename */ +const eIMAPCapabilityFlag kACLCapability = 0x00000100; /* ACL extension */ +const eIMAPCapabilityFlag kNamespaceCapability = 0x00000200; /* IMAP4 Namespace Extension */ +const eIMAPCapabilityFlag kHasIDCapability = 0x00000400; /* client user agent id extension */ +const eIMAPCapabilityFlag kXServerInfoCapability = 0x00000800; /* XSERVERINFO extension for admin urls */ +const eIMAPCapabilityFlag kHasAuthPlainCapability = 0x00001000; /* new form of auth plain base64 login */ +const eIMAPCapabilityFlag kUidplusCapability = 0x00002000; /* RFC 2359 UIDPLUS extension */ +const eIMAPCapabilityFlag kLiteralPlusCapability = 0x00004000; /* RFC 2088 LITERAL+ extension */ +const eIMAPCapabilityFlag kAOLImapCapability = 0x00008000; /* aol imap extensions */ +const eIMAPCapabilityFlag kHasLanguageCapability = 0x00010000; /* language extensions */ +const eIMAPCapabilityFlag kHasCRAMCapability = 0x00020000; /* CRAM auth extension */ +const eIMAPCapabilityFlag kQuotaCapability = 0x00040000; /* RFC 2087 quota extension */ +const eIMAPCapabilityFlag kHasIdleCapability = 0x00080000; /* RFC 2177 idle extension */ +const eIMAPCapabilityFlag kHasAuthNTLMCapability = 0x00100000; /* AUTH NTLM extension */ +const eIMAPCapabilityFlag kHasAuthMSNCapability = 0x00200000; /* AUTH MSN extension */ +const eIMAPCapabilityFlag kHasStartTLSCapability = 0x00400000; /* STARTTLS support */ +const eIMAPCapabilityFlag kHasAuthNoneCapability = 0x00800000; /* needs no login */ +const eIMAPCapabilityFlag kHasAuthGssApiCapability = 0x01000000; /* GSSAPI AUTH */ +const eIMAPCapabilityFlag kHasCondStoreCapability = 0x02000000; /* RFC 3551 CondStore extension */ +const eIMAPCapabilityFlag kHasEnableCapability = 0x04000000; /* RFC 5161 ENABLE extension */ +const eIMAPCapabilityFlag kHasXListCapability = 0x08000000; /* XLIST extension */ +const eIMAPCapabilityFlag kHasCompressDeflateCapability = 0x10000000; /* RFC 4978 COMPRESS extension */ +const eIMAPCapabilityFlag kHasAuthExternalCapability = 0x20000000; /* RFC 2222 SASL AUTH EXTERNAL */ +const eIMAPCapabilityFlag kHasMoveCapability = 0x40000000; /* Proposed MOVE RFC */ +const eIMAPCapabilityFlag kHasHighestModSeqCapability = 0x80000000; /* Subset of RFC 3551 */ +// above are 32bit; below start the uint64_t bits 33-64 +const eIMAPCapabilityFlag kHasListExtendedCapability = 0x100000000LL; /* RFC 5258 */ +const eIMAPCapabilityFlag kHasSpecialUseCapability = 0x200000000LL; /* RFC 6154: Sent, Draft etc. folders */ +const eIMAPCapabilityFlag kGmailImapCapability = 0x400000000LL; /* X-GM-EXT-1 capability extension for gmail */ +const eIMAPCapabilityFlag kHasXOAuth2Capability = 0x800000000LL; /* AUTH XOAUTH2 extension */ +const eIMAPCapabilityFlag kHasClientIDCapability = 0x1000000000LL; /* ClientID capability */ +const eIMAPCapabilityFlag kHasUTF8AcceptCapability = 0x2000000000LL; /* RFC 6855: UTF8=ACCEPT */ + + +// this used to be part of the connection object class - maybe we should move it into +// something similar +typedef enum { + kEveryThingRFC822, + kEveryThingRFC822Peek, + kHeadersRFC822andUid, + kUid, + kFlags, + kRFC822Size, + kRFC822HeadersOnly, + kMIMEPart, + kMIMEHeader, + kBodyStart +} nsIMAPeFetchFields; + +typedef struct _utf_name_struct { + bool toUtf7Imap; + unsigned char *sourceString; + unsigned char *convertedString; +} utf_name_struct; + +typedef struct _ProgressInfo { + char16_t *message; + int32_t currentProgress; + int32_t maxProgress; +} ProgressInfo; + +typedef enum { + eContinue, + eContinueNew, + eListMyChildren, + eNewServerDirectory, + eCancelled +} EMailboxDiscoverStatus; + +typedef enum { + kInvalidateQuota, + kStoreQuota, + kValidateQuota +} nsImapQuotaAction; + +#endif +// clang-format on diff --git a/comm/mailnews/imap/src/nsImapFlagAndUidState.cpp b/comm/mailnews/imap/src/nsImapFlagAndUidState.cpp new file mode 100644 index 0000000000..cc3217d780 --- /dev/null +++ b/comm/mailnews/imap/src/nsImapFlagAndUidState.cpp @@ -0,0 +1,315 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" // for pre-compiled headers + +#include "nsImapCore.h" +#include "nsImapFlagAndUidState.h" +#include "nsMsgUtils.h" +#include "prcmon.h" +#include "nspr.h" + +NS_IMPL_ISUPPORTS(nsImapFlagAndUidState, nsIImapFlagAndUidState) + +using namespace mozilla; + +NS_IMETHODIMP nsImapFlagAndUidState::GetNumberOfMessages(int32_t* result) { + if (!result) return NS_ERROR_NULL_POINTER; + *result = fUids.Length(); + return NS_OK; +} + +NS_IMETHODIMP nsImapFlagAndUidState::GetUidOfMessage(int32_t zeroBasedIndex, + uint32_t* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + PR_CEnterMonitor(this); + *aResult = fUids.SafeElementAt(zeroBasedIndex, nsMsgKey_None); + PR_CExitMonitor(this); + return NS_OK; +} + +NS_IMETHODIMP +nsImapFlagAndUidState::HasMessage(uint32_t uid, bool* result) { + NS_ENSURE_ARG_POINTER(result); + *result = fUids.Contains(uid); + return NS_OK; +} + +NS_IMETHODIMP nsImapFlagAndUidState::GetMessageFlags(int32_t zeroBasedIndex, + uint16_t* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + *aResult = fFlags.SafeElementAt(zeroBasedIndex, kNoImapMsgFlag); + return NS_OK; +} + +NS_IMETHODIMP nsImapFlagAndUidState::SetMessageFlags(int32_t zeroBasedIndex, + unsigned short flags) { + if (zeroBasedIndex < (int32_t)fUids.Length()) fFlags[zeroBasedIndex] = flags; + return NS_OK; +} + +NS_IMETHODIMP nsImapFlagAndUidState::GetNumberOfRecentMessages( + int32_t* result) { + if (!result) return NS_ERROR_NULL_POINTER; + + PR_CEnterMonitor(this); + uint32_t counter = 0; + int32_t numUnseenMessages = 0; + + for (counter = 0; counter < fUids.Length(); counter++) { + if (fFlags[counter] & kImapMsgRecentFlag) numUnseenMessages++; + } + PR_CExitMonitor(this); + + *result = numUnseenMessages; + + return NS_OK; +} + +NS_IMETHODIMP nsImapFlagAndUidState::GetPartialUIDFetch( + bool* aPartialUIDFetch) { + NS_ENSURE_ARG_POINTER(aPartialUIDFetch); + *aPartialUIDFetch = fPartialUIDFetch; + return NS_OK; +} + +/* amount to expand for imap entry flags when we need more */ + +nsImapFlagAndUidState::nsImapFlagAndUidState(int32_t numberOfMessages) + : fUids(numberOfMessages), + fFlags(numberOfMessages), + m_customFlagsHash(10), + m_customAttributesHash(10), + mLock("nsImapFlagAndUidState.mLock") { + fSupportedUserFlags = 0; + fNumberDeleted = 0; + fPartialUIDFetch = true; + fStartCapture = false; + fNumAdded = 0; +} + +nsImapFlagAndUidState::~nsImapFlagAndUidState() {} + +NS_IMETHODIMP +nsImapFlagAndUidState::OrSupportedUserFlags(uint16_t flags) { + fSupportedUserFlags |= flags; + return NS_OK; +} + +NS_IMETHODIMP +nsImapFlagAndUidState::GetSupportedUserFlags(uint16_t* aFlags) { + NS_ENSURE_ARG_POINTER(aFlags); + *aFlags = fSupportedUserFlags; + return NS_OK; +} + +// we need to reset our flags, (re-read all) but chances are the memory +// allocation needed will be very close to what we were already using + +NS_IMETHODIMP nsImapFlagAndUidState::Reset() { + PR_CEnterMonitor(this); + fNumberDeleted = 0; + m_customFlagsHash.Clear(); + fUids.Clear(); + fFlags.Clear(); + fPartialUIDFetch = true; + fStartCapture = false; + fNumAdded = 0; + PR_CExitMonitor(this); + return NS_OK; +} + +// Remove (expunge) a message from our array, since now it is gone for good + +NS_IMETHODIMP nsImapFlagAndUidState::ExpungeByIndex(uint32_t msgIndex) { + // protect ourselves in case the server gave us an index key of -1 or 0 + if ((int32_t)msgIndex <= 0) return NS_ERROR_INVALID_ARG; + + if ((uint32_t)fUids.Length() < msgIndex) return NS_ERROR_INVALID_ARG; + + PR_CEnterMonitor(this); + msgIndex--; // msgIndex is 1-relative + if (fFlags[msgIndex] & + kImapMsgDeletedFlag) // see if we already had counted this one as deleted + fNumberDeleted--; + fUids.RemoveElementAt(msgIndex); + fFlags.RemoveElementAt(msgIndex); + PR_CExitMonitor(this); + return NS_OK; +} + +// adds to sorted list, protects against duplicates and going past array bounds. +NS_IMETHODIMP nsImapFlagAndUidState::AddUidFlagPair(uint32_t uid, + imapMessageFlagsType flags, + uint32_t zeroBasedIndex) { + if (uid == nsMsgKey_None) // ignore uid of -1 + return NS_OK; + // check for potential overflow in buffer size for uid array + if (zeroBasedIndex > 0x3FFFFFFF) return NS_ERROR_INVALID_ARG; + PR_CEnterMonitor(this); + // make sure there is room for this pair + if (zeroBasedIndex >= fUids.Length()) { + int32_t sizeToGrowBy = zeroBasedIndex - fUids.Length() + 1; + fUids.InsertElementsAt(fUids.Length(), sizeToGrowBy, 0); + fFlags.InsertElementsAt(fFlags.Length(), sizeToGrowBy, 0); + if (fStartCapture) { + // A new partial (CONDSTORE/CHANGEDSINCE) fetch response is occurring + // so need to start the count of number of uid/flag combos added. + fNumAdded = 0; + fStartCapture = false; + } + fNumAdded++; + } + + fUids[zeroBasedIndex] = uid; + fFlags[zeroBasedIndex] = flags; + if (flags & kImapMsgDeletedFlag) fNumberDeleted++; + PR_CExitMonitor(this); + return NS_OK; +} + +NS_IMETHODIMP nsImapFlagAndUidState::GetNumberOfDeletedMessages( + int32_t* numDeletedMessages) { + NS_ENSURE_ARG_POINTER(numDeletedMessages); + *numDeletedMessages = NumberOfDeletedMessages(); + return NS_OK; +} + +int32_t nsImapFlagAndUidState::NumberOfDeletedMessages() { + return fNumberDeleted; +} + +// since the uids are sorted, start from the back (rb) + +uint32_t nsImapFlagAndUidState::GetHighestNonDeletedUID() { + uint32_t msgIndex = fUids.Length(); + do { + if (msgIndex <= 0) return (0); + msgIndex--; + if (fUids[msgIndex] && !(fFlags[msgIndex] & kImapMsgDeletedFlag)) + return fUids[msgIndex]; + } while (msgIndex > 0); + return 0; +} + +// Has the user read the last message here ? Used when we first open the inbox +// to see if there really is new mail there. + +bool nsImapFlagAndUidState::IsLastMessageUnseen() { + uint32_t msgIndex = fUids.Length(); + + if (msgIndex <= 0) return false; + msgIndex--; + // if last message is deleted, it was probably filtered the last time around + if (fUids[msgIndex] && + (fFlags[msgIndex] & (kImapMsgSeenFlag | kImapMsgDeletedFlag))) + return false; + return true; +} + +// find a message flag given a key with non-recursive binary search, since some +// folders may have thousand of messages, once we find the key set its index, or +// the index of where the key should be inserted + +imapMessageFlagsType nsImapFlagAndUidState::GetMessageFlagsFromUID( + uint32_t uid, bool* foundIt, int32_t* ndx) { + PR_CEnterMonitor(this); + *ndx = (int32_t)fUids.IndexOfFirstElementGt(uid) - 1; + *foundIt = *ndx >= 0 && fUids[*ndx] == uid; + imapMessageFlagsType retFlags = (*foundIt) ? fFlags[*ndx] : kNoImapMsgFlag; + PR_CExitMonitor(this); + return retFlags; +} + +NS_IMETHODIMP +nsImapFlagAndUidState::GetMessageFlagsByUid(uint32_t uid, + imapMessageFlagsType* retFlags) { + PR_CEnterMonitor(this); + int32_t ndx = (int32_t)fUids.IndexOf(uid); + bool foundIt = ndx >= 0; + *retFlags = foundIt ? fFlags[ndx] : kNoImapMsgFlag; + PR_CExitMonitor(this); + return NS_OK; +} + +NS_IMETHODIMP nsImapFlagAndUidState::AddUidCustomFlagPair( + uint32_t uid, const char* customFlag) { + if (!customFlag) return NS_OK; + + MutexAutoLock mon(mLock); + nsCString ourCustomFlags; + nsCString oldValue; + if (m_customFlagsHash.Get(uid, &oldValue)) { + // We'll store multiple keys as space-delimited since space is not + // a valid character in a keyword. First, we need to look for the + // customFlag in the existing flags; + nsDependentCString customFlagString(customFlag); + int32_t existingCustomFlagPos = oldValue.Find(customFlagString); + uint32_t customFlagLen = customFlagString.Length(); + while (existingCustomFlagPos != kNotFound) { + // if existing flags ends with this exact flag, or flag + ' ' + // and the flag is at the beginning of the string or there is ' ' + flag + // then we have this flag already; + if (((oldValue.Length() == existingCustomFlagPos + customFlagLen) || + (oldValue.CharAt(existingCustomFlagPos + customFlagLen) == ' ')) && + ((existingCustomFlagPos == 0) || + (oldValue.CharAt(existingCustomFlagPos - 1) == ' '))) + return NS_OK; + // else, advance to next flag + existingCustomFlagPos = oldValue.Find( + customFlagString, existingCustomFlagPos + customFlagLen); + } + ourCustomFlags.Assign(oldValue); + ourCustomFlags.Append(' '); + ourCustomFlags.Append(customFlag); + m_customFlagsHash.Remove(uid); + } else { + ourCustomFlags.Assign(customFlag); + } + m_customFlagsHash.InsertOrUpdate(uid, ourCustomFlags); + return NS_OK; +} + +NS_IMETHODIMP nsImapFlagAndUidState::GetCustomFlags(uint32_t uid, + char** customFlags) { + MutexAutoLock mon(mLock); + nsCString value; + if (m_customFlagsHash.Get(uid, &value)) { + *customFlags = NS_xstrdup(value.get()); + return (*customFlags) ? NS_OK : NS_ERROR_FAILURE; + } + *customFlags = nullptr; + return NS_OK; +} + +NS_IMETHODIMP nsImapFlagAndUidState::ClearCustomFlags(uint32_t uid) { + MutexAutoLock mon(mLock); + m_customFlagsHash.Remove(uid); + return NS_OK; +} + +NS_IMETHODIMP nsImapFlagAndUidState::SetCustomAttribute( + uint32_t aUid, const nsACString& aCustomAttributeName, + const nsACString& aCustomAttributeValue) { + nsCString key; + key.AppendInt((int64_t)aUid); + key.Append(aCustomAttributeName); + nsCString value; + value.Assign(aCustomAttributeValue); + m_customAttributesHash.InsertOrUpdate(key, value); + return NS_OK; +} + +NS_IMETHODIMP nsImapFlagAndUidState::GetCustomAttribute( + uint32_t aUid, const nsACString& aCustomAttributeName, + nsACString& aCustomAttributeValue) { + nsCString key; + key.AppendInt((int64_t)aUid); + key.Append(aCustomAttributeName); + nsCString val = m_customAttributesHash.Get(key); + aCustomAttributeValue.Assign(val); + return NS_OK; +} diff --git a/comm/mailnews/imap/src/nsImapFlagAndUidState.h b/comm/mailnews/imap/src/nsImapFlagAndUidState.h new file mode 100644 index 0000000000..b8d2fa2b45 --- /dev/null +++ b/comm/mailnews/imap/src/nsImapFlagAndUidState.h @@ -0,0 +1,56 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsImapFlagAndUidState_h___ +#define nsImapFlagAndUidState_h___ + +#include "MailNewsTypes.h" +#include "nsIImapFlagAndUidState.h" +#include "nsImapCore.h" +#include "nsTArray.h" +#include "mozilla/Mutex.h" + +const int32_t kImapFlagAndUidStateSize = 100; + +#include "nsTHashMap.h" + +class nsImapFlagAndUidState : public nsIImapFlagAndUidState { + public: + NS_DECL_THREADSAFE_ISUPPORTS + explicit nsImapFlagAndUidState(int numberOfMessages); + + NS_DECL_NSIIMAPFLAGANDUIDSTATE + + int32_t NumberOfDeletedMessages(); + + imapMessageFlagsType GetMessageFlagsFromUID(uint32_t uid, bool* foundIt, + int32_t* ndx); + + bool IsLastMessageUnseen(void); + bool GetPartialUIDFetch() { return fPartialUIDFetch; } + void SetPartialUIDFetch(bool isPartial) { fPartialUIDFetch = isPartial; } + uint32_t GetHighestNonDeletedUID(); + uint16_t GetSupportedUserFlags() { return fSupportedUserFlags; } + void StartCapture() { fStartCapture = true; } + uint32_t GetNumAdded() { return fNumAdded; } + + private: + virtual ~nsImapFlagAndUidState(); + + nsTArray<nsMsgKey> fUids; + nsTArray<imapMessageFlagsType> fFlags; + // Hash table, mapping uids to extra flags + nsTHashMap<nsUint32HashKey, nsCString> m_customFlagsHash; + // Hash table, mapping UID+customAttributeName to customAttributeValue. + nsTHashMap<nsCStringHashKey, nsCString> m_customAttributesHash; + uint16_t fSupportedUserFlags; + int32_t fNumberDeleted; + bool fPartialUIDFetch; + uint32_t fNumAdded; + bool fStartCapture; + mozilla::Mutex mLock; +}; + +#endif diff --git a/comm/mailnews/imap/src/nsImapGenericParser.cpp b/comm/mailnews/imap/src/nsImapGenericParser.cpp new file mode 100644 index 0000000000..009c7c1e5a --- /dev/null +++ b/comm/mailnews/imap/src/nsImapGenericParser.cpp @@ -0,0 +1,407 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" // for pre-compiled headers + +#include "nsImapGenericParser.h" +#include "nsString.h" + +////////////////// nsImapGenericParser ///////////////////////// + +nsImapGenericParser::nsImapGenericParser() + : fNextToken(nullptr), + fCurrentLine(nullptr), + fLineOfTokens(nullptr), + fStartOfLineOfTokens(nullptr), + fCurrentTokenPlaceHolder(nullptr), + fAtEndOfLine(false), + fParserState(stateOK) {} + +nsImapGenericParser::~nsImapGenericParser() { + PR_FREEIF(fCurrentLine); + PR_FREEIF(fStartOfLineOfTokens); +} + +void nsImapGenericParser::HandleMemoryFailure() { SetConnected(false); } + +void nsImapGenericParser::ResetLexAnalyzer() { + PR_FREEIF(fCurrentLine); + PR_FREEIF(fStartOfLineOfTokens); + + fNextToken = fCurrentLine = fLineOfTokens = fStartOfLineOfTokens = + fCurrentTokenPlaceHolder = nullptr; + fAtEndOfLine = false; +} + +bool nsImapGenericParser::LastCommandSuccessful() { + return fParserState == stateOK; +} + +void nsImapGenericParser::SetSyntaxError(bool error, const char* msg) { + if (error) + fParserState |= stateSyntaxErrorFlag; + else + fParserState &= ~stateSyntaxErrorFlag; + NS_ASSERTION(!error, "syntax error in generic parser"); +} + +void nsImapGenericParser::SetConnected(bool connected) { + if (connected) + fParserState &= ~stateDisconnectedFlag; + else + fParserState |= stateDisconnectedFlag; +} + +void nsImapGenericParser::skip_to_CRLF() { + while (Connected() && !fAtEndOfLine) AdvanceToNextToken(); +} + +// fNextToken initially should point to +// a string after the initial open paren ("(") +// After this call, fNextToken points to the +// first character after the matching close +// paren. Only call AdvanceToNextToken() to get the NEXT +// token after the one returned in fNextToken. +void nsImapGenericParser::skip_to_close_paren() { + int numberOfCloseParensNeeded = 1; + while (ContinueParse()) { + // go through fNextToken, account for nested parens + const char* loc; + for (loc = fNextToken; loc && *loc; loc++) { + if (*loc == '(') + numberOfCloseParensNeeded++; + else if (*loc == ')') { + numberOfCloseParensNeeded--; + if (numberOfCloseParensNeeded == 0) { + fNextToken = loc + 1; + if (!fNextToken || !*fNextToken) AdvanceToNextToken(); + return; + } + } else if (*loc == '{' || *loc == '"') { + // quoted or literal + fNextToken = loc; + char* a = CreateString(); + PR_FREEIF(a); + break; // move to next token + } + } + if (ContinueParse()) AdvanceToNextToken(); + } +} + +void nsImapGenericParser::AdvanceToNextToken() { + if (!fCurrentLine || fAtEndOfLine) AdvanceToNextLine(); + if (Connected()) { + if (!fStartOfLineOfTokens) { + // this is the first token of the line; setup tokenizer now + fStartOfLineOfTokens = PL_strdup(fCurrentLine); + if (!fStartOfLineOfTokens) { + HandleMemoryFailure(); + return; + } + fLineOfTokens = fStartOfLineOfTokens; + fCurrentTokenPlaceHolder = fStartOfLineOfTokens; + } + fNextToken = NS_strtok(WHITESPACE, &fCurrentTokenPlaceHolder); + if (!fNextToken) { + fAtEndOfLine = true; + fNextToken = CRLF; + } + } +} + +void nsImapGenericParser::AdvanceToNextLine() { + PR_FREEIF(fCurrentLine); + PR_FREEIF(fStartOfLineOfTokens); + + bool ok = GetNextLineForParser(&fCurrentLine); + if (!ok) { + SetConnected(false); + fStartOfLineOfTokens = nullptr; + fLineOfTokens = nullptr; + fCurrentTokenPlaceHolder = nullptr; + fAtEndOfLine = true; + fNextToken = CRLF; + } else if (!fCurrentLine) { + HandleMemoryFailure(); + } else { + fNextToken = nullptr; + // determine if there are any tokens (without calling AdvanceToNextToken); + // otherwise we are already at end of line + NS_ASSERTION(strlen(WHITESPACE) == 3, "assume 3 chars of whitespace"); + char* firstToken = fCurrentLine; + while (*firstToken && + (*firstToken == WHITESPACE[0] || *firstToken == WHITESPACE[1] || + *firstToken == WHITESPACE[2])) + firstToken++; + fAtEndOfLine = (*firstToken == '\0'); + } +} + +// advances |fLineOfTokens| by |bytesToAdvance| bytes +void nsImapGenericParser::AdvanceTokenizerStartingPoint( + int32_t bytesToAdvance) { + NS_ASSERTION(bytesToAdvance >= 0, "bytesToAdvance must not be negative"); + if (!fStartOfLineOfTokens) { + AdvanceToNextToken(); // the tokenizer was not yet initialized, do it now + if (!fStartOfLineOfTokens) return; + } + + if (!fStartOfLineOfTokens) return; + // The last call to AdvanceToNextToken() cleared the token separator to '\0' + // iff |fCurrentTokenPlaceHolder|. We must recover this token separator now. + if (fCurrentTokenPlaceHolder) { + int endTokenOffset = fCurrentTokenPlaceHolder - fStartOfLineOfTokens - 1; + if (endTokenOffset >= 0) + fStartOfLineOfTokens[endTokenOffset] = fCurrentLine[endTokenOffset]; + } + + NS_ASSERTION(bytesToAdvance + (fLineOfTokens - fStartOfLineOfTokens) <= + (int32_t)strlen(fCurrentLine), + "cannot advance beyond end of fLineOfTokens"); + fLineOfTokens += bytesToAdvance; + fCurrentTokenPlaceHolder = fLineOfTokens; +} + +// RFC3501: astring = 1*ASTRING-CHAR / string +// string = quoted / literal +// This function leaves us off with fCurrentTokenPlaceHolder immediately after +// the end of the Astring. Call AdvanceToNextToken() to get the token after it. +char* nsImapGenericParser::CreateAstring() { + if (*fNextToken == '{') return CreateLiteral(); // literal + if (*fNextToken == '"') return CreateQuoted(); // quoted + return CreateAtom(true); // atom +} + +// Create an atom +// This function does not advance the parser. +// Call AdvanceToNextToken() to get the next token after the atom. +// RFC3501: atom = 1*ATOM-CHAR +// ASTRING-CHAR = ATOM-CHAR / resp-specials +// ATOM-CHAR = <any CHAR except atom-specials> +// atom-specials = "(" / ")" / "{" / SP / CTL / list-wildcards / +// quoted-specials / resp-specials +// list-wildcards = "%" / "*" +// quoted-specials = DQUOTE / "\" +// resp-specials = "]" +// "Characters are 7-bit US-ASCII unless otherwise specified." [RFC3501, 1.2.] +char* nsImapGenericParser::CreateAtom(bool isAstring) { + char* rv = PL_strdup(fNextToken); + if (!rv) { + HandleMemoryFailure(); + return nullptr; + } + // We wish to stop at the following characters (in decimal ascii) + // 1-31 (CTL), 32 (SP), 34 '"', 37 '%', 40-42 "()*", 92 '\\', 123 '{' + // also, ']' is only allowed in astrings + char* last = rv; + char c = *last; + while ((c > 42 || c == 33 || c == 35 || c == 36 || c == 38 || c == 39) && + c != '\\' && c != '{' && (isAstring || c != ']')) + c = *++last; + if (rv == last) { + SetSyntaxError(true, "no atom characters found"); + PL_strfree(rv); + return nullptr; + } + if (*last) { + // not the whole token was consumed + *last = '\0'; + AdvanceTokenizerStartingPoint((fNextToken - fLineOfTokens) + (last - rv)); + } + return rv; +} + +// CreateNilString return either NULL (for "NIL") or a string +// Call with fNextToken pointing to the thing which we think is the nilstring. +// This function leaves us off with fCurrentTokenPlaceHolder immediately after +// the end of the string. +// Regardless of type, call AdvanceToNextToken() to get the token after it. +// RFC3501: nstring = string / nil +// nil = "NIL" +char* nsImapGenericParser::CreateNilString() { + if (!PL_strncasecmp(fNextToken, "NIL", 3)) { + // check if there is text after "NIL" in fNextToken, + // equivalent handling as in CreateQuoted + if (fNextToken[3]) + AdvanceTokenizerStartingPoint((fNextToken - fLineOfTokens) + 3); + return NULL; + } + return CreateString(); +} + +// Create a string, which can either be quoted or literal, +// but not an atom. +// This function leaves us off with fCurrentTokenPlaceHolder immediately after +// the end of the String. Call AdvanceToNextToken() to get the token after it. +char* nsImapGenericParser::CreateString() { + if (*fNextToken == '{') { + char* rv = CreateLiteral(); // literal + return (rv); + } + if (*fNextToken == '"') { + char* rv = CreateQuoted(); // quoted + return (rv); + } + SetSyntaxError(true, "string does not start with '{' or '\"'"); + return NULL; +} + +// This function sets fCurrentTokenPlaceHolder immediately after the end of the +// closing quote. Call AdvanceToNextToken() to get the token after it. +// QUOTED_CHAR ::= <any TEXT_CHAR except quoted_specials> / +// "\" quoted_specials +// TEXT_CHAR ::= <any CHAR except CR and LF> +// quoted_specials ::= <"> / "\" +// Note that according to RFC 1064 and RFC 2060, CRs and LFs are not allowed +// inside a quoted string. It is sufficient to read from the current line only. +char* nsImapGenericParser::CreateQuoted(bool /*skipToEnd*/) { + // one char past opening '"' + char* currentChar = fCurrentLine + (fNextToken - fStartOfLineOfTokens) + 1; + + int escapeCharsCut = 0; + nsCString returnString(currentChar); + int charIndex; + for (charIndex = 0; returnString.CharAt(charIndex) != '"'; charIndex++) { + if (!returnString.CharAt(charIndex)) { + SetSyntaxError(true, "no closing '\"' found in quoted"); + return nullptr; + } + if (returnString.CharAt(charIndex) == '\\') { + // eat the escape character, but keep the escaped character + returnString.Cut(charIndex, 1); + escapeCharsCut++; + } + } + // +2 because of the start and end quotes + AdvanceTokenizerStartingPoint((fNextToken - fLineOfTokens) + charIndex + + escapeCharsCut + 2); + + returnString.SetLength(charIndex); + return ToNewCString(returnString); +} + +// This function leaves us off with fCurrentTokenPlaceHolder immediately after +// the end of the literal string. Call AdvanceToNextToken() to get the token +// after the literal string. +// RFC3501: literal = "{" number "}" CRLF *CHAR8 +// ; Number represents the number of CHAR8s +// CHAR8 = %x01-ff +// ; any OCTET except NUL, %x00 +char* nsImapGenericParser::CreateLiteral() { + int32_t numberOfCharsInMessage = atoi(fNextToken + 1); + uint32_t numBytes = numberOfCharsInMessage + 1; + NS_ASSERTION(numBytes, "overflow!"); + if (!numBytes) return nullptr; + char* returnString = (char*)PR_Malloc(numBytes); + if (!returnString) { + HandleMemoryFailure(); + return nullptr; + } + + int32_t currentLineLength = 0; + int32_t charsReadSoFar = 0; + int32_t bytesToCopy = 0; + while (charsReadSoFar < numberOfCharsInMessage) { + AdvanceToNextLine(); + if (!ContinueParse()) break; + + currentLineLength = strlen(fCurrentLine); + bytesToCopy = (currentLineLength > numberOfCharsInMessage - charsReadSoFar + ? numberOfCharsInMessage - charsReadSoFar + : currentLineLength); + NS_ASSERTION(bytesToCopy, "zero-length line?"); + memcpy(returnString + charsReadSoFar, fCurrentLine, bytesToCopy); + charsReadSoFar += bytesToCopy; + } + + if (ContinueParse()) { + if (currentLineLength == bytesToCopy) { + // We have consumed the entire line. + // Consider the input "{4}\r\n" "L1\r\n" " A2\r\n" which is read + // line-by-line. Reading an Astring, this should result in "L1\r\n". + // Note that the second line is "L1\r\n", where the "\r\n" is part of + // the literal. Hence, we now read the next line to ensure that the + // next call to AdvanceToNextToken() leads to fNextToken=="A2" in our + // example. + AdvanceToNextLine(); + } else + AdvanceTokenizerStartingPoint(bytesToCopy); + } + + returnString[charsReadSoFar] = 0; + return returnString; +} + +// Call this to create a buffer containing all characters within +// a given set of parentheses. +// Call this with fNextToken[0]=='(', that is, the open paren +// of the group. +// It will allocate and return all characters up to and including the +// corresponding closing paren, and leave the parser in the right place +// afterwards. +char* nsImapGenericParser::CreateParenGroup() { + NS_ASSERTION(fNextToken[0] == '(', "we don't have a paren group!"); + + int numOpenParens = 0; + AdvanceTokenizerStartingPoint(fNextToken - fLineOfTokens); + + // Build up a buffer containing the paren group. + nsCString returnString; + char* parenGroupStart = fCurrentTokenPlaceHolder; + NS_ASSERTION(parenGroupStart[0] == '(', "we don't have a paren group (2)!"); + while (*fCurrentTokenPlaceHolder) { + if (*fCurrentTokenPlaceHolder == '{') // literal + { + // Ensure it is a properly formatted literal. + NS_ASSERTION(!strcmp("}\r\n", fCurrentTokenPlaceHolder + + strlen(fCurrentTokenPlaceHolder) - 3), + "not a literal"); + + // Append previous characters and the "{xx}\r\n" to buffer. + returnString.Append(parenGroupStart); + + // Append literal itself. + AdvanceToNextToken(); + if (!ContinueParse()) break; + char* lit = CreateLiteral(); + NS_ASSERTION(lit, "syntax error or out of memory"); + if (!lit) break; + returnString.Append(lit); + PR_Free(lit); + if (!ContinueParse()) break; + parenGroupStart = fCurrentTokenPlaceHolder; + } else if (*fCurrentTokenPlaceHolder == '"') // quoted + { + // Append the _escaped_ version of the quoted string: + // just skip it (because the quoted string must be on the same line). + AdvanceToNextToken(); + if (!ContinueParse()) break; + char* q = CreateQuoted(); + if (!q) break; + PR_Free(q); + if (!ContinueParse()) break; + } else { + // Append this character to the buffer. + char c = *fCurrentTokenPlaceHolder++; + if (c == '(') + numOpenParens++; + else if (c == ')') { + numOpenParens--; + if (numOpenParens == 0) break; + } + } + } + + if (numOpenParens != 0 || !ContinueParse()) { + SetSyntaxError(true, "closing ')' not found in paren group"); + return nullptr; + } + + returnString.Append(parenGroupStart, + fCurrentTokenPlaceHolder - parenGroupStart); + AdvanceToNextToken(); + return ToNewCString(returnString); +} diff --git a/comm/mailnews/imap/src/nsImapGenericParser.h b/comm/mailnews/imap/src/nsImapGenericParser.h new file mode 100644 index 0000000000..50abffa5e1 --- /dev/null +++ b/comm/mailnews/imap/src/nsImapGenericParser.h @@ -0,0 +1,74 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* +nsImapGenericParser is the base parser class used by the server parser and body +shell parser +*/ + +#ifndef nsImapGenericParser_H +#define nsImapGenericParser_H + +#define WHITESPACE " \015\012" // token delimiter + +class nsImapGenericParser { + public: + nsImapGenericParser(); + virtual ~nsImapGenericParser(); + + // Add any specific stuff in the derived class + virtual bool LastCommandSuccessful(); + + bool SyntaxError() { return (fParserState & stateSyntaxErrorFlag) != 0; } + bool ContinueParse() { return fParserState == stateOK; } + bool Connected() { return !(fParserState & stateDisconnectedFlag); } + void SetConnected(bool error); + + protected: + // This is a pure virtual member which must be overridden in the derived class + // for each different implementation of a nsImapGenericParser. + // For instance, one implementation (the nsIMAPServerState) might get the next + // line from an open socket, whereas another implementation might just get it + // from a buffer somewhere. This fills in nextLine with the buffer, and + // returns true if everything is OK. Returns false if there was some error + // encountered. In that case, we reset the parser. + virtual bool GetNextLineForParser(char** nextLine) = 0; + + virtual void HandleMemoryFailure(); + void skip_to_CRLF(); + void skip_to_close_paren(); + char* CreateString(); + char* CreateAstring(); + char* CreateNilString(); + char* CreateLiteral(); + char* CreateAtom(bool isAstring = false); + char* CreateQuoted(bool skipToEnd = true); + char* CreateParenGroup(); + virtual void SetSyntaxError(bool error, const char* msg); + + void AdvanceToNextToken(); + void AdvanceToNextLine(); + void AdvanceTokenizerStartingPoint(int32_t bytesToAdvance); + void ResetLexAnalyzer(); + + protected: + // use with care + const char* fNextToken; + char* fCurrentLine; + char* fLineOfTokens; + char* fStartOfLineOfTokens; + char* fCurrentTokenPlaceHolder; + bool fAtEndOfLine; + + private: + enum nsImapGenericParserState { + stateOK = 0, + stateSyntaxErrorFlag = 0x1, + stateDisconnectedFlag = 0x2 + }; + uint32_t fParserState; +}; + +#endif diff --git a/comm/mailnews/imap/src/nsImapHostSessionList.cpp b/comm/mailnews/imap/src/nsImapHostSessionList.cpp new file mode 100644 index 0000000000..fbdbeac231 --- /dev/null +++ b/comm/mailnews/imap/src/nsImapHostSessionList.cpp @@ -0,0 +1,595 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" +#include "nsImapHostSessionList.h" +#include "nsImapNamespace.h" +#include "nsIImapIncomingServer.h" +#include "nsCOMPtr.h" +#include "nsIMsgIncomingServer.h" +#include "nsIObserverService.h" +#include "nsServiceManagerUtils.h" +#include "nsMsgUtils.h" +#include "mozilla/Services.h" + +nsIMAPHostInfo::nsIMAPHostInfo(const char* serverKey, + nsIImapIncomingServer* server) { + fServerKey = serverKey; + NS_ASSERTION(server, "*** Fatal null imap incoming server..."); + server->GetServerDirectory(fOnlineDir); + fNextHost = NULL; + fCapabilityFlags = kCapabilityUndefined; + fHierarchyDelimiters = NULL; +#ifdef DEBUG_bienvenu1 + fHaveWeEverDiscoveredFolders = + true; // try this, see what bad happens - we'll need to + // figure out a way to make new accounts have it be false +#else + fHaveWeEverDiscoveredFolders = false; // try this, see what bad happens +#endif + fDiscoveryForHostInProgress = false; + fCanonicalOnlineSubDir = NULL; + fNamespaceList = nsImapNamespaceList::CreatensImapNamespaceList(); + fUsingSubscription = true; + server->GetUsingSubscription(&fUsingSubscription); + fOnlineTrashFolderExists = false; + fShouldAlwaysListInbox = true; + fPasswordVerifiedOnline = false; + fDeleteIsMoveToTrash = true; + fShowDeletedMessages = false; + fGotNamespaces = false; + fHaveAdminURL = false; + fNamespacesOverridable = true; + server->GetOverrideNamespaces(&fNamespacesOverridable); + fTempNamespaceList = nsImapNamespaceList::CreatensImapNamespaceList(); +} + +nsIMAPHostInfo::~nsIMAPHostInfo() { + PR_Free(fHierarchyDelimiters); + delete fNamespaceList; + delete fTempNamespaceList; +} + +NS_IMPL_ISUPPORTS(nsImapHostSessionList, nsIImapHostSessionList, nsIObserver, + nsISupportsWeakReference) + +nsImapHostSessionList::nsImapHostSessionList() { + gCachedHostInfoMonitor = PR_NewMonitor(/* "accessing-hostlist-monitor"*/); + fHostInfoList = nullptr; +} + +nsImapHostSessionList::~nsImapHostSessionList() { + ResetAll(); + PR_DestroyMonitor(gCachedHostInfoMonitor); +} + +nsresult nsImapHostSessionList::Init() { + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + NS_ENSURE_TRUE(observerService, NS_ERROR_UNEXPECTED); + observerService->AddObserver(this, "profile-before-change", true); + observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, true); + return NS_OK; +} + +NS_IMETHODIMP nsImapHostSessionList::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* someData) { + if (!strcmp(aTopic, "profile-before-change")) + ResetAll(); + else if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + NS_ENSURE_TRUE(observerService, NS_ERROR_UNEXPECTED); + observerService->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); + observerService->RemoveObserver(this, "profile-before-change"); + } + return NS_OK; +} + +nsIMAPHostInfo* nsImapHostSessionList::FindHost(const char* serverKey) { + nsIMAPHostInfo* host; + + // ### should also check userName here, if NON NULL + for (host = fHostInfoList; host; host = host->fNextHost) { + if (host->fServerKey.Equals(serverKey, nsCaseInsensitiveCStringComparator)) + return host; + } + return host; +} + +// reset any cached connection info - delete the lot of 'em +NS_IMETHODIMP nsImapHostSessionList::ResetAll() { + PR_EnterMonitor(gCachedHostInfoMonitor); + nsIMAPHostInfo* nextHost = NULL; + for (nsIMAPHostInfo* host = fHostInfoList; host; host = nextHost) { + nextHost = host->fNextHost; + delete host; + } + fHostInfoList = NULL; + PR_ExitMonitor(gCachedHostInfoMonitor); + return NS_OK; +} + +NS_IMETHODIMP +nsImapHostSessionList::AddHostToList(const char* serverKey, + nsIImapIncomingServer* server) { + nsIMAPHostInfo* newHost = NULL; + PR_EnterMonitor(gCachedHostInfoMonitor); + if (!FindHost(serverKey)) { + // stick it on the front + newHost = new nsIMAPHostInfo(serverKey, server); + if (newHost) { + newHost->fNextHost = fHostInfoList; + fHostInfoList = newHost; + } + } + PR_ExitMonitor(gCachedHostInfoMonitor); + return (newHost == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK; +} + +NS_IMETHODIMP nsImapHostSessionList::GetPasswordForHost(const char* serverKey, + nsString& result) { + PR_EnterMonitor(gCachedHostInfoMonitor); + nsIMAPHostInfo* host = FindHost(serverKey); + if (host) result = host->fCachedPassword; + PR_ExitMonitor(gCachedHostInfoMonitor); + return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK; +} + +NS_IMETHODIMP nsImapHostSessionList::SetPasswordForHost( + const char* serverKey, const nsAString& password) { + PR_EnterMonitor(gCachedHostInfoMonitor); + nsIMAPHostInfo* host = FindHost(serverKey); + if (host) host->fCachedPassword = password; + PR_ExitMonitor(gCachedHostInfoMonitor); + return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK; +} + +NS_IMETHODIMP nsImapHostSessionList::SetPasswordVerifiedOnline( + const char* serverKey) { + PR_EnterMonitor(gCachedHostInfoMonitor); + nsIMAPHostInfo* host = FindHost(serverKey); + if (host) host->fPasswordVerifiedOnline = true; + PR_ExitMonitor(gCachedHostInfoMonitor); + return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK; +} + +NS_IMETHODIMP nsImapHostSessionList::GetPasswordVerifiedOnline( + const char* serverKey, bool& result) { + PR_EnterMonitor(gCachedHostInfoMonitor); + nsIMAPHostInfo* host = FindHost(serverKey); + if (host) result = host->fPasswordVerifiedOnline; + PR_ExitMonitor(gCachedHostInfoMonitor); + return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK; +} + +NS_IMETHODIMP nsImapHostSessionList::GetOnlineDirForHost(const char* serverKey, + nsString& result) { + PR_EnterMonitor(gCachedHostInfoMonitor); + nsIMAPHostInfo* host = FindHost(serverKey); + if (host) CopyASCIItoUTF16(host->fOnlineDir, result); + PR_ExitMonitor(gCachedHostInfoMonitor); + return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK; +} + +NS_IMETHODIMP nsImapHostSessionList::SetOnlineDirForHost( + const char* serverKey, const char* onlineDir) { + PR_EnterMonitor(gCachedHostInfoMonitor); + nsIMAPHostInfo* host = FindHost(serverKey); + if (host) { + if (onlineDir) host->fOnlineDir = onlineDir; + } + PR_ExitMonitor(gCachedHostInfoMonitor); + return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK; +} + +NS_IMETHODIMP nsImapHostSessionList::GetDeleteIsMoveToTrashForHost( + const char* serverKey, bool& result) { + PR_EnterMonitor(gCachedHostInfoMonitor); + nsIMAPHostInfo* host = FindHost(serverKey); + if (host) result = host->fDeleteIsMoveToTrash; + PR_ExitMonitor(gCachedHostInfoMonitor); + return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK; +} + +NS_IMETHODIMP nsImapHostSessionList::GetShowDeletedMessagesForHost( + const char* serverKey, bool& result) { + PR_EnterMonitor(gCachedHostInfoMonitor); + nsIMAPHostInfo* host = FindHost(serverKey); + if (host) result = host->fShowDeletedMessages; + PR_ExitMonitor(gCachedHostInfoMonitor); + return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK; +} + +NS_IMETHODIMP nsImapHostSessionList::SetDeleteIsMoveToTrashForHost( + const char* serverKey, bool isMoveToTrash) { + PR_EnterMonitor(gCachedHostInfoMonitor); + nsIMAPHostInfo* host = FindHost(serverKey); + if (host) host->fDeleteIsMoveToTrash = isMoveToTrash; + PR_ExitMonitor(gCachedHostInfoMonitor); + return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK; +} + +NS_IMETHODIMP nsImapHostSessionList::SetShowDeletedMessagesForHost( + const char* serverKey, bool showDeletedMessages) { + PR_EnterMonitor(gCachedHostInfoMonitor); + nsIMAPHostInfo* host = FindHost(serverKey); + if (host) host->fShowDeletedMessages = showDeletedMessages; + PR_ExitMonitor(gCachedHostInfoMonitor); + return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK; +} + +NS_IMETHODIMP nsImapHostSessionList::GetGotNamespacesForHost( + const char* serverKey, bool& result) { + PR_EnterMonitor(gCachedHostInfoMonitor); + nsIMAPHostInfo* host = FindHost(serverKey); + if (host) result = host->fGotNamespaces; + PR_ExitMonitor(gCachedHostInfoMonitor); + return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK; +} + +NS_IMETHODIMP nsImapHostSessionList::SetGotNamespacesForHost( + const char* serverKey, bool gotNamespaces) { + PR_EnterMonitor(gCachedHostInfoMonitor); + nsIMAPHostInfo* host = FindHost(serverKey); + if (host) host->fGotNamespaces = gotNamespaces; + PR_ExitMonitor(gCachedHostInfoMonitor); + return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK; +} + +NS_IMETHODIMP nsImapHostSessionList::GetHostIsUsingSubscription( + const char* serverKey, bool& result) { + PR_EnterMonitor(gCachedHostInfoMonitor); + nsIMAPHostInfo* host = FindHost(serverKey); + if (host) result = host->fUsingSubscription; + PR_ExitMonitor(gCachedHostInfoMonitor); + return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK; +} + +NS_IMETHODIMP nsImapHostSessionList::SetHostIsUsingSubscription( + const char* serverKey, bool usingSubscription) { + PR_EnterMonitor(gCachedHostInfoMonitor); + nsIMAPHostInfo* host = FindHost(serverKey); + if (host) host->fUsingSubscription = usingSubscription; + PR_ExitMonitor(gCachedHostInfoMonitor); + return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK; +} + +NS_IMETHODIMP nsImapHostSessionList::GetHostHasAdminURL(const char* serverKey, + bool& result) { + PR_EnterMonitor(gCachedHostInfoMonitor); + nsIMAPHostInfo* host = FindHost(serverKey); + if (host) result = host->fHaveAdminURL; + PR_ExitMonitor(gCachedHostInfoMonitor); + return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK; +} + +NS_IMETHODIMP nsImapHostSessionList::SetHostHasAdminURL(const char* serverKey, + bool haveAdminURL) { + PR_EnterMonitor(gCachedHostInfoMonitor); + nsIMAPHostInfo* host = FindHost(serverKey); + if (host) host->fHaveAdminURL = haveAdminURL; + PR_ExitMonitor(gCachedHostInfoMonitor); + return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK; +} + +NS_IMETHODIMP nsImapHostSessionList::GetHaveWeEverDiscoveredFoldersForHost( + const char* serverKey, bool& result) { + PR_EnterMonitor(gCachedHostInfoMonitor); + nsIMAPHostInfo* host = FindHost(serverKey); + if (host) result = host->fHaveWeEverDiscoveredFolders; + PR_ExitMonitor(gCachedHostInfoMonitor); + return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK; +} + +NS_IMETHODIMP nsImapHostSessionList::SetHaveWeEverDiscoveredFoldersForHost( + const char* serverKey, bool discovered) { + PR_EnterMonitor(gCachedHostInfoMonitor); + nsIMAPHostInfo* host = FindHost(serverKey); + if (host) host->fHaveWeEverDiscoveredFolders = discovered; + PR_ExitMonitor(gCachedHostInfoMonitor); + return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK; +} + +NS_IMETHODIMP nsImapHostSessionList::GetDiscoveryForHostInProgress( + const char* serverKey, bool& result) { + PR_EnterMonitor(gCachedHostInfoMonitor); + nsIMAPHostInfo* host = FindHost(serverKey); + if (host) + result = host->fDiscoveryForHostInProgress; + else + result = false; + PR_ExitMonitor(gCachedHostInfoMonitor); + return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK; +} + +NS_IMETHODIMP nsImapHostSessionList::SetDiscoveryForHostInProgress( + const char* serverKey, bool inProgress) { + PR_EnterMonitor(gCachedHostInfoMonitor); + nsIMAPHostInfo* host = FindHost(serverKey); + if (host) host->fDiscoveryForHostInProgress = inProgress; + PR_ExitMonitor(gCachedHostInfoMonitor); + return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK; +} + +NS_IMETHODIMP nsImapHostSessionList::SetOnlineTrashFolderExistsForHost( + const char* serverKey, bool exists) { + PR_EnterMonitor(gCachedHostInfoMonitor); + nsIMAPHostInfo* host = FindHost(serverKey); + if (host) host->fOnlineTrashFolderExists = exists; + PR_ExitMonitor(gCachedHostInfoMonitor); + return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK; +} + +NS_IMETHODIMP nsImapHostSessionList::GetOnlineTrashFolderExistsForHost( + const char* serverKey, bool& result) { + PR_EnterMonitor(gCachedHostInfoMonitor); + nsIMAPHostInfo* host = FindHost(serverKey); + if (host) result = host->fOnlineTrashFolderExists; + PR_ExitMonitor(gCachedHostInfoMonitor); + return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK; +} + +NS_IMETHODIMP nsImapHostSessionList::AddNewNamespaceForHost( + const char* serverKey, nsImapNamespace* ns) { + PR_EnterMonitor(gCachedHostInfoMonitor); + nsIMAPHostInfo* host = FindHost(serverKey); + if (host) host->fNamespaceList->AddNewNamespace(ns); + PR_ExitMonitor(gCachedHostInfoMonitor); + return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK; +} + +NS_IMETHODIMP nsImapHostSessionList::SetNamespaceFromPrefForHost( + const char* serverKey, const char* namespacePref, + EIMAPNamespaceType nstype) { + PR_EnterMonitor(gCachedHostInfoMonitor); + nsIMAPHostInfo* host = FindHost(serverKey); + if (host) { + if (namespacePref) { + int numNamespaces = host->fNamespaceList->UnserializeNamespaces( + namespacePref, nullptr, 0); + char** prefixes = (char**)PR_CALLOC(numNamespaces * sizeof(char*)); + if (prefixes) { + int len = host->fNamespaceList->UnserializeNamespaces( + namespacePref, prefixes, numNamespaces); + for (int i = 0; i < len; i++) { + char* thisns = prefixes[i]; + char delimiter = '/'; // a guess + if (PL_strlen(thisns) >= 1) delimiter = thisns[PL_strlen(thisns) - 1]; + nsImapNamespace* ns = + new nsImapNamespace(nstype, thisns, delimiter, true); + if (ns) host->fNamespaceList->AddNewNamespace(ns); + PR_FREEIF(thisns); + } + PR_Free(prefixes); + } + } + } + PR_ExitMonitor(gCachedHostInfoMonitor); + return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK; +} + +NS_IMETHODIMP nsImapHostSessionList::GetNamespaceForMailboxForHost( + const char* serverKey, const char* mailbox_name, nsImapNamespace*& result) { + PR_EnterMonitor(gCachedHostInfoMonitor); + nsIMAPHostInfo* host = FindHost(serverKey); + if (host) result = host->fNamespaceList->GetNamespaceForMailbox(mailbox_name); + PR_ExitMonitor(gCachedHostInfoMonitor); + return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK; +} + +NS_IMETHODIMP nsImapHostSessionList::ClearPrefsNamespacesForHost( + const char* serverKey) { + PR_EnterMonitor(gCachedHostInfoMonitor); + nsIMAPHostInfo* host = FindHost(serverKey); + if (host) host->fNamespaceList->ClearNamespaces(true, false, true); + PR_ExitMonitor(gCachedHostInfoMonitor); + return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK; +} + +NS_IMETHODIMP nsImapHostSessionList::ClearServerAdvertisedNamespacesForHost( + const char* serverKey) { + PR_EnterMonitor(gCachedHostInfoMonitor); + nsIMAPHostInfo* host = FindHost(serverKey); + if (host) host->fNamespaceList->ClearNamespaces(false, true, true); + PR_ExitMonitor(gCachedHostInfoMonitor); + return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK; +} + +NS_IMETHODIMP nsImapHostSessionList::GetDefaultNamespaceOfTypeForHost( + const char* serverKey, EIMAPNamespaceType type, nsImapNamespace*& result) { + PR_EnterMonitor(gCachedHostInfoMonitor); + nsIMAPHostInfo* host = FindHost(serverKey); + if (host) result = host->fNamespaceList->GetDefaultNamespaceOfType(type); + PR_ExitMonitor(gCachedHostInfoMonitor); + return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK; +} + +NS_IMETHODIMP nsImapHostSessionList::GetNamespacesOverridableForHost( + const char* serverKey, bool& result) { + PR_EnterMonitor(gCachedHostInfoMonitor); + nsIMAPHostInfo* host = FindHost(serverKey); + if (host) result = host->fNamespacesOverridable; + PR_ExitMonitor(gCachedHostInfoMonitor); + return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK; +} + +NS_IMETHODIMP nsImapHostSessionList::SetNamespacesOverridableForHost( + const char* serverKey, bool overridable) { + PR_EnterMonitor(gCachedHostInfoMonitor); + nsIMAPHostInfo* host = FindHost(serverKey); + if (host) host->fNamespacesOverridable = overridable; + PR_ExitMonitor(gCachedHostInfoMonitor); + return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK; +} + +NS_IMETHODIMP nsImapHostSessionList::GetNumberOfNamespacesForHost( + const char* serverKey, uint32_t& result) { + int32_t intResult = 0; + + PR_EnterMonitor(gCachedHostInfoMonitor); + nsIMAPHostInfo* host = FindHost(serverKey); + if (host) intResult = host->fNamespaceList->GetNumberOfNamespaces(); + PR_ExitMonitor(gCachedHostInfoMonitor); + NS_ASSERTION(intResult >= 0, "negative number of namespaces"); + result = (uint32_t)intResult; + return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK; +} + +NS_IMETHODIMP nsImapHostSessionList::GetNamespaceNumberForHost( + const char* serverKey, int32_t n, nsImapNamespace*& result) { + PR_EnterMonitor(gCachedHostInfoMonitor); + nsIMAPHostInfo* host = FindHost(serverKey); + if (host) result = host->fNamespaceList->GetNamespaceNumber(n); + PR_ExitMonitor(gCachedHostInfoMonitor); + return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK; +} + +nsresult nsImapHostSessionList::SetNamespacesPrefForHost( + nsIImapIncomingServer* aHost, EIMAPNamespaceType type, const char* pref) { + if (type == kPersonalNamespace) + aHost->SetPersonalNamespace(nsDependentCString(pref)); + else if (type == kPublicNamespace) + aHost->SetPublicNamespace(nsDependentCString(pref)); + else if (type == kOtherUsersNamespace) + aHost->SetOtherUsersNamespace(nsDependentCString(pref)); + else + NS_ASSERTION(false, "bogus namespace type"); + return NS_OK; +} +// do we need this? What should we do about the master thing? +// Make sure this is running in the Mozilla thread when called +NS_IMETHODIMP nsImapHostSessionList::CommitNamespacesForHost( + nsIImapIncomingServer* aHost) { + NS_ENSURE_ARG_POINTER(aHost); + nsCString serverKey; + nsCOMPtr<nsIMsgIncomingServer> incomingServer = do_QueryInterface(aHost); + if (!incomingServer) return NS_ERROR_NULL_POINTER; + + nsresult rv = incomingServer->GetKey(serverKey); + NS_ENSURE_SUCCESS(rv, rv); + + PR_EnterMonitor(gCachedHostInfoMonitor); + nsIMAPHostInfo* host = FindHost(serverKey.get()); + if (host) { + host->fGotNamespaces = + true; // so we only issue NAMESPACE once per host per session. + EIMAPNamespaceType type = kPersonalNamespace; + for (int i = 1; i <= 3; i++) { + switch (i) { + case 1: + type = kPersonalNamespace; + break; + case 2: + type = kPublicNamespace; + break; + case 3: + type = kOtherUsersNamespace; + break; + default: + type = kPersonalNamespace; + break; + } + + int32_t numInNS = host->fNamespaceList->GetNumberOfNamespaces(type); + if (numInNS == 0) + SetNamespacesPrefForHost(aHost, type, ""); + else if (numInNS >= 1) { + char* pref = PR_smprintf(""); + for (int count = 1; count <= numInNS; count++) { + nsImapNamespace* ns = + host->fNamespaceList->GetNamespaceNumber(count, type); + if (ns) { + if (count > 1) { + // append the comma + char* tempPref = PR_smprintf("%s,", pref); + PR_FREEIF(pref); + pref = tempPref; + } + char* tempPref = PR_smprintf("%s\"%s\"", pref, ns->GetPrefix()); + PR_FREEIF(pref); + pref = tempPref; + } + } + if (pref) { + SetNamespacesPrefForHost(aHost, type, pref); + PR_Free(pref); + } + } + } + // clear, but don't delete the entries in, the temp namespace list + host->fTempNamespaceList->ClearNamespaces(true, true, false); + + // Now reset all of libmsg's namespace references. + // Did I mention this needs to be running in the mozilla thread? + aHost->ResetNamespaceReferences(); + } + PR_ExitMonitor(gCachedHostInfoMonitor); + return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK; +} + +NS_IMETHODIMP nsImapHostSessionList::FlushUncommittedNamespacesForHost( + const char* serverKey, bool& result) { + PR_EnterMonitor(gCachedHostInfoMonitor); + nsIMAPHostInfo* host = FindHost(serverKey); + if (host) host->fTempNamespaceList->ClearNamespaces(true, true, true); + PR_ExitMonitor(gCachedHostInfoMonitor); + return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK; +} + +// Returns NULL if there is no personal namespace on the given host +NS_IMETHODIMP nsImapHostSessionList::GetOnlineInboxPathForHost( + const char* serverKey, nsString& result) { + PR_EnterMonitor(gCachedHostInfoMonitor); + nsIMAPHostInfo* host = FindHost(serverKey); + if (host) { + nsImapNamespace* ns = NULL; + ns = host->fNamespaceList->GetDefaultNamespaceOfType(kPersonalNamespace); + if (ns) { + CopyASCIItoUTF16(nsDependentCString(ns->GetPrefix()), result); + result.AppendLiteral("INBOX"); + } + } else + result.Truncate(); + PR_ExitMonitor(gCachedHostInfoMonitor); + return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK; +} + +NS_IMETHODIMP nsImapHostSessionList::GetShouldAlwaysListInboxForHost( + const char* /*serverKey*/, bool& result) { + result = true; + + /* + PR_EnterMonitor(gCachedHostInfoMonitor); + nsIMAPHostInfo *host = FindHost(serverKey); + if (host) + ret = host->fShouldAlwaysListInbox; + PR_ExitMonitor(gCachedHostInfoMonitor); + */ + return NS_OK; +} + +NS_IMETHODIMP nsImapHostSessionList::SetShouldAlwaysListInboxForHost( + const char* serverKey, bool shouldList) { + PR_EnterMonitor(gCachedHostInfoMonitor); + nsIMAPHostInfo* host = FindHost(serverKey); + if (host) host->fShouldAlwaysListInbox = shouldList; + PR_ExitMonitor(gCachedHostInfoMonitor); + return (host == NULL) ? NS_ERROR_ILLEGAL_VALUE : NS_OK; +} + +NS_IMETHODIMP +nsImapHostSessionList::SetNamespaceHierarchyDelimiterFromMailboxForHost( + const char* serverKey, const char* boxName, char delimiter) { + PR_EnterMonitor(gCachedHostInfoMonitor); + nsIMAPHostInfo* host = FindHost(serverKey); + if (host) { + nsImapNamespace* ns = host->fNamespaceList->GetNamespaceForMailbox(boxName); + if (ns && !ns->GetIsDelimiterFilledIn()) ns->SetDelimiter(delimiter, true); + } + PR_ExitMonitor(gCachedHostInfoMonitor); + return (host) ? NS_OK : NS_ERROR_ILLEGAL_VALUE; +} diff --git a/comm/mailnews/imap/src/nsImapHostSessionList.h b/comm/mailnews/imap/src/nsImapHostSessionList.h new file mode 100644 index 0000000000..325cbae868 --- /dev/null +++ b/comm/mailnews/imap/src/nsImapHostSessionList.h @@ -0,0 +1,169 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef _nsImapHostSessionList_H_ +#define _nsImapHostSessionList_H_ + +#include "mozilla/Attributes.h" +#include "nsImapCore.h" +#include "../public/nsIImapHostSessionList.h" +#include "nsIObserver.h" +#include "nsWeakReference.h" +#include "nspr.h" + +class nsImapNamespaceList; +class nsIImapIncomingServer; + +class nsIMAPHostInfo { + public: + friend class nsImapHostSessionList; + + nsIMAPHostInfo(const char* serverKey, nsIImapIncomingServer* server); + ~nsIMAPHostInfo(); + + protected: + nsCString fServerKey; + nsString fCachedPassword; + nsCString fOnlineDir; + nsIMAPHostInfo* fNextHost; + eIMAPCapabilityFlags fCapabilityFlags; + char* fHierarchyDelimiters; // string of top-level hierarchy delimiters + bool fHaveWeEverDiscoveredFolders; + bool fDiscoveryForHostInProgress; + char* fCanonicalOnlineSubDir; + nsImapNamespaceList *fNamespaceList, *fTempNamespaceList; + bool fNamespacesOverridable; + bool fUsingSubscription; + bool fOnlineTrashFolderExists; + bool fShouldAlwaysListInbox; + bool fHaveAdminURL; + bool fPasswordVerifiedOnline; + bool fDeleteIsMoveToTrash; + bool fShowDeletedMessages; + bool fGotNamespaces; +}; + +// this is an interface to a linked list of host info's +class nsImapHostSessionList : public nsIImapHostSessionList, + public nsIObserver, + public nsSupportsWeakReference { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIOBSERVER + + nsImapHostSessionList(); + nsresult Init(); + // Host List + NS_IMETHOD AddHostToList(const char* serverKey, + nsIImapIncomingServer* server) override; + NS_IMETHOD ResetAll() override; + + // Capabilities + NS_IMETHOD GetHostHasAdminURL(const char* serverKey, bool& result) override; + NS_IMETHOD SetHostHasAdminURL(const char* serverKey, + bool hasAdminUrl) override; + // Subscription + NS_IMETHOD GetHostIsUsingSubscription(const char* serverKey, + bool& result) override; + NS_IMETHOD SetHostIsUsingSubscription(const char* serverKey, + bool usingSubscription) override; + + // Passwords + NS_IMETHOD GetPasswordForHost(const char* serverKey, + nsString& result) override; + NS_IMETHOD SetPasswordForHost(const char* serverKey, + const nsAString& password) override; + NS_IMETHOD GetPasswordVerifiedOnline(const char* serverKey, + bool& result) override; + NS_IMETHOD SetPasswordVerifiedOnline(const char* serverKey) override; + + // OnlineDir + NS_IMETHOD GetOnlineDirForHost(const char* serverKey, + nsString& result) override; + NS_IMETHOD SetOnlineDirForHost(const char* serverKey, + const char* onlineDir) override; + + // Delete is move to trash folder + NS_IMETHOD GetDeleteIsMoveToTrashForHost(const char* serverKey, + bool& result) override; + NS_IMETHOD SetDeleteIsMoveToTrashForHost(const char* serverKey, + bool isMoveToTrash) override; + // imap delete model (or not) + NS_IMETHOD GetShowDeletedMessagesForHost(const char* serverKey, + bool& result) override; + NS_IMETHOD SetShowDeletedMessagesForHost(const char* serverKey, + bool showDeletedMessages) override; + + // Get namespaces + NS_IMETHOD GetGotNamespacesForHost(const char* serverKey, + bool& result) override; + NS_IMETHOD SetGotNamespacesForHost(const char* serverKey, + bool gotNamespaces) override; + // Folders + NS_IMETHOD SetHaveWeEverDiscoveredFoldersForHost(const char* serverKey, + bool discovered) override; + NS_IMETHOD GetHaveWeEverDiscoveredFoldersForHost(const char* serverKey, + bool& result) override; + NS_IMETHOD SetDiscoveryForHostInProgress(const char* serverKey, + bool inProgress) override; + NS_IMETHOD GetDiscoveryForHostInProgress(const char* serverKey, + bool& result) override; + + // Trash Folder + NS_IMETHOD SetOnlineTrashFolderExistsForHost(const char* serverKey, + bool exists) override; + NS_IMETHOD GetOnlineTrashFolderExistsForHost(const char* serverKey, + bool& result) override; + + // INBOX + NS_IMETHOD GetOnlineInboxPathForHost(const char* serverKey, + nsString& result) override; + NS_IMETHOD GetShouldAlwaysListInboxForHost(const char* serverKey, + bool& result) override; + NS_IMETHOD SetShouldAlwaysListInboxForHost(const char* serverKey, + bool shouldList) override; + + // Namespaces + NS_IMETHOD GetNamespaceForMailboxForHost(const char* serverKey, + const char* mailbox_name, + nsImapNamespace*& result) override; + NS_IMETHOD SetNamespaceFromPrefForHost(const char* serverKey, + const char* namespacePref, + EIMAPNamespaceType type) override; + NS_IMETHOD AddNewNamespaceForHost(const char* serverKey, + nsImapNamespace* ns) override; + NS_IMETHOD ClearServerAdvertisedNamespacesForHost( + const char* serverKey) override; + NS_IMETHOD ClearPrefsNamespacesForHost(const char* serverKey) override; + NS_IMETHOD GetDefaultNamespaceOfTypeForHost( + const char* serverKey, EIMAPNamespaceType type, + nsImapNamespace*& result) override; + NS_IMETHOD SetNamespacesOverridableForHost(const char* serverKey, + bool overridable) override; + NS_IMETHOD GetNamespacesOverridableForHost(const char* serverKey, + bool& result) override; + NS_IMETHOD GetNumberOfNamespacesForHost(const char* serverKey, + uint32_t& result) override; + NS_IMETHOD GetNamespaceNumberForHost(const char* serverKey, int32_t n, + nsImapNamespace*& result) override; + // ### dmb hoo boy, how are we going to do this? + NS_IMETHOD CommitNamespacesForHost(nsIImapIncomingServer* host) override; + NS_IMETHOD FlushUncommittedNamespacesForHost(const char* serverKey, + bool& result) override; + + // Hierarchy Delimiters + NS_IMETHOD SetNamespaceHierarchyDelimiterFromMailboxForHost( + const char* serverKey, const char* boxName, char delimiter) override; + + PRMonitor* gCachedHostInfoMonitor; + nsIMAPHostInfo* fHostInfoList; + + protected: + virtual ~nsImapHostSessionList(); + nsresult SetNamespacesPrefForHost(nsIImapIncomingServer* aHost, + EIMAPNamespaceType type, const char* pref); + nsIMAPHostInfo* FindHost(const char* serverKey); +}; +#endif diff --git a/comm/mailnews/imap/src/nsImapIncomingServer.cpp b/comm/mailnews/imap/src/nsImapIncomingServer.cpp new file mode 100644 index 0000000000..67b5df6d53 --- /dev/null +++ b/comm/mailnews/imap/src/nsImapIncomingServer.cpp @@ -0,0 +1,3032 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" + +#include "netCore.h" +#include "../public/nsIImapHostSessionList.h" +#include "nsImapIncomingServer.h" +#include "nsIMsgAccountManager.h" +#include "nsIMsgIdentity.h" +#include "nsIImapUrl.h" +#include "nsIUrlListener.h" +#include "nsThreadUtils.h" +#include "nsImapProtocol.h" +#include "nsCOMPtr.h" +#include "nsIPrefBranch.h" +#include "nsIPrefService.h" +#include "nsMsgFolderFlags.h" +#include "prmem.h" +#include "plstr.h" +#include "nsIMsgFolder.h" +#include "nsIMsgWindow.h" +#include "nsImapMailFolder.h" +#include "nsIMsgMailNewsUrl.h" +#include "nsIImapService.h" +#include "nsMsgI18N.h" +#include "nsIImapMockChannel.h" +// for the memory cache... +#include "nsICacheEntry.h" +#include "nsImapUrl.h" +#include "nsIMsgProtocolInfo.h" +#include "nsIMsgMailSession.h" +#include "nsImapNamespace.h" +#include "nsArrayUtils.h" +#include "nsMsgUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsComponentManagerUtils.h" +#include "nsCRTGlue.h" +#include "mozilla/Components.h" +#include "nsNetUtil.h" +#include "mozilla/Utf8.h" +#include "mozilla/LoadInfo.h" + +using namespace mozilla; + +// Despite its name, this contains a folder path, for example INBOX/Trash. +#define PREF_TRASH_FOLDER_PATH "trash_folder_name" +#define DEFAULT_TRASH_FOLDER_PATH "Trash" // XXX Is this a useful default? + +#define NS_SUBSCRIBABLESERVER_CID \ + { \ + 0x8510876a, 0x1dd2, 0x11b2, { \ + 0x82, 0x53, 0x91, 0xf7, 0x1b, 0x34, 0x8a, 0x25 \ + } \ + } +static NS_DEFINE_CID(kSubscribableServerCID, NS_SUBSCRIBABLESERVER_CID); +#define NS_IIMAPHOSTSESSIONLIST_CID \ + { \ + 0x479ce8fc, 0xe725, 0x11d2, { \ + 0xa5, 0x05, 0x00, 0x60, 0xb0, 0xfc, 0x04, 0xb7 \ + } \ + } +static NS_DEFINE_CID(kCImapHostSessionListCID, NS_IIMAPHOSTSESSIONLIST_CID); + +NS_IMPL_ADDREF_INHERITED(nsImapIncomingServer, nsMsgIncomingServer) +NS_IMPL_RELEASE_INHERITED(nsImapIncomingServer, nsMsgIncomingServer) + +NS_INTERFACE_MAP_BEGIN(nsImapIncomingServer) + NS_INTERFACE_MAP_ENTRY(nsIImapServerSink) + NS_INTERFACE_MAP_ENTRY(nsIImapIncomingServer) + NS_INTERFACE_MAP_ENTRY(nsISubscribableServer) + NS_INTERFACE_MAP_ENTRY(nsIUrlListener) +NS_INTERFACE_MAP_END_INHERITING(nsMsgIncomingServer) + +nsImapIncomingServer::nsImapIncomingServer() + : mLock("nsImapIncomingServer.mLock") { + m_capability = kCapabilityUndefined; + mDoingSubscribeDialog = false; + mDoingLsub = false; + m_canHaveFilters = true; + m_userAuthenticated = false; + m_shuttingDown = false; + mUtf8AcceptEnabled = false; +} + +nsImapIncomingServer::~nsImapIncomingServer() { + mozilla::DebugOnly<nsresult> rv = ClearInner(); + NS_ASSERTION(NS_SUCCEEDED(rv), "ClearInner failed"); + CloseCachedConnections(); +} + +NS_IMETHODIMP nsImapIncomingServer::SetKey( + const nsACString& aKey) // override nsMsgIncomingServer's implementation... +{ + nsMsgIncomingServer::SetKey(aKey); + + // okay now that the key has been set, we need to add ourselves to the + // host session list... + + // every time we create an imap incoming server, we need to add it to the + // host session list!! + + nsresult rv; + nsCOMPtr<nsIImapHostSessionList> hostSession = + do_GetService(kCImapHostSessionListCID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString key(aKey); + hostSession->AddHostToList(key.get(), this); + nsMsgImapDeleteModel deleteModel = + nsMsgImapDeleteModels::MoveToTrash; // default to trash + GetDeleteModel(&deleteModel); + hostSession->SetDeleteIsMoveToTrashForHost( + key.get(), deleteModel == nsMsgImapDeleteModels::MoveToTrash); + hostSession->SetShowDeletedMessagesForHost( + key.get(), deleteModel == nsMsgImapDeleteModels::IMAPDelete); + + nsAutoCString onlineDir; + rv = GetServerDirectory(onlineDir); + NS_ENSURE_SUCCESS(rv, rv); + if (!onlineDir.IsEmpty()) + hostSession->SetOnlineDirForHost(key.get(), onlineDir.get()); + + nsCString personalNamespace; + nsCString publicNamespace; + nsCString otherUsersNamespace; + + rv = GetPersonalNamespace(personalNamespace); + NS_ENSURE_SUCCESS(rv, rv); + rv = GetPublicNamespace(publicNamespace); + NS_ENSURE_SUCCESS(rv, rv); + rv = GetOtherUsersNamespace(otherUsersNamespace); + NS_ENSURE_SUCCESS(rv, rv); + + if (personalNamespace.IsEmpty() && publicNamespace.IsEmpty() && + otherUsersNamespace.IsEmpty()) + personalNamespace.AssignLiteral("\"\""); + + hostSession->SetNamespaceFromPrefForHost(key.get(), personalNamespace.get(), + kPersonalNamespace); + + if (!publicNamespace.IsEmpty()) + hostSession->SetNamespaceFromPrefForHost(key.get(), publicNamespace.get(), + kPublicNamespace); + + if (!otherUsersNamespace.IsEmpty()) + hostSession->SetNamespaceFromPrefForHost( + key.get(), otherUsersNamespace.get(), kOtherUsersNamespace); + return rv; +} + +// construct the pretty name to show to the user if they haven't +// specified one. This should be overridden for news and mail. +NS_IMETHODIMP +nsImapIncomingServer::GetConstructedPrettyName(nsAString& retval) { + nsAutoCString username; + nsAutoCString hostName; + nsresult rv; + + nsCOMPtr<nsIMsgAccountManager> accountManager = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgIdentity> identity; + rv = + accountManager->GetFirstIdentityForServer(this, getter_AddRefs(identity)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString emailAddress; + + if (NS_SUCCEEDED(rv) && identity) { + nsCString identityEmailAddress; + identity->GetEmail(identityEmailAddress); + CopyASCIItoUTF16(identityEmailAddress, emailAddress); + } else { + rv = GetUsername(username); + NS_ENSURE_SUCCESS(rv, rv); + rv = GetHostName(hostName); + NS_ENSURE_SUCCESS(rv, rv); + if (!username.IsEmpty() && !hostName.IsEmpty()) { + CopyASCIItoUTF16(username, emailAddress); + emailAddress.Append('@'); + emailAddress.Append(NS_ConvertASCIItoUTF16(hostName)); + } + } + + return GetFormattedStringFromName(emailAddress, "imapDefaultAccountName", + retval); +} + +NS_IMETHODIMP nsImapIncomingServer::GetLocalStoreType(nsACString& type) { + type.AssignLiteral("imap"); + return NS_OK; +} + +NS_IMETHODIMP nsImapIncomingServer::GetLocalDatabaseType(nsACString& type) { + type.AssignLiteral("imap"); + return NS_OK; +} + +NS_IMETHODIMP +nsImapIncomingServer::GetServerDirectory(nsACString& serverDirectory) { + return GetCharValue("server_sub_directory", serverDirectory); +} + +NS_IMETHODIMP +nsImapIncomingServer::SetServerDirectory(const nsACString& serverDirectory) { + nsCString serverKey; + nsresult rv = GetKey(serverKey); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIImapHostSessionList> hostSession = + do_GetService(kCImapHostSessionListCID, &rv); + if (NS_SUCCEEDED(rv)) + hostSession->SetOnlineDirForHost( + serverKey.get(), PromiseFlatCString(serverDirectory).get()); + } + return SetCharValue("server_sub_directory", serverDirectory); +} + +NS_IMETHODIMP +nsImapIncomingServer::GetOverrideNamespaces(bool* bVal) { + return GetBoolValue("override_namespaces", bVal); +} + +NS_IMETHODIMP +nsImapIncomingServer::SetOverrideNamespaces(bool bVal) { + nsCString serverKey; + GetKey(serverKey); + if (!serverKey.IsEmpty()) { + nsresult rv; + nsCOMPtr<nsIImapHostSessionList> hostSession = + do_GetService(kCImapHostSessionListCID, &rv); + if (NS_SUCCEEDED(rv)) + hostSession->SetNamespacesOverridableForHost(serverKey.get(), bVal); + } + return SetBoolValue("override_namespaces", bVal); +} + +NS_IMETHODIMP +nsImapIncomingServer::GetUsingSubscription(bool* bVal) { + return GetBoolValue("using_subscription", bVal); +} + +NS_IMETHODIMP +nsImapIncomingServer::SetUsingSubscription(bool bVal) { + nsCString serverKey; + GetKey(serverKey); + if (!serverKey.IsEmpty()) { + nsresult rv; + nsCOMPtr<nsIImapHostSessionList> hostSession = + do_GetService(kCImapHostSessionListCID, &rv); + if (NS_SUCCEEDED(rv)) + hostSession->SetHostIsUsingSubscription(serverKey.get(), bVal); + } + return SetBoolValue("using_subscription", bVal); +} + +NS_IMETHODIMP +nsImapIncomingServer::GetMaximumConnectionsNumber(int32_t* aMaxConnections) { + NS_ENSURE_ARG_POINTER(aMaxConnections); + + nsresult rv = GetIntValue("max_cached_connections", aMaxConnections); + // Get our maximum connection count. We need at least 1. If the value is 0, + // we use the default of 5. If it's negative, we treat that as 1. + if (NS_SUCCEEDED(rv) && *aMaxConnections > 0) return NS_OK; + + *aMaxConnections = (NS_FAILED(rv) || (*aMaxConnections == 0)) ? 5 : 1; + (void)SetMaximumConnectionsNumber(*aMaxConnections); + + return NS_OK; +} + +NS_IMETHODIMP +nsImapIncomingServer::SetMaximumConnectionsNumber(int32_t aMaxConnections) { + return SetIntValue("max_cached_connections", aMaxConnections); +} + +NS_IMPL_SERVERPREF_BOOL(nsImapIncomingServer, ForceSelect, "force_select_imap") + +NS_IMPL_SERVERPREF_BOOL(nsImapIncomingServer, DualUseFolders, + "dual_use_folders") + +NS_IMPL_SERVERPREF_STR(nsImapIncomingServer, AdminUrl, "admin_url") + +NS_IMPL_SERVERPREF_BOOL(nsImapIncomingServer, CleanupInboxOnExit, + "cleanup_inbox_on_exit") + +NS_IMPL_SERVERPREF_BOOL(nsImapIncomingServer, OfflineDownload, + "offline_download") + +NS_IMPL_SERVERPREF_BOOL(nsImapIncomingServer, DownloadBodiesOnGetNewMail, + "download_bodies_on_get_new_mail") + +NS_IMPL_SERVERPREF_BOOL(nsImapIncomingServer, AutoSyncOfflineStores, + "autosync_offline_stores") + +NS_IMPL_SERVERPREF_BOOL(nsImapIncomingServer, UseIdle, "use_idle") + +NS_IMPL_SERVERPREF_BOOL(nsImapIncomingServer, CheckAllFoldersForNew, + "check_all_folders_for_new") + +NS_IMPL_SERVERPREF_BOOL(nsImapIncomingServer, UseCondStore, "use_condstore") + +NS_IMPL_SERVERPREF_BOOL(nsImapIncomingServer, IsGMailServer, "is_gmail") + +NS_IMPL_SERVERPREF_BOOL(nsImapIncomingServer, UseCompressDeflate, + "use_compress_deflate") + +NS_IMPL_SERVERPREF_INT(nsImapIncomingServer, AutoSyncMaxAgeDays, + "autosync_max_age_days") + +NS_IMPL_SERVERPREF_BOOL(nsImapIncomingServer, AllowUTF8Accept, + "allow_utf8_accept") + +NS_IMETHODIMP +nsImapIncomingServer::GetShuttingDown(bool* retval) { + NS_ENSURE_ARG_POINTER(retval); + *retval = m_shuttingDown; + return NS_OK; +} + +NS_IMETHODIMP +nsImapIncomingServer::SetShuttingDown(bool val) { + m_shuttingDown = val; + return NS_OK; +} + +NS_IMETHODIMP +nsImapIncomingServer::GetDeleteModel(int32_t* retval) { + NS_ENSURE_ARG(retval); + return GetIntValue("delete_model", retval); +} + +NS_IMETHODIMP +nsImapIncomingServer::SetDeleteModel(int32_t ivalue) { + nsresult rv = SetIntValue("delete_model", ivalue); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIImapHostSessionList> hostSession = + do_GetService(kCImapHostSessionListCID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + hostSession->SetDeleteIsMoveToTrashForHost( + m_serverKey.get(), ivalue == nsMsgImapDeleteModels::MoveToTrash); + hostSession->SetShowDeletedMessagesForHost( + m_serverKey.get(), ivalue == nsMsgImapDeleteModels::IMAPDelete); + + // Despite its name, this returns the trash folder path, for example + // INBOX/Trash. + nsAutoString trashFolderName; + nsresult rv = GetTrashFolderName(trashFolderName); + if (NS_SUCCEEDED(rv)) { + nsAutoCString trashFolderNameUtf7or8; + bool useUTF8 = false; + GetUtf8AcceptEnabled(&useUTF8); + if (useUTF8) { + CopyUTF16toUTF8(trashFolderName, trashFolderNameUtf7or8); + } else { + CopyUTF16toMUTF7(trashFolderName, trashFolderNameUtf7or8); + } + nsCOMPtr<nsIMsgFolder> trashFolder; + // 'trashFolderName' being a path here works well since this is appended + // to the server's root folder in GetFolder(). + rv = GetFolder(trashFolderNameUtf7or8, getter_AddRefs(trashFolder)); + NS_ENSURE_SUCCESS(rv, rv); + nsCString trashURI; + trashFolder->GetURI(trashURI); + rv = GetMsgFolderFromURI(trashFolder, trashURI, + getter_AddRefs(trashFolder)); + if (NS_SUCCEEDED(rv) && trashFolder) { + // If the trash folder is used, set the flag, otherwise clear it. + if (ivalue == nsMsgImapDeleteModels::MoveToTrash) + trashFolder->SetFlag(nsMsgFolderFlags::Trash); + else + trashFolder->ClearFlag(nsMsgFolderFlags::Trash); + } + } + } + return rv; +} + +NS_IMPL_SERVERPREF_INT(nsImapIncomingServer, TimeOutLimits, "timeout") + +NS_IMPL_SERVERPREF_STR(nsImapIncomingServer, ServerIDPref, "serverIDResponse") + +NS_IMPL_SERVERPREF_STR(nsImapIncomingServer, PersonalNamespace, + "namespace.personal") + +NS_IMPL_SERVERPREF_STR(nsImapIncomingServer, PublicNamespace, + "namespace.public") + +NS_IMPL_SERVERPREF_STR(nsImapIncomingServer, OtherUsersNamespace, + "namespace.other_users") + +NS_IMPL_SERVERPREF_BOOL(nsImapIncomingServer, FetchByChunks, "fetch_by_chunks") + +NS_IMPL_SERVERPREF_BOOL(nsImapIncomingServer, SendID, "send_client_info") + +NS_IMETHODIMP +nsImapIncomingServer::GetIsAOLServer(bool* aBool) { + NS_ENSURE_ARG_POINTER(aBool); + *aBool = ((m_capability & kAOLImapCapability) != 0); + return NS_OK; +} + +NS_IMETHODIMP +nsImapIncomingServer::SetIsAOLServer(bool aBool) { + if (aBool) + m_capability |= kAOLImapCapability; + else + m_capability &= ~kAOLImapCapability; + return NS_OK; +} + +NS_IMETHODIMP +nsImapIncomingServer::UpdateTrySTARTTLSPref(bool aStartTLSSucceeded) { + SetSocketType(aStartTLSSucceeded ? nsMsgSocketType::alwaysSTARTTLS + : nsMsgSocketType::plain); + return NS_OK; +} + +NS_IMETHODIMP +nsImapIncomingServer::GetImapConnectionAndLoadUrl(nsIImapUrl* aImapUrl, + nsISupports* aConsumer) { + nsCOMPtr<nsIImapProtocol> aProtocol; + + nsresult rv = GetImapConnection(aImapUrl, getter_AddRefs(aProtocol)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl = do_QueryInterface(aImapUrl, &rv); + if (aProtocol) { + rv = aProtocol->LoadImapUrl(mailnewsurl, aConsumer); + // *** jt - in case of the time out situation or the connection gets + // terminated by some unforeseen problems let's give it a second chance + // to run the url + if (NS_FAILED(rv) && rv != NS_ERROR_ILLEGAL_VALUE) { + rv = aProtocol->LoadImapUrl(mailnewsurl, aConsumer); + } + } else { // unable to get an imap connection to run the url; add to the url + // queue + nsImapProtocol::LogImapUrl("queuing url", aImapUrl); + PR_CEnterMonitor(this); + m_urlQueue.AppendObject(aImapUrl); + m_urlConsumers.AppendElement(aConsumer); + NS_IF_ADDREF(aConsumer); + PR_CExitMonitor(this); + // let's try running it now - maybe the connection is free now. + bool urlRun; + rv = LoadNextQueuedUrl(nullptr, &urlRun); + } + return rv; +} + +NS_IMETHODIMP +nsImapIncomingServer::PrepareToRetryUrl(nsIImapUrl* aImapUrl, + nsIImapMockChannel** aChannel) { + NS_ENSURE_ARG_POINTER(aChannel); + NS_ENSURE_ARG_POINTER(aImapUrl); + // maybe there's more we could do here, but this is all we need now. + return aImapUrl->GetMockChannel(aChannel); +} + +NS_IMETHODIMP +nsImapIncomingServer::SuspendUrl(nsIImapUrl* aImapUrl) { + NS_ENSURE_ARG_POINTER(aImapUrl); + nsImapProtocol::LogImapUrl("suspending url", aImapUrl); + PR_CEnterMonitor(this); + m_urlQueue.AppendObject(aImapUrl); + m_urlConsumers.AppendElement(nullptr); + PR_CExitMonitor(this); + return NS_OK; +} + +NS_IMETHODIMP +nsImapIncomingServer::RetryUrl(nsIImapUrl* aImapUrl, + nsIImapMockChannel* aChannel) { + nsresult rv; + // Get current thread envent queue + aImapUrl->SetMockChannel(aChannel); + nsCOMPtr<nsIImapProtocol> protocolInstance; + nsImapProtocol::LogImapUrl("creating protocol instance to retry queued url", + aImapUrl); + nsCOMPtr<nsIThread> thread(do_GetCurrentThread()); + rv = GetImapConnection(aImapUrl, getter_AddRefs(protocolInstance)); + if (NS_SUCCEEDED(rv) && protocolInstance) { + nsCOMPtr<nsIURI> url = do_QueryInterface(aImapUrl, &rv); + if (NS_SUCCEEDED(rv) && url) { + nsImapProtocol::LogImapUrl("retrying url", aImapUrl); + rv = protocolInstance->LoadImapUrl( + url, nullptr); // ### need to save the display consumer. + } + } + return rv; +} + +// checks to see if there are any queued urls on this incoming server, +// and if so, tries to run the oldest one. Returns true if the url is run +// on the passed in protocol connection. +NS_IMETHODIMP +nsImapIncomingServer::LoadNextQueuedUrl(nsIImapProtocol* aProtocol, + bool* aResult) { + if (WeAreOffline()) return NS_MSG_ERROR_OFFLINE; + + nsresult rv = NS_OK; + bool urlRun = false; + bool keepGoing = true; + nsCOMPtr<nsIImapProtocol> protocolInstance; + + MutexAutoLock mon(mLock); + int32_t cnt = m_urlQueue.Count(); + + while (cnt > 0 && !urlRun && keepGoing) { + nsCOMPtr<nsIImapUrl> aImapUrl(m_urlQueue[0]); + + bool removeUrlFromQueue = false; + if (aImapUrl) { + nsImapProtocol::LogImapUrl("considering playing queued url", aImapUrl); + rv = DoomUrlIfChannelHasError(aImapUrl, &removeUrlFromQueue); + NS_ENSURE_SUCCESS(rv, rv); + // if we didn't doom the url, lets run it. + if (!removeUrlFromQueue) { + nsISupports* aConsumer = m_urlConsumers.ElementAt(0); + NS_IF_ADDREF(aConsumer); + + nsImapProtocol::LogImapUrl( + "creating protocol instance to play queued url", aImapUrl); + rv = GetImapConnection(aImapUrl, getter_AddRefs(protocolInstance)); + if (NS_SUCCEEDED(rv) && protocolInstance) { + nsCOMPtr<nsIURI> url = do_QueryInterface(aImapUrl, &rv); + if (NS_SUCCEEDED(rv) && url) { + nsImapProtocol::LogImapUrl("playing queued url", aImapUrl); + rv = protocolInstance->LoadImapUrl(url, aConsumer); + if (NS_SUCCEEDED(rv)) { + bool isInbox; + protocolInstance->IsBusy(&urlRun, &isInbox); + if (!urlRun) + nsImapProtocol::LogImapUrl("didn't need to run", aImapUrl); + removeUrlFromQueue = true; + } else { + nsImapProtocol::LogImapUrl("playing queued url failed", aImapUrl); + } + } + } else { + nsImapProtocol::LogImapUrl( + "failed creating protocol instance to play queued url", aImapUrl); + keepGoing = false; + } + NS_IF_RELEASE(aConsumer); + } + if (removeUrlFromQueue) { + m_urlQueue.RemoveObjectAt(0); + m_urlConsumers.RemoveElementAt(0); + } + } + cnt = m_urlQueue.Count(); + } + if (aResult) *aResult = urlRun && aProtocol && aProtocol == protocolInstance; + + return rv; +} + +NS_IMETHODIMP +nsImapIncomingServer::AbortQueuedUrls() { + nsresult rv = NS_OK; + + MutexAutoLock mon(mLock); + int32_t cnt = m_urlQueue.Count(); + + while (cnt > 0) { + nsCOMPtr<nsIImapUrl> aImapUrl(m_urlQueue[cnt - 1]); + bool removeUrlFromQueue = false; + + if (aImapUrl) { + rv = DoomUrlIfChannelHasError(aImapUrl, &removeUrlFromQueue); + NS_ENSURE_SUCCESS(rv, rv); + if (removeUrlFromQueue) { + m_urlQueue.RemoveObjectAt(cnt - 1); + m_urlConsumers.RemoveElementAt(cnt - 1); + } + } + cnt--; + } + + return rv; +} + +// if this url has a channel with an error, doom it and its mem cache entries, +// and notify url listeners. +nsresult nsImapIncomingServer::DoomUrlIfChannelHasError(nsIImapUrl* aImapUrl, + bool* urlDoomed) { + nsresult rv = NS_OK; + + nsCOMPtr<nsIMsgMailNewsUrl> aMailNewsUrl(do_QueryInterface(aImapUrl, &rv)); + + if (aMailNewsUrl && aImapUrl) { + nsCOMPtr<nsIImapMockChannel> mockChannel; + + if (NS_SUCCEEDED(aImapUrl->GetMockChannel(getter_AddRefs(mockChannel))) && + mockChannel) { + nsresult requestStatus; + mockChannel->GetStatus(&requestStatus); + if (NS_FAILED(requestStatus)) { + nsresult res; + *urlDoomed = true; + nsImapProtocol::LogImapUrl("dooming url", aImapUrl); + + mockChannel + ->Close(); // try closing it to get channel listener nulled out. + + if (aMailNewsUrl) { + nsCOMPtr<nsICacheEntry> cacheEntry; + res = aMailNewsUrl->GetMemCacheEntry(getter_AddRefs(cacheEntry)); + if (NS_SUCCEEDED(res) && cacheEntry) cacheEntry->AsyncDoom(nullptr); + // we're aborting this url - tell listeners + aMailNewsUrl->SetUrlState(false, NS_MSG_ERROR_URL_ABORTED); + } + } + } + } + return rv; +} + +NS_IMETHODIMP +nsImapIncomingServer::RemoveConnection(nsIImapProtocol* aImapConnection) { + PR_CEnterMonitor(this); + if (aImapConnection) m_connectionCache.RemoveObject(aImapConnection); + + PR_CExitMonitor(this); + return NS_OK; +} + +bool nsImapIncomingServer::ConnectionTimeOut(nsIImapProtocol* aConnection) { + bool retVal = false; + if (!aConnection) return retVal; + nsresult rv; + + int32_t timeoutInMinutes = 0; + rv = GetTimeOutLimits(&timeoutInMinutes); + if (NS_FAILED(rv) || timeoutInMinutes <= 0 || timeoutInMinutes > 29) { + timeoutInMinutes = 29; + SetTimeOutLimits(timeoutInMinutes); + } + + PRTime cacheTimeoutLimits = timeoutInMinutes * 60 * PR_USEC_PER_SEC; + PRTime lastActiveTimeStamp; + rv = aConnection->GetLastActiveTimeStamp(&lastActiveTimeStamp); + + if (PR_Now() - lastActiveTimeStamp >= cacheTimeoutLimits) { + RemoveConnection(aConnection); + aConnection->TellThreadToDie(false); + retVal = true; + } + return retVal; +} + +nsresult nsImapIncomingServer::GetImapConnection( + nsIImapUrl* aImapUrl, nsIImapProtocol** aImapConnection) { + NS_ENSURE_ARG_POINTER(aImapUrl); + + nsresult rv = NS_OK; + bool canRunUrlImmediately = false; + bool canRunButBusy = false; + nsCOMPtr<nsIImapProtocol> connection; + nsCOMPtr<nsIImapProtocol> freeConnection; + bool isBusy = false; + bool isInboxConnection = false; + + PR_CEnterMonitor(this); + + int32_t maxConnections; + (void)GetMaximumConnectionsNumber(&maxConnections); + + int32_t cnt = m_connectionCache.Count(); + *aImapConnection = nullptr; + + // iterate through the connection cache for a connection that can handle this + // url. + // loop until we find a connection that can run the url, or doesn't have to + // wait? + for (int32_t i = cnt - 1; i >= 0 && !canRunUrlImmediately && !canRunButBusy; + i--) { + connection = m_connectionCache[i]; + if (connection) { + bool badConnection = ConnectionTimeOut(connection); + if (!badConnection) { + badConnection = NS_FAILED(connection->CanHandleUrl( + aImapUrl, &canRunUrlImmediately, &canRunButBusy)); +#ifdef DEBUG_bienvenu + nsAutoCString curSelectedFolderName; + if (connection) + connection->GetSelectedMailboxName( + getter_Copies(curSelectedFolderName)); + // check that no other connection is in the same selected state. + if (!curSelectedFolderName.IsEmpty()) { + for (uint32_t j = 0; j < cnt; j++) { + if (j != i) { + nsCOMPtr<nsIImapProtocol> otherConnection = + do_QueryElementAt(m_connectionCache, j); + if (otherConnection) { + nsAutoCString otherSelectedFolderName; + otherConnection->GetSelectedMailboxName( + getter_Copies(otherSelectedFolderName)); + NS_ASSERTION( + !curSelectedFolderName.Equals(otherSelectedFolderName), + "two connections selected on same folder"); + } + } + } + } +#endif // DEBUG_bienvenu + } + if (badConnection) { + connection = nullptr; + continue; + } + } + + // if this connection is wrong, but it's not busy, check if we should + // designate it as the free connection. + if (!canRunUrlImmediately && !canRunButBusy && connection) { + rv = connection->IsBusy(&isBusy, &isInboxConnection); + if (NS_FAILED(rv)) continue; + // if max connections is <= 1, we have to re-use the inbox connection. + if (!isBusy && (!isInboxConnection || maxConnections <= 1)) { + if (!freeConnection) + freeConnection = connection; + else // check which is the better free connection to use. + { // We prefer one not in the selected state. + nsAutoCString selectedFolderName; + connection->GetSelectedMailboxName(getter_Copies(selectedFolderName)); + if (selectedFolderName.IsEmpty()) freeConnection = connection; + } + } + } + // don't leave this loop with connection set if we can't use it! + if (!canRunButBusy && !canRunUrlImmediately) connection = nullptr; + } + + nsImapState requiredState; + aImapUrl->GetRequiredImapState(&requiredState); + // refresh cnt in case we killed one or more dead connections. This + // will prevent us from not spinning up a new connection when all + // connections were dead. + cnt = m_connectionCache.Count(); + // if we got here and we have a connection, then we should return it! + if (canRunUrlImmediately && connection) { + connection.forget(aImapConnection); + } else if (canRunButBusy) { + // do nothing; return NS_OK; for queuing + } + // CanHandleUrl will pretend that some types of urls require a selected state + // url (e.g., a folder delete or msg append) but we shouldn't create new + // connections for these types of urls if we have a free connection. So we + // check the actual required state here. + else if (cnt < maxConnections && + (!freeConnection || + requiredState == nsIImapUrl::nsImapSelectedState)) { + rv = CreateProtocolInstance(aImapConnection); + } else if (freeConnection) { + freeConnection.forget(aImapConnection); + } else { + if (cnt >= maxConnections) + nsImapProtocol::LogImapUrl("exceeded connection cache limit", aImapUrl); + // caller will queue the url + } + + PR_CExitMonitor(this); + return rv; +} + +nsresult nsImapIncomingServer::CreateProtocolInstance( + nsIImapProtocol** aImapConnection) { + // create a new connection and add it to the connection cache + // we may need to flag the protocol connection as busy so we don't get + // a race condition where someone else goes through this code + + int32_t authMethod; + GetAuthMethod(&authMethod); + nsresult rv; + // pre-flight that we have nss - on the ui thread - for MD5 etc. + switch (authMethod) { + case nsMsgAuthMethod::passwordEncrypted: + case nsMsgAuthMethod::secure: + case nsMsgAuthMethod::anything: { + nsCOMPtr<nsISupports> dummyUsedToEnsureNSSIsInitialized = + do_GetService("@mozilla.org/psm;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + } break; + default: + break; + } + nsCOMPtr<nsIImapHostSessionList> hostSession = + do_GetService(kCImapHostSessionListCID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + RefPtr<nsImapProtocol> protocolInstance(new nsImapProtocol()); + rv = protocolInstance->Initialize(hostSession, this); + NS_ENSURE_SUCCESS(rv, rv); + // It implements nsIChannel, and all channels require loadInfo. + protocolInstance->SetLoadInfo(new mozilla::net::LoadInfo( + nsContentUtils::GetSystemPrincipal(), nullptr, nullptr, + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_OTHER)); + + // take the protocol instance and add it to the connectionCache + m_connectionCache.AppendObject(protocolInstance); + protocolInstance.forget(aImapConnection); + return rv; +} + +NS_IMETHODIMP nsImapIncomingServer::CloseConnectionForFolder( + nsIMsgFolder* aMsgFolder) { + nsresult rv = NS_OK; + nsCOMPtr<nsIImapProtocol> connection; + bool isBusy = false, isInbox = false; + nsCString inFolderName; + nsCString connectionFolderName; + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(aMsgFolder); + + if (!imapFolder) return NS_ERROR_NULL_POINTER; + + int32_t cnt = m_connectionCache.Count(); + NS_ENSURE_SUCCESS(rv, rv); + + imapFolder->GetOnlineName(inFolderName); + PR_CEnterMonitor(this); + + for (int32_t i = 0; i < cnt; ++i) { + connection = m_connectionCache[i]; + if (connection) { + rv = connection->GetSelectedMailboxName( + getter_Copies(connectionFolderName)); + if (connectionFolderName.Equals(inFolderName)) { + rv = connection->IsBusy(&isBusy, &isInbox); + if (!isBusy) rv = connection->TellThreadToDie(true); + break; // found it, end of the loop + } + } + } + + PR_CExitMonitor(this); + return rv; +} + +NS_IMETHODIMP nsImapIncomingServer::ResetConnection( + const nsACString& folderName) { + nsresult rv = NS_OK; + nsCOMPtr<nsIImapProtocol> connection; + bool isBusy = false, isInbox = false; + nsCString curFolderName; + + int32_t cnt = m_connectionCache.Count(); + + PR_CEnterMonitor(this); + + for (int32_t i = 0; i < cnt; ++i) { + connection = m_connectionCache[i]; + if (connection) { + rv = connection->GetSelectedMailboxName(getter_Copies(curFolderName)); + if (curFolderName.Equals(folderName)) { + rv = connection->IsBusy(&isBusy, &isInbox); + if (!isBusy) rv = connection->ResetToAuthenticatedState(); + break; // found it, end of the loop + } + } + } + + PR_CExitMonitor(this); + return rv; +} + +NS_IMETHODIMP +nsImapIncomingServer::PerformExpand(nsIMsgWindow* aMsgWindow) { + nsString password; + nsresult rv; + rv = GetPassword(password); + NS_ENSURE_SUCCESS(rv, rv); + + if (password.IsEmpty()) { + // Check if this is due to oauth2 showing empty password. If so, keep going. + int32_t authMethod = 0; + GetAuthMethod(&authMethod); + if (authMethod != nsMsgAuthMethod::OAuth2) return NS_OK; + } + + rv = ResetFoldersToUnverified(nullptr); + + nsCOMPtr<nsIMsgFolder> rootMsgFolder; + rv = GetRootFolder(getter_AddRefs(rootMsgFolder)); + NS_ENSURE_SUCCESS(rv, rv); + + if (!rootMsgFolder) return NS_ERROR_FAILURE; + + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIThread> thread(do_GetCurrentThread()); + rv = imapService->DiscoverAllFolders(rootMsgFolder, this, aMsgWindow); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIImapHostSessionList> hostSessionList = + do_GetService(kCImapHostSessionListCID, &rv); + if (NS_SUCCEEDED(rv)) { + nsAutoCString serverKey; + rv = GetKey(serverKey); + if (!serverKey.IsEmpty()) + hostSessionList->SetDiscoveryForHostInProgress(serverKey.get(), true); + } + return rv; +} + +NS_IMETHODIMP +nsImapIncomingServer::VerifyLogon(nsIUrlListener* aUrlListener, + nsIMsgWindow* aMsgWindow, nsIURI** aURL) { + nsresult rv; + + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgFolder> rootFolder; + // this will create the resource if it doesn't exist, but it shouldn't + // do anything on disk. + rv = GetRootFolder(getter_AddRefs(rootFolder)); + NS_ENSURE_SUCCESS(rv, rv); + return imapService->VerifyLogon(rootFolder, aUrlListener, aMsgWindow, aURL); +} + +NS_IMETHODIMP nsImapIncomingServer::PerformBiff(nsIMsgWindow* aMsgWindow) { + nsCOMPtr<nsIMsgFolder> rootMsgFolder; + nsresult rv = GetRootMsgFolder(getter_AddRefs(rootMsgFolder)); + if (NS_SUCCEEDED(rv)) { + SetPerformingBiff(true); + rv = rootMsgFolder->GetNewMessages(aMsgWindow, nullptr); + } + return rv; +} + +NS_IMETHODIMP +nsImapIncomingServer::CloseCachedConnections() { + nsCOMPtr<nsIImapProtocol> connection; + PR_CEnterMonitor(this); + + // iterate through the connection cache closing open connections. + int32_t cnt = m_connectionCache.Count(); + + for (int32_t i = cnt; i > 0; --i) { + connection = m_connectionCache[i - 1]; + if (connection) connection->TellThreadToDie(true); + } + + PR_CExitMonitor(this); + return NS_OK; +} + +nsresult nsImapIncomingServer::CreateRootFolderFromUri( + const nsACString& serverUri, nsIMsgFolder** rootFolder) { + nsImapMailFolder* newRootFolder = new nsImapMailFolder; + newRootFolder->Init(serverUri); + NS_ADDREF(*rootFolder = newRootFolder); + return NS_OK; +} + +// nsIImapServerSink impl +// aNewFolder will not be set if we're listing for the subscribe UI, since +// that's the way 4.x worked. +NS_IMETHODIMP nsImapIncomingServer::PossibleImapMailbox( + const nsACString& folderPath, char hierarchyDelimiter, int32_t boxFlags, + bool* aNewFolder) { + NS_ENSURE_ARG_POINTER(aNewFolder); + NS_ENSURE_TRUE(!folderPath.IsEmpty(), NS_ERROR_FAILURE); + + // folderPath is in canonical format, i.e., hierarchy separator has been + // replaced with '/' + nsresult rv; + bool found = false; + bool haveParent = false; + nsCOMPtr<nsIMsgImapMailFolder> hostFolder; + nsCOMPtr<nsIMsgFolder> aFolder; + bool explicitlyVerify = false; + + *aNewFolder = false; + nsCOMPtr<nsIMsgFolder> rootFolder; + rv = GetRootFolder(getter_AddRefs(rootFolder)); + + if (NS_FAILED(rv)) return rv; + + nsAutoCString dupFolderPath(folderPath); + if (dupFolderPath.Last() == '/') { + dupFolderPath.SetLength(dupFolderPath.Length() - 1); + if (dupFolderPath.IsEmpty()) return NS_ERROR_FAILURE; + // *** this is what we did in 4.x in order to list uw folder only + // mailbox in order to get the \NoSelect flag + explicitlyVerify = !(boxFlags & kNameSpace); + } + if (mDoingSubscribeDialog) { + // Make sure the imapmailfolder object has the right delimiter because the + // unsubscribed folders (those not in the 'lsub' list) have the delimiter + // set to the default ('^'). + if (rootFolder && !dupFolderPath.IsEmpty()) { + nsCOMPtr<nsIMsgFolder> msgFolder; + bool isNamespace = false; + bool noSelect = false; + + rv = rootFolder->FindSubFolder(dupFolderPath, getter_AddRefs(msgFolder)); + NS_ENSURE_SUCCESS(rv, rv); + m_subscribeFolders.AppendObject(msgFolder); + noSelect = (boxFlags & kNoselect) != 0; + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = + do_QueryInterface(msgFolder, &rv); + NS_ENSURE_SUCCESS(rv, rv); + imapFolder->SetHierarchyDelimiter(hierarchyDelimiter); + isNamespace = (boxFlags & kNameSpace) != 0; + if (!isNamespace) + rv = AddTo(dupFolderPath, + mDoingLsub && !noSelect /* add as subscribed */, !noSelect, + mDoingLsub /* change if exists */); + NS_ENSURE_SUCCESS(rv, rv); + return rv; + } + } + + hostFolder = do_QueryInterface(rootFolder, &rv); + if (NS_FAILED(rv)) return rv; + + nsAutoCString tempFolderName(dupFolderPath); + nsAutoCString tokenStr, remStr, changedStr; + int32_t slashPos = tempFolderName.FindChar('/'); + if (slashPos > 0) { + tokenStr = StringHead(tempFolderName, slashPos); + remStr = Substring(tempFolderName, slashPos); + } else + tokenStr.Assign(tempFolderName); + + if ((int32_t(PL_strcasecmp(tokenStr.get(), "INBOX")) == 0) && + (strcmp(tokenStr.get(), "INBOX") != 0)) + changedStr.AppendLiteral("INBOX"); + else + changedStr.Append(tokenStr); + + if (slashPos > 0) changedStr.Append(remStr); + + dupFolderPath.Assign(changedStr); + nsAutoCString folderName(dupFolderPath); + + nsAutoCString uri; + nsCString serverUri; + GetServerURI(serverUri); + uri.Assign(serverUri); + int32_t leafPos = folderName.RFindChar('/'); + nsAutoCString parentName(folderName); + nsAutoCString parentUri(uri); + + if (leafPos > 0) { + // If there is a hierarchy, there is a parent. + // Don't strip off slash if it's the first character + parentName.SetLength(leafPos); + folderName.Cut(0, leafPos + 1); // get rid of the parent name + haveParent = true; + parentUri.Append('/'); + parentUri.Append(parentName); + } + if (folderPath.LowerCaseEqualsLiteral("inbox") && + hierarchyDelimiter == kOnlineHierarchySeparatorNil) { + hierarchyDelimiter = '/'; // set to default in this case (as in 4.x) + hostFolder->SetHierarchyDelimiter(hierarchyDelimiter); + } + + nsCOMPtr<nsIMsgFolder> child; + + // nsCString possibleName(aSpec->allocatedPathName); + uri.Append('/'); + uri.Append(dupFolderPath); + bool caseInsensitive = dupFolderPath.LowerCaseEqualsLiteral("inbox"); + rootFolder->GetChildWithURI(uri, true, caseInsensitive, + getter_AddRefs(child)); + // if we couldn't find this folder by URI, tell the imap code it's a new + // folder to us + *aNewFolder = !child; + if (child) found = true; + if (!found) { + // trying to find/discover the parent + if (haveParent) { + nsCOMPtr<nsIMsgFolder> parent; + bool parentIsNew; + caseInsensitive = parentName.LowerCaseEqualsLiteral("inbox"); + rootFolder->GetChildWithURI(parentUri, true, caseInsensitive, + getter_AddRefs(parent)); + if (!parent /* || parentFolder->GetFolderNeedsAdded()*/) { + PossibleImapMailbox( + parentName, hierarchyDelimiter, + kNoselect | // be defensive + ((boxFlags & // only inherit certain flags from the child + (kPublicMailbox | kOtherUsersMailbox | kPersonalMailbox))), + &parentIsNew); + } + } + rv = hostFolder->CreateClientSubfolderInfo( + dupFolderPath, hierarchyDelimiter, boxFlags, false); + NS_ENSURE_SUCCESS(rv, rv); + caseInsensitive = dupFolderPath.LowerCaseEqualsLiteral("inbox"); + rootFolder->GetChildWithURI(uri, true, caseInsensitive, + getter_AddRefs(child)); + } + if (child) { + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(child); + if (imapFolder) { + nsAutoCString onlineName; + nsAutoString unicodeName; + imapFolder->SetVerifiedAsOnlineFolder(true); + imapFolder->SetHierarchyDelimiter(hierarchyDelimiter); + if (boxFlags & kImapTrash) { + int32_t deleteModel; + GetDeleteModel(&deleteModel); + if (deleteModel == nsMsgImapDeleteModels::MoveToTrash) + child->SetFlag(nsMsgFolderFlags::Trash); + } + + imapFolder->SetBoxFlags(boxFlags); + imapFolder->SetExplicitlyVerify(explicitlyVerify); + imapFolder->GetOnlineName(onlineName); + + // online name needs to use the correct hierarchy delimiter (I think...) + // or the canonical path - one or the other, but be consistent. + dupFolderPath.ReplaceChar('/', hierarchyDelimiter); + if (hierarchyDelimiter != '/') { + nsImapUrl::UnescapeSlashes(dupFolderPath); + } + + // GMail gives us a localized name for the inbox but doesn't let + // us select that localized name. + if (boxFlags & kImapInbox) + imapFolder->SetOnlineName("INBOX"_ns); + else if (onlineName.IsEmpty() || !onlineName.Equals(dupFolderPath)) + imapFolder->SetOnlineName(dupFolderPath); + + if (hierarchyDelimiter != '/') { + nsImapUrl::UnescapeSlashes(folderName); + } + if (NS_SUCCEEDED(CopyFolderNameToUTF16(folderName, unicodeName))) + child->SetPrettyName(unicodeName); + } + } + if (!found && child) + child->SetMsgDatabase(nullptr); // close the db, so we don't hold open all + // the .msf files for new folders + return NS_OK; +} + +NS_IMETHODIMP nsImapIncomingServer::AddFolderRights( + const nsACString& mailboxName, const nsACString& userName, + const nsACString& rights) { + nsCOMPtr<nsIMsgFolder> rootFolder; + nsresult rv = GetRootFolder(getter_AddRefs(rootFolder)); + if (NS_SUCCEEDED(rv) && rootFolder) { + nsCOMPtr<nsIMsgImapMailFolder> imapRoot = do_QueryInterface(rootFolder); + if (imapRoot) { + nsCOMPtr<nsIMsgImapMailFolder> foundFolder; + rv = imapRoot->FindOnlineSubFolder(mailboxName, + getter_AddRefs(foundFolder)); + if (NS_SUCCEEDED(rv) && foundFolder) + return foundFolder->AddFolderRights(userName, rights); + } + } + return rv; +} + +NS_IMETHODIMP nsImapIncomingServer::FolderNeedsACLInitialized( + const nsACString& folderPath, bool* aNeedsACLInitialized) { + NS_ENSURE_ARG_POINTER(aNeedsACLInitialized); + nsCOMPtr<nsIMsgFolder> rootFolder; + nsresult rv = GetRootFolder(getter_AddRefs(rootFolder)); + if (NS_SUCCEEDED(rv) && rootFolder) { + nsCOMPtr<nsIMsgImapMailFolder> imapRoot = do_QueryInterface(rootFolder); + if (imapRoot) { + nsCOMPtr<nsIMsgImapMailFolder> foundFolder; + rv = imapRoot->FindOnlineSubFolder(folderPath, + getter_AddRefs(foundFolder)); + if (NS_SUCCEEDED(rv) && foundFolder) { + nsCOMPtr<nsIImapMailFolderSink> folderSink = + do_QueryInterface(foundFolder); + if (folderSink) + return folderSink->GetFolderNeedsACLListed(aNeedsACLInitialized); + } + } + } + *aNeedsACLInitialized = false; // maybe we want to say TRUE here... + return NS_OK; +} + +NS_IMETHODIMP nsImapIncomingServer::RefreshFolderRights( + const nsACString& folderPath) { + nsCOMPtr<nsIMsgFolder> rootFolder; + nsresult rv = GetRootFolder(getter_AddRefs(rootFolder)); + if (NS_SUCCEEDED(rv) && rootFolder) { + nsCOMPtr<nsIMsgImapMailFolder> imapRoot = do_QueryInterface(rootFolder); + if (imapRoot) { + nsCOMPtr<nsIMsgImapMailFolder> foundFolder; + rv = imapRoot->FindOnlineSubFolder(folderPath, + getter_AddRefs(foundFolder)); + if (NS_SUCCEEDED(rv) && foundFolder) + return foundFolder->RefreshFolderRights(); + } + } + return rv; +} + +nsresult nsImapIncomingServer::GetFolder(const nsACString& name, + nsIMsgFolder** pFolder) { + NS_ENSURE_ARG_POINTER(pFolder); + NS_ENSURE_TRUE(!name.IsEmpty(), NS_ERROR_FAILURE); + nsresult rv; + *pFolder = nullptr; + + nsCOMPtr<nsIMsgFolder> rootFolder; + rv = GetRootFolder(getter_AddRefs(rootFolder)); + if (NS_SUCCEEDED(rv) && rootFolder) { + nsCString uri; + rv = rootFolder->GetURI(uri); + if (NS_SUCCEEDED(rv) && !uri.IsEmpty()) { + nsAutoCString uriString(uri); + uriString.Append('/'); + uriString.Append(name); + rv = GetOrCreateFolder(uriString, pFolder); + } + } + return rv; +} + +NS_IMETHODIMP nsImapIncomingServer::OnlineFolderDelete( + const nsACString& aFolderName) { + return NS_OK; +} + +NS_IMETHODIMP nsImapIncomingServer::OnlineFolderCreateFailed( + const nsACString& aFolderName) { + return NS_OK; +} + +NS_IMETHODIMP nsImapIncomingServer::OnlineFolderRename( + nsIMsgWindow* msgWindow, const nsACString& oldName, + const nsACString& newName) { + nsresult rv = NS_ERROR_FAILURE; + if (!newName.IsEmpty()) { + nsCOMPtr<nsIMsgFolder> me; + rv = GetFolder(oldName, getter_AddRefs(me)); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIMsgFolder> parent; + nsCString tmpNewName(newName); + int32_t folderStart = tmpNewName.RFindChar('/'); + if (folderStart > 0) { + rv = GetFolder(StringHead(tmpNewName, folderStart), + getter_AddRefs(parent)); + } else // root is the parent + rv = GetRootFolder(getter_AddRefs(parent)); + if (NS_SUCCEEDED(rv) && parent) { + nsCOMPtr<nsIMsgImapMailFolder> folder; + folder = do_QueryInterface(me, &rv); + if (NS_SUCCEEDED(rv)) { + folder->RenameLocal(tmpNewName, parent); + nsCOMPtr<nsIMsgImapMailFolder> parentImapFolder = + do_QueryInterface(parent); + + if (parentImapFolder) + parentImapFolder->RenameClient(msgWindow, me, oldName, tmpNewName); + + nsCOMPtr<nsIMsgFolder> newFolder; + nsString unicodeNewName; + // `tmpNewName` is in MUTF-7 or UTF-8. It needs to be convert to UTF-8. + CopyFolderNameToUTF16(tmpNewName, unicodeNewName); + CopyUTF16toUTF8(unicodeNewName, tmpNewName); + rv = GetFolder(tmpNewName, getter_AddRefs(newFolder)); + if (NS_SUCCEEDED(rv)) { + newFolder->NotifyFolderEvent(kRenameCompleted); + } + } + } + } + return rv; +} + +NS_IMETHODIMP nsImapIncomingServer::FolderIsNoSelect( + const nsACString& aFolderName, bool* result) { + NS_ENSURE_ARG_POINTER(result); + nsCOMPtr<nsIMsgFolder> msgFolder; + nsresult rv = GetFolder(aFolderName, getter_AddRefs(msgFolder)); + if (NS_SUCCEEDED(rv) && msgFolder) { + uint32_t flags; + msgFolder->GetFlags(&flags); + *result = ((flags & nsMsgFolderFlags::ImapNoselect) != 0); + } else + *result = false; + return NS_OK; +} + +NS_IMETHODIMP nsImapIncomingServer::SetFolderAdminURL( + const nsACString& aFolderName, const nsACString& aFolderAdminUrl) { + nsCOMPtr<nsIMsgFolder> rootFolder; + nsresult rv = GetRootFolder(getter_AddRefs(rootFolder)); + if (NS_SUCCEEDED(rv) && rootFolder) { + nsCOMPtr<nsIMsgImapMailFolder> imapRoot = do_QueryInterface(rootFolder); + if (imapRoot) { + nsCOMPtr<nsIMsgImapMailFolder> foundFolder; + rv = imapRoot->FindOnlineSubFolder(aFolderName, + getter_AddRefs(foundFolder)); + if (NS_SUCCEEDED(rv) && foundFolder) + return foundFolder->SetAdminUrl(aFolderAdminUrl); + } + } + return rv; +} + +NS_IMETHODIMP nsImapIncomingServer::FolderVerifiedOnline( + const nsACString& folderName, bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + *aResult = false; + nsCOMPtr<nsIMsgFolder> rootFolder; + nsresult rv = GetRootFolder(getter_AddRefs(rootFolder)); + if (NS_SUCCEEDED(rv) && rootFolder) { + nsCOMPtr<nsIMsgFolder> folder; + rv = rootFolder->FindSubFolder(folderName, getter_AddRefs(folder)); + if (NS_SUCCEEDED(rv) && folder) { + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(folder); + if (imapFolder) imapFolder->GetVerifiedAsOnlineFolder(aResult); + } + } + return rv; +} + +NS_IMETHODIMP nsImapIncomingServer::DiscoveryDone() { + if (mDoingSubscribeDialog) return NS_OK; + + nsCOMPtr<nsIMsgFolder> rootMsgFolder; + nsresult rv = GetRootFolder(getter_AddRefs(rootMsgFolder)); + if (NS_SUCCEEDED(rv) && rootMsgFolder) { + // GetResource() may return a node which is not in the folder + // tree hierarchy but in the rdf cache in case of the non-existing default + // Sent, Drafts, and Templates folders. The resource will be eventually + // released when the rdf service shuts down. When we create the default + // folders later on in the imap server, the subsequent GetResource() of the + // same uri will get us the cached rdf resource which should have the folder + // flag set appropriately. + + nsCOMPtr<nsIMsgAccountManager> accountMgr = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgIdentity> identity; + rv = accountMgr->GetFirstIdentityForServer(this, getter_AddRefs(identity)); + if (NS_SUCCEEDED(rv) && identity) { + nsCString folderUri; + identity->GetFccFolder(folderUri); + nsCString existingUri; + + if (CheckSpecialFolder(folderUri, nsMsgFolderFlags::SentMail, + existingUri)) { + identity->SetFccFolder(existingUri); + identity->SetFccFolderPickerMode("1"_ns); + } + identity->GetDraftFolder(folderUri); + if (CheckSpecialFolder(folderUri, nsMsgFolderFlags::Drafts, + existingUri)) { + identity->SetDraftFolder(existingUri); + identity->SetDraftsFolderPickerMode("1"_ns); + } + bool archiveEnabled; + identity->GetArchiveEnabled(&archiveEnabled); + if (archiveEnabled) { + identity->GetArchiveFolder(folderUri); + if (CheckSpecialFolder(folderUri, nsMsgFolderFlags::Archive, + existingUri)) { + identity->SetArchiveFolder(existingUri); + identity->SetArchivesFolderPickerMode("1"_ns); + } + } + identity->GetStationeryFolder(folderUri); + if (!folderUri.IsEmpty()) { + nsCOMPtr<nsIMsgFolder> folder; + rv = GetOrCreateFolder(folderUri, getter_AddRefs(folder)); + if (NS_SUCCEEDED(rv)) rv = folder->SetFlag(nsMsgFolderFlags::Templates); + } + } + + nsCOMPtr<nsISpamSettings> spamSettings; + rv = GetSpamSettings(getter_AddRefs(spamSettings)); + if (NS_SUCCEEDED(rv) && spamSettings) { + nsCString spamFolderUri, existingUri; + spamSettings->GetSpamFolderURI(spamFolderUri); + if (CheckSpecialFolder(spamFolderUri, nsMsgFolderFlags::Junk, + existingUri)) { + // This only sets the cached values in the spam settings object. + spamSettings->SetActionTargetFolder(existingUri); + spamSettings->SetMoveTargetMode( + nsISpamSettings::MOVE_TARGET_MODE_FOLDER); + // Set the preferences too so that the values persist. + SetUnicharValue("spamActionTargetFolder", + NS_ConvertUTF8toUTF16(existingUri)); + SetIntValue("moveTargetMode", nsISpamSettings::MOVE_TARGET_MODE_FOLDER); + } + } + + bool isGMailServer; + GetIsGMailServer(&isGMailServer); + + // Verify there is only one trash folder. Another might be present if + // the trash name has been changed. Or we might be a gmail server and + // want to switch to gmail's trash folder. + nsTArray<RefPtr<nsIMsgFolder>> trashFolders; + rv = rootMsgFolder->GetFoldersWithFlags(nsMsgFolderFlags::Trash, + trashFolders); + + if (NS_SUCCEEDED(rv)) { + nsAutoString trashName; + if (NS_SUCCEEDED(GetTrashFolderName(trashName))) { + for (auto trashFolder : trashFolders) { + // If we're a gmail server, we clear the trash flags from folder(s) + // without the kImapXListTrash flag. For normal servers, we clear + // the trash folder flag if the folder name doesn't match the + // pref trash folder name. + nsAutoString retval; + rv = GetUnicharValue(PREF_TRASH_FOLDER_PATH, retval); + if (isGMailServer && (NS_FAILED(rv) || retval.IsEmpty())) { + nsCOMPtr<nsIMsgImapMailFolder> imapFolder( + do_QueryInterface(trashFolder)); + int32_t boxFlags; + imapFolder->GetBoxFlags(&boxFlags); + if (boxFlags & kImapXListTrash) { + continue; + } + } else { + // Store the trash folder path. We maintain the full path in the + // trash_folder_name preference since the full path is stored + // there when selecting a trash folder in the Account Manager. + nsAutoCString trashURL; + rv = trashFolder->GetFolderURL(trashURL); + if (NS_FAILED(rv)) { + continue; + } + nsCOMPtr<nsIURI> uri; + rv = NS_NewURI(getter_AddRefs(uri), trashURL); + if (NS_FAILED(rv)) { + continue; + } + nsAutoCString trashPath; + uri->GetPathQueryRef(trashPath); + nsAutoCString unescapedName; + MsgUnescapeString(Substring(trashPath, 1), // Skip leading slash. + nsINetUtil::ESCAPE_URL_PATH, unescapedName); + nsAutoString nameUnicode; + if (NS_FAILED(CopyFolderNameToUTF16(unescapedName, nameUnicode)) || + trashName.Equals(nameUnicode)) { + continue; + } + if (trashFolders.Length() == 1) { + // We got here because the preferred trash folder does not + // exist, but a folder got discovered to be the trash folder. + SetUnicharValue(PREF_TRASH_FOLDER_PATH, nameUnicode); + continue; + } + } + // We clear the trash folder flag if the trash folder path doesn't + // match mail.server.serverX.trash_folder_name. + trashFolder->ClearFlag(nsMsgFolderFlags::Trash); + } + } + } + } + + bool usingSubscription = true; + GetUsingSubscription(&usingSubscription); + + nsCOMArray<nsIMsgImapMailFolder> unverifiedFolders; + GetUnverifiedFolders(unverifiedFolders); + + int32_t count = unverifiedFolders.Count(); + for (int32_t k = 0; k < count; ++k) { + bool explicitlyVerify = false; + bool hasSubFolders = false; + uint32_t folderFlags; + nsCOMPtr<nsIMsgImapMailFolder> currentImapFolder(unverifiedFolders[k]); + nsCOMPtr<nsIMsgFolder> currentFolder( + do_QueryInterface(currentImapFolder, &rv)); + if (NS_FAILED(rv)) continue; + + currentFolder->GetFlags(&folderFlags); + if (folderFlags & + nsMsgFolderFlags::Virtual) // don't remove virtual folders + continue; + + if ((!usingSubscription || + (NS_SUCCEEDED( + currentImapFolder->GetExplicitlyVerify(&explicitlyVerify)) && + explicitlyVerify)) || + ((NS_SUCCEEDED(currentFolder->GetHasSubFolders(&hasSubFolders)) && + hasSubFolders) && + !NoDescendentsAreVerified(currentFolder))) { + bool isNamespace; + currentImapFolder->GetIsNamespace(&isNamespace); + if (!isNamespace) // don't list namespaces explicitly + { + // If there are no subfolders and this is unverified, we don't want to + // run this url. That is, we want to undiscover the folder. + // If there are subfolders and no descendants are verified, we want to + // undiscover all of the folders. + // Only if there are subfolders and at least one of them is verified + // do we want to refresh that folder's flags, because it won't be going + // away. + currentImapFolder->SetExplicitlyVerify(false); + currentImapFolder->List(); + } + } else { + nsCOMPtr<nsIMsgFolder> parent; + currentFolder->GetParent(getter_AddRefs(parent)); + if (parent) { + currentImapFolder->RemoveLocalSelf(); + } + } + } + + return rv; +} + +// Check if the special folder corresponding to the uri exists. If not, check +// if there already exists a folder with the special folder flag (the server may +// have told us about a folder to use through XLIST). If so, return the uri of +// the existing special folder. If not, set the special flag on the folder so +// it will be there if and when the folder is created. +// Return true if we found an existing special folder different than +// the one specified in prefs, and the one specified by prefs doesn't exist. +bool nsImapIncomingServer::CheckSpecialFolder(nsCString& folderUri, + uint32_t folderFlag, + nsCString& existingUri) { + nsCOMPtr<nsIMsgFolder> folder; + nsCOMPtr<nsIMsgFolder> rootMsgFolder; + nsresult rv = GetRootFolder(getter_AddRefs(rootMsgFolder)); + NS_ENSURE_SUCCESS(rv, false); + nsCOMPtr<nsIMsgFolder> existingFolder; + rootMsgFolder->GetFolderWithFlags(folderFlag, getter_AddRefs(existingFolder)); + + if (!folderUri.IsEmpty() && + NS_SUCCEEDED(GetOrCreateFolder(folderUri, getter_AddRefs(folder)))) { + nsCOMPtr<nsIMsgFolder> parent; + folder->GetParent(getter_AddRefs(parent)); + if (parent) { + existingFolder = nullptr; + } + if (!existingFolder) { + folder->SetFlag(folderFlag); + } + + nsString folderName; + folder->GetPrettyName(folderName); + // this will set the localized name based on the folder flag. + folder->SetPrettyName(folderName); + } + + if (existingFolder) { + existingFolder->GetURI(existingUri); + return true; + } + + return false; +} + +bool nsImapIncomingServer::NoDescendentsAreVerified( + nsIMsgFolder* parentFolder) { + nsTArray<RefPtr<nsIMsgFolder>> subFolders; + nsresult rv = parentFolder->GetSubFolders(subFolders); + if (NS_SUCCEEDED(rv)) { + for (nsIMsgFolder* child : subFolders) { + nsCOMPtr<nsIMsgImapMailFolder> childImapFolder = + do_QueryInterface(child, &rv); + if (NS_SUCCEEDED(rv) && childImapFolder) { + bool childVerified = false; + rv = childImapFolder->GetVerifiedAsOnlineFolder(&childVerified); + if (NS_SUCCEEDED(rv) && childVerified) { + return false; + } + if (!NoDescendentsAreVerified(child)) { + return false; + } + } + } + } + // If we get this far we didn't find any verified. + return true; +} + +bool nsImapIncomingServer::AllDescendentsAreNoSelect( + nsIMsgFolder* parentFolder) { + nsTArray<RefPtr<nsIMsgFolder>> subFolders; + nsresult rv = parentFolder->GetSubFolders(subFolders); + if (NS_SUCCEEDED(rv)) { + for (nsIMsgFolder* child : subFolders) { + nsCOMPtr<nsIMsgImapMailFolder> childImapFolder = + do_QueryInterface(child, &rv); + if (NS_SUCCEEDED(rv) && childImapFolder) { + uint32_t flags; + rv = child->GetFlags(&flags); + bool isNoSelect = + NS_SUCCEEDED(rv) && (flags & nsMsgFolderFlags::ImapNoselect); + if (!isNoSelect) { + return false; + } + if (!AllDescendentsAreNoSelect(child)) { + return false; + } + } + } + } + // If we get this far we found none without the Noselect flag. + return true; +} + +NS_IMETHODIMP +nsImapIncomingServer::PromptLoginFailed(nsIMsgWindow* aMsgWindow, + int32_t* aResult) { + nsAutoCString hostName; + GetHostName(hostName); + + nsAutoCString userName; + GetUsername(userName); + + nsAutoString accountName; + GetPrettyName(accountName); + + return MsgPromptLoginFailed(aMsgWindow, hostName, userName, accountName, + aResult); +} + +NS_IMETHODIMP +nsImapIncomingServer::FEAlert(const nsAString& aAlertString, + nsIMsgMailNewsUrl* aUrl) { + GetStringBundle(); + + if (m_stringBundle) { + nsAutoString hostName; + nsresult rv = GetPrettyName(hostName); + if (NS_SUCCEEDED(rv)) { + nsString message; + nsString tempString(aAlertString); + AutoTArray<nsString, 2> params = {hostName, tempString}; + + rv = m_stringBundle->FormatStringFromName("imapServerAlert", params, + message); + if (NS_SUCCEEDED(rv)) { + aUrl->SetErrorCode("imap-server-alert"_ns); + aUrl->SetErrorMessage(message); + + return AlertUser(message, aUrl); + } + } + } + return AlertUser(aAlertString, aUrl); +} + +nsresult nsImapIncomingServer::AlertUser(const nsAString& aString, + nsIMsgMailNewsUrl* aUrl) { + nsresult rv; + nsCOMPtr<nsIMsgMailSession> mailSession = + do_GetService("@mozilla.org/messenger/services/session;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + return mailSession->AlertUser(aString, aUrl); +} + +NS_IMETHODIMP +nsImapIncomingServer::FEAlertWithName(const char* aMsgName, + nsIMsgMailNewsUrl* aUrl) { + // don't bother the user if we're shutting down. + if (m_shuttingDown) return NS_OK; + + GetStringBundle(); + + nsString message; + + if (m_stringBundle) { + nsAutoCString hostName; + nsresult rv = GetHostName(hostName); + if (NS_SUCCEEDED(rv)) { + AutoTArray<nsString, 1> params; + CopyUTF8toUTF16(hostName, *params.AppendElement()); + rv = m_stringBundle->FormatStringFromName(aMsgName, params, message); + if (NS_SUCCEEDED(rv)) { + aUrl->SetErrorCode(nsDependentCString(aMsgName)); + aUrl->SetErrorMessage(message); + + return AlertUser(message, aUrl); + } + } + } + + // Error condition + message.AssignLiteral("String Name "); + message.AppendASCII(aMsgName); + FEAlert(message, aUrl); + return NS_OK; +} + +NS_IMETHODIMP nsImapIncomingServer::FEAlertFromServer( + const nsACString& aServerString, nsIMsgMailNewsUrl* aUrl) { + NS_ENSURE_TRUE(!aServerString.IsEmpty(), NS_OK); + + nsCString message(aServerString); + message.Trim(" \t\b\r\n"); + NS_ENSURE_TRUE(!message.IsEmpty(), NS_OK); + if (message.Last() != '.') message.Append('.'); + + // Skip over the first two words (the command tag and "NO"). + // Find the first word break. + int32_t pos = message.FindChar(' '); + + // Find the second word break. + if (pos != -1) pos = message.FindChar(' ', pos + 1); + + // Adjust the message. + if (pos != -1) message = Substring(message, pos + 1); + + nsString hostName; + GetPrettyName(hostName); + + AutoTArray<nsString, 3> formatStrings = {hostName}; + + const char* msgName; + nsString fullMessage; + nsCOMPtr<nsIImapUrl> imapUrl = do_QueryInterface(aUrl); + NS_ENSURE_TRUE(imapUrl, NS_ERROR_INVALID_ARG); + + nsImapState imapState; + nsImapAction imapAction; + + imapUrl->GetRequiredImapState(&imapState); + imapUrl->GetImapAction(&imapAction); + nsString folderName; + + NS_ConvertUTF8toUTF16 unicodeMsg(message); + + aUrl->SetErrorCode("imap-server-error"_ns); + aUrl->SetErrorMessage(unicodeMsg); + + nsCOMPtr<nsIMsgFolder> folder; + if (imapState == nsIImapUrl::nsImapSelectedState || + imapAction == nsIImapUrl::nsImapFolderStatus) { + aUrl->GetFolder(getter_AddRefs(folder)); + if (folder) folder->GetPrettyName(folderName); + msgName = "imapFolderCommandFailed"; + formatStrings.AppendElement(folderName); + } else { + msgName = "imapServerCommandFailed"; + } + + formatStrings.AppendElement(unicodeMsg); + + nsresult rv = GetStringBundle(); + NS_ENSURE_SUCCESS(rv, rv); + if (m_stringBundle) { + rv = m_stringBundle->FormatStringFromName(msgName, formatStrings, + fullMessage); + NS_ENSURE_SUCCESS(rv, rv); + } + + return AlertUser(fullMessage, aUrl); +} + +#define IMAP_MSGS_URL "chrome://messenger/locale/imapMsgs.properties" + +nsresult nsImapIncomingServer::GetStringBundle() { + if (m_stringBundle) return NS_OK; + + nsCOMPtr<nsIStringBundleService> sBundleService = + mozilla::components::StringBundle::Service(); + NS_ENSURE_TRUE(sBundleService, NS_ERROR_UNEXPECTED); + return sBundleService->CreateBundle(IMAP_MSGS_URL, + getter_AddRefs(m_stringBundle)); +} + +NS_IMETHODIMP +nsImapIncomingServer::GetImapStringByName(const char* msgName, + nsAString& aString) { + nsresult rv = NS_OK; + GetStringBundle(); + if (m_stringBundle) { + nsString res_str; + rv = m_stringBundle->GetStringFromName(msgName, res_str); + aString.Assign(res_str); + if (NS_SUCCEEDED(rv)) return rv; + } + aString.AssignLiteral("String Name "); + // mscott: FIX ME + aString.AppendASCII(msgName); + return NS_OK; +} + +nsresult nsImapIncomingServer::ResetFoldersToUnverified( + nsIMsgFolder* parentFolder) { + nsresult rv = NS_OK; + if (!parentFolder) { + nsCOMPtr<nsIMsgFolder> rootFolder; + rv = GetRootFolder(getter_AddRefs(rootFolder)); + NS_ENSURE_SUCCESS(rv, rv); + return ResetFoldersToUnverified(rootFolder); + } + + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = + do_QueryInterface(parentFolder, &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = imapFolder->SetVerifiedAsOnlineFolder(false); + nsTArray<RefPtr<nsIMsgFolder>> subFolders; + rv = parentFolder->GetSubFolders(subFolders); + NS_ENSURE_SUCCESS(rv, rv); + + for (nsIMsgFolder* child : subFolders) { + rv = ResetFoldersToUnverified(child); + NS_ENSURE_SUCCESS(rv, rv); + } + + return rv; +} + +void nsImapIncomingServer::GetUnverifiedFolders( + nsCOMArray<nsIMsgImapMailFolder>& aFoldersArray) { + nsCOMPtr<nsIMsgFolder> rootFolder; + if (NS_FAILED(GetRootFolder(getter_AddRefs(rootFolder))) || !rootFolder) + return; + + nsCOMPtr<nsIMsgImapMailFolder> imapRoot(do_QueryInterface(rootFolder)); + // don't need to verify the root. + if (imapRoot) imapRoot->SetVerifiedAsOnlineFolder(true); + + GetUnverifiedSubFolders(rootFolder, aFoldersArray); +} + +void nsImapIncomingServer::GetUnverifiedSubFolders( + nsIMsgFolder* parentFolder, + nsCOMArray<nsIMsgImapMailFolder>& aFoldersArray) { + nsCOMPtr<nsIMsgImapMailFolder> imapFolder(do_QueryInterface(parentFolder)); + + bool verified = false, explicitlyVerify = false; + if (imapFolder) { + nsresult rv = imapFolder->GetVerifiedAsOnlineFolder(&verified); + if (NS_SUCCEEDED(rv)) + rv = imapFolder->GetExplicitlyVerify(&explicitlyVerify); + + if (NS_SUCCEEDED(rv) && (!verified || explicitlyVerify)) + aFoldersArray.AppendObject(imapFolder); + } + + nsTArray<RefPtr<nsIMsgFolder>> subFolders; + if (NS_SUCCEEDED(parentFolder->GetSubFolders(subFolders))) { + for (nsIMsgFolder* child : subFolders) { + GetUnverifiedSubFolders(child, aFoldersArray); + } + } +} + +NS_IMETHODIMP nsImapIncomingServer::ForgetSessionPassword(bool modifyLogin) { + bool usingOauth2 = false; + if (modifyLogin) { + // Only need to check for oauth2 if modifyLogin is true. + int32_t authMethod = 0; + GetAuthMethod(&authMethod); + usingOauth2 = (authMethod == nsMsgAuthMethod::OAuth2); + } + + // Clear the cached password if not using Oauth2 or if modifyLogin is false. + if (!usingOauth2 || !modifyLogin) { + nsresult rv = nsMsgIncomingServer::ForgetSessionPassword(modifyLogin); + NS_ENSURE_SUCCESS(rv, rv); + + // fix for bugscape bug #15485 + // if we use turbo, and we logout, we need to make sure + // the server doesn't think it's authenticated. + // the biff timer continues to fire when you use turbo + // (see #143848). if we exited, we've set the password to null + // but if we're authenticated, and the biff timer goes off + // we'll still perform biff, because we use m_userAuthenticated + // to determine if we require a password for biff. + // (if authenticated, we don't require a password + // see nsMsgBiffManager::PerformBiff()) + // performing biff without a password will pop up the prompt dialog + // which is pretty wacky, when it happens after you quit the application + m_userAuthenticated = false; + } + return NS_OK; +} + +NS_IMETHODIMP nsImapIncomingServer::GetServerRequiresPasswordForBiff( + bool* aServerRequiresPasswordForBiff) { + NS_ENSURE_ARG_POINTER(aServerRequiresPasswordForBiff); + // if the user has already been authenticated, we've got the password + *aServerRequiresPasswordForBiff = !m_userAuthenticated; + return NS_OK; +} + +NS_IMETHODIMP nsImapIncomingServer::ForgetPassword() { + return nsMsgIncomingServer::ForgetPassword(); +} + +NS_IMETHODIMP +nsImapIncomingServer::AsyncGetPassword(nsIImapProtocol* aProtocol, + bool aNewPasswordRequested, + nsAString& aPassword) { + if (m_password.IsEmpty()) { + // We're now going to need to do something that will end up with us either + // poking login manager or prompting the user. We need to ensure we only + // do one prompt at a time (and login manager could cause a master password + // prompt), so we need to use the async prompter. + nsresult rv; + nsCOMPtr<nsIMsgAsyncPrompter> asyncPrompter = + do_GetService("@mozilla.org/messenger/msgAsyncPrompter;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgAsyncPromptListener> promptListener( + do_QueryInterface(aProtocol)); + rv = asyncPrompter->QueueAsyncAuthPrompt(m_serverKey, aNewPasswordRequested, + promptListener); + // Explicit NS_ENSURE_SUCCESS for debug purposes as errors tend to get + // hidden. + NS_ENSURE_SUCCESS(rv, rv); + } + if (!m_password.IsEmpty()) aPassword = m_password; + return NS_OK; +} + +// Get password already stored in login manager. This won't trigger a prompt +// if no password string is present. +NS_IMETHODIMP +nsImapIncomingServer::SyncGetPassword(nsAString& aPassword) { + nsresult rv = NS_OK; + if (NS_SUCCEEDED(GetPasswordWithoutUI()) && !m_password.IsEmpty()) + aPassword = m_password; + else + rv = NS_ERROR_NOT_AVAILABLE; + return rv; +} + +NS_IMETHODIMP +nsImapIncomingServer::PromptPassword(nsIMsgWindow* aMsgWindow, + nsAString& aPassword) { + nsAutoCString userName; + GetUsername(userName); + + nsAutoCString hostName; + GetHostName(hostName); + + nsresult rv = GetStringBundle(); + NS_ENSURE_SUCCESS(rv, rv); + + AutoTArray<nsString, 1> formatStrings; + CopyUTF8toUTF16(userName, *formatStrings.AppendElement()); + + nsString passwordTitle; + rv = m_stringBundle->FormatStringFromName( + "imapEnterPasswordPromptTitleWithUsername", formatStrings, passwordTitle); + NS_ENSURE_SUCCESS(rv, rv); + + AutoTArray<nsString, 2> formatStrings2; + CopyUTF8toUTF16(userName, *formatStrings2.AppendElement()); + CopyUTF8toUTF16(hostName, *formatStrings2.AppendElement()); + + nsString passwordText; + rv = m_stringBundle->FormatStringFromName("imapEnterServerPasswordPrompt", + formatStrings2, passwordText); + NS_ENSURE_SUCCESS(rv, rv); + + rv = GetPasswordWithUI(passwordText, passwordTitle, aPassword); + if (NS_SUCCEEDED(rv)) m_password = aPassword; + return rv; +} + +// for the nsIImapServerSink interface +NS_IMETHODIMP nsImapIncomingServer::SetCapability( + eIMAPCapabilityFlags capability) { + m_capability = capability; + SetIsGMailServer((capability & kGmailImapCapability) != 0); + return NS_OK; +} + +NS_IMETHODIMP +nsImapIncomingServer::GetCapability(eIMAPCapabilityFlags* capability) { + NS_ENSURE_ARG_POINTER(capability); + *capability = m_capability; + return NS_OK; +} + +NS_IMETHODIMP nsImapIncomingServer::SetServerID(const nsACString& aServerID) { + return SetServerIDPref(aServerID); +} + +NS_IMETHODIMP nsImapIncomingServer::CommitNamespaces() { + nsresult rv; + nsCOMPtr<nsIImapHostSessionList> hostSession = + do_GetService(kCImapHostSessionListCID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + return hostSession->CommitNamespacesForHost(this); +} + +NS_IMETHODIMP nsImapIncomingServer::PseudoInterruptMsgLoad( + nsIMsgFolder* aImapFolder, nsIMsgWindow* aMsgWindow, bool* interrupted) { + nsresult rv = NS_OK; + nsCOMPtr<nsIImapProtocol> connection; + PR_CEnterMonitor(this); + // iterate through the connection cache for a connection that is loading + // a message in this folder and should be pseudo-interrupted. + int32_t cnt = m_connectionCache.Count(); + + for (int32_t i = 0; i < cnt; ++i) { + connection = m_connectionCache[i]; + if (connection) + rv = connection->PseudoInterruptMsgLoad(aImapFolder, aMsgWindow, + interrupted); + } + + PR_CExitMonitor(this); + return rv; +} + +NS_IMETHODIMP nsImapIncomingServer::ResetNamespaceReferences() { + nsCOMPtr<nsIMsgFolder> rootFolder; + nsresult rv = GetRootFolder(getter_AddRefs(rootFolder)); + if (NS_SUCCEEDED(rv) && rootFolder) { + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(rootFolder); + if (imapFolder) rv = imapFolder->ResetNamespaceReferences(); + } + return rv; +} + +NS_IMETHODIMP nsImapIncomingServer::SetUserAuthenticated( + bool aUserAuthenticated) { + m_userAuthenticated = aUserAuthenticated; + if (aUserAuthenticated) { + nsresult rv; + nsCOMPtr<nsIMsgAccountManager> accountManager = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + accountManager->SetUserNeedsToAuthenticate(false); + } + return NS_OK; +} + +NS_IMETHODIMP nsImapIncomingServer::GetUserAuthenticated( + bool* aUserAuthenticated) { + NS_ENSURE_ARG_POINTER(aUserAuthenticated); + *aUserAuthenticated = m_userAuthenticated; + return NS_OK; +} + +/* void SetMailServerUrls (in string manageMailAccount, in string manageLists, + * in string manageFilters); */ +NS_IMETHODIMP nsImapIncomingServer::SetMailServerUrls( + const nsACString& manageMailAccount, const nsACString& manageLists, + const nsACString& manageFilters) { + return SetManageMailAccountUrl(manageMailAccount); +} + +NS_IMETHODIMP nsImapIncomingServer::SetManageMailAccountUrl( + const nsACString& manageMailAccountUrl) { + m_manageMailAccountUrl = manageMailAccountUrl; + return NS_OK; +} + +NS_IMETHODIMP nsImapIncomingServer::GetManageMailAccountUrl( + nsACString& manageMailAccountUrl) { + manageMailAccountUrl = m_manageMailAccountUrl; + return NS_OK; +} + +NS_IMETHODIMP +nsImapIncomingServer::StartPopulatingWithUri(nsIMsgWindow* aMsgWindow, + bool aForceToServer /*ignored*/, + const nsACString& uri) { + nsresult rv; + mDoingSubscribeDialog = true; + + rv = EnsureInner(); + NS_ENSURE_SUCCESS(rv, rv); + rv = mInner->StartPopulatingWithUri(aMsgWindow, aForceToServer, uri); + NS_ENSURE_SUCCESS(rv, rv); + + // imap always uses the canonical delimiter form of paths for subscribe ui. + rv = SetDelimiter('/'); + NS_ENSURE_SUCCESS(rv, rv); + + rv = SetShowFullName(false); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString serverUri; + rv = GetServerURI(serverUri); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + /* + if uri = imap://user@host/foo/bar, the serverUri is imap://user@host + to get path from uri, skip over imap://user@host + 1 (for the /) + */ + return imapService->GetListOfFoldersWithPath( + this, aMsgWindow, Substring(uri, serverUri.Length() + 1)); +} + +NS_IMETHODIMP +nsImapIncomingServer::StartPopulating(nsIMsgWindow* aMsgWindow, + bool aForceToServer /*ignored*/, + bool aGetOnlyNew) { + nsresult rv; + mDoingSubscribeDialog = true; + + rv = EnsureInner(); + NS_ENSURE_SUCCESS(rv, rv); + rv = mInner->StartPopulating(aMsgWindow, aForceToServer, aGetOnlyNew); + NS_ENSURE_SUCCESS(rv, rv); + + // imap always uses the canonical delimiter form of paths for subscribe ui. + rv = SetDelimiter('/'); + NS_ENSURE_SUCCESS(rv, rv); + + rv = SetShowFullName(false); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + return imapService->GetListOfFoldersOnServer(this, aMsgWindow); +} + +NS_IMETHODIMP +nsImapIncomingServer::OnStartRunningUrl(nsIURI* url) { return NS_OK; } + +NS_IMETHODIMP +nsImapIncomingServer::OnStopRunningUrl(nsIURI* url, nsresult exitCode) { + nsresult rv = exitCode; + + // xxx todo get msgWindow from url + nsCOMPtr<nsIMsgWindow> msgWindow; + nsCOMPtr<nsIImapUrl> imapUrl = do_QueryInterface(url); + if (imapUrl) { + nsImapAction imapAction = nsIImapUrl::nsImapTest; + imapUrl->GetImapAction(&imapAction); + switch (imapAction) { + case nsIImapUrl::nsImapDiscoverAllAndSubscribedBoxesUrl: + case nsIImapUrl::nsImapDiscoverChildrenUrl: + rv = UpdateSubscribed(); + NS_ENSURE_SUCCESS(rv, rv); + mDoingSubscribeDialog = false; + rv = StopPopulating(msgWindow); + NS_ENSURE_SUCCESS(rv, rv); + break; + case nsIImapUrl::nsImapDiscoverAllBoxesUrl: + if (NS_SUCCEEDED(exitCode)) DiscoveryDone(); + break; + case nsIImapUrl::nsImapFolderStatus: { + nsCOMPtr<nsIMsgFolder> msgFolder; + nsCOMPtr<nsIMsgMailNewsUrl> mailUrl = do_QueryInterface(imapUrl); + mailUrl->GetFolder(getter_AddRefs(msgFolder)); + if (msgFolder) { + nsresult rv; + nsCOMPtr<nsIMsgMailSession> session = + do_GetService("@mozilla.org/messenger/services/session;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + bool folderOpen; + rv = session->IsFolderOpenInWindow(msgFolder, &folderOpen); + if (NS_SUCCEEDED(rv) && !folderOpen && msgFolder) + msgFolder->SetMsgDatabase(nullptr); + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = + do_QueryInterface(msgFolder); + m_foldersToStat.RemoveObject(imapFolder); + } + // if we get an error running the url, it's better + // not to chain the next url. + if (NS_FAILED(exitCode) && exitCode != NS_MSG_ERROR_IMAP_COMMAND_FAILED) + m_foldersToStat.Clear(); + if (m_foldersToStat.Count() > 0) + m_foldersToStat[0]->UpdateStatus(this, nullptr); + break; + } + default: + break; + } + } + return NS_OK; +} + +NS_IMETHODIMP +nsImapIncomingServer::SetIncomingServer(nsIMsgIncomingServer* aServer) { + nsresult rv = EnsureInner(); + NS_ENSURE_SUCCESS(rv, rv); + return mInner->SetIncomingServer(aServer); +} + +NS_IMETHODIMP +nsImapIncomingServer::SetShowFullName(bool showFullName) { + nsresult rv = EnsureInner(); + NS_ENSURE_SUCCESS(rv, rv); + return mInner->SetShowFullName(showFullName); +} + +NS_IMETHODIMP +nsImapIncomingServer::GetDelimiter(char* aDelimiter) { + nsresult rv = EnsureInner(); + NS_ENSURE_SUCCESS(rv, rv); + return mInner->GetDelimiter(aDelimiter); +} + +NS_IMETHODIMP +nsImapIncomingServer::SetDelimiter(char aDelimiter) { + nsresult rv = EnsureInner(); + NS_ENSURE_SUCCESS(rv, rv); + return mInner->SetDelimiter(aDelimiter); +} + +NS_IMETHODIMP +nsImapIncomingServer::SetAsSubscribed(const nsACString& path) { + nsresult rv = EnsureInner(); + NS_ENSURE_SUCCESS(rv, rv); + return mInner->SetAsSubscribed(path); +} + +NS_IMETHODIMP +nsImapIncomingServer::UpdateSubscribed() { return NS_OK; } + +NS_IMETHODIMP +nsImapIncomingServer::AddTo(const nsACString& aName, bool addAsSubscribed, + bool aSubscribable, bool changeIfExists) { + nsresult rv = EnsureInner(); + NS_ENSURE_SUCCESS(rv, rv); + + // RFC 3501 allows UTF-8 in addition to MUTF-7. + // If it's not UTF-8, it's not 7bit-ASCII and cannot be MUTF-7 either. + // We just ignore it. + if (!mozilla::IsUtf8(aName)) return NS_OK; + // Now handle subscription folder names as UTF-8 so don't convert to MUTF-7. + return mInner->AddTo(aName, addAsSubscribed, aSubscribable, changeIfExists); +} + +NS_IMETHODIMP +nsImapIncomingServer::StopPopulating(nsIMsgWindow* aMsgWindow) { + nsresult rv = EnsureInner(); + NS_ENSURE_SUCCESS(rv, rv); + return mInner->StopPopulating(aMsgWindow); +} + +NS_IMETHODIMP +nsImapIncomingServer::SubscribeCleanup() { + m_subscribeFolders.Clear(); + return ClearInner(); +} + +NS_IMETHODIMP +nsImapIncomingServer::SetSubscribeListener(nsISubscribeListener* aListener) { + nsresult rv = EnsureInner(); + NS_ENSURE_SUCCESS(rv, rv); + return mInner->SetSubscribeListener(aListener); +} + +NS_IMETHODIMP +nsImapIncomingServer::GetSubscribeListener(nsISubscribeListener** aListener) { + nsresult rv = EnsureInner(); + NS_ENSURE_SUCCESS(rv, rv); + return mInner->GetSubscribeListener(aListener); +} + +NS_IMETHODIMP +nsImapIncomingServer::Subscribe(const char16_t* aName) { + NS_ENSURE_ARG_POINTER(aName); + return SubscribeToFolder(nsDependentString(aName), true, nullptr); +} + +NS_IMETHODIMP +nsImapIncomingServer::Unsubscribe(const char16_t* aName) { + NS_ENSURE_ARG_POINTER(aName); + + return SubscribeToFolder(nsDependentString(aName), false, nullptr); +} + +NS_IMETHODIMP +nsImapIncomingServer::SubscribeToFolder(const nsAString& aName, bool subscribe, + nsIURI** aUri) { + nsresult rv; + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgFolder> rootMsgFolder; + rv = GetRootFolder(getter_AddRefs(rootMsgFolder)); + NS_ENSURE_SUCCESS(rv, rv); + + // Locate the folder so that the correct hierarchical delimiter is used in the + // folder pathnames, otherwise root's (ie, '^') is used and this is wrong. + + // aName is not a genuine UTF-16 but just a zero-padded MUTF-7. + NS_ConvertUTF16toUTF8 folderCName(aName); + nsCOMPtr<nsIMsgFolder> msgFolder; + if (rootMsgFolder && !aName.IsEmpty()) + rv = rootMsgFolder->FindSubFolder(folderCName, getter_AddRefs(msgFolder)); + + nsCOMPtr<nsIThread> thread(do_GetCurrentThread()); + + if (subscribe) + rv = imapService->SubscribeFolder(msgFolder, aName, nullptr, aUri); + else + rv = imapService->UnsubscribeFolder(msgFolder, aName, nullptr, nullptr); + + return rv; +} + +NS_IMETHODIMP +nsImapIncomingServer::SetDoingLsub(bool doingLsub) { + mDoingLsub = doingLsub; + return NS_OK; +} + +NS_IMETHODIMP +nsImapIncomingServer::GetDoingLsub(bool* doingLsub) { + NS_ENSURE_ARG_POINTER(doingLsub); + *doingLsub = mDoingLsub; + return NS_OK; +} + +NS_IMETHODIMP +nsImapIncomingServer::SetUtf8AcceptEnabled(bool enabled) { + mUtf8AcceptEnabled = enabled; + return NS_OK; +} + +NS_IMETHODIMP +nsImapIncomingServer::GetUtf8AcceptEnabled(bool* enabled) { + NS_ENSURE_ARG_POINTER(enabled); + *enabled = mUtf8AcceptEnabled; + return NS_OK; +} + +NS_IMETHODIMP +nsImapIncomingServer::ReDiscoverAllFolders() { return PerformExpand(nullptr); } + +NS_IMETHODIMP +nsImapIncomingServer::SetState(const nsACString& path, bool state, + bool* stateChanged) { + nsresult rv = EnsureInner(); + NS_ENSURE_SUCCESS(rv, rv); + return mInner->SetState(path, state, stateChanged); +} + +NS_IMETHODIMP +nsImapIncomingServer::HasChildren(const nsACString& path, bool* aHasChildren) { + nsresult rv = EnsureInner(); + NS_ENSURE_SUCCESS(rv, rv); + return mInner->HasChildren(path, aHasChildren); +} + +NS_IMETHODIMP +nsImapIncomingServer::IsSubscribed(const nsACString& path, + bool* aIsSubscribed) { + nsresult rv = EnsureInner(); + NS_ENSURE_SUCCESS(rv, rv); + return mInner->IsSubscribed(path, aIsSubscribed); +} + +NS_IMETHODIMP +nsImapIncomingServer::IsSubscribable(const nsACString& path, + bool* aIsSubscribable) { + nsresult rv = EnsureInner(); + NS_ENSURE_SUCCESS(rv, rv); + return mInner->IsSubscribable(path, aIsSubscribable); +} + +NS_IMETHODIMP +nsImapIncomingServer::GetLeafName(const nsACString& path, + nsAString& aLeafName) { + nsresult rv = EnsureInner(); + NS_ENSURE_SUCCESS(rv, rv); + return mInner->GetLeafName(path, aLeafName); +} + +NS_IMETHODIMP +nsImapIncomingServer::GetFirstChildURI(const nsACString& path, + nsACString& aResult) { + nsresult rv = EnsureInner(); + NS_ENSURE_SUCCESS(rv, rv); + return mInner->GetFirstChildURI(path, aResult); +} + +NS_IMETHODIMP +nsImapIncomingServer::GetChildURIs(const nsACString& aPath, + nsTArray<nsCString>& aResult) { + nsresult rv = EnsureInner(); + NS_ENSURE_SUCCESS(rv, rv); + return mInner->GetChildURIs(aPath, aResult); +} + +nsresult nsImapIncomingServer::EnsureInner() { + nsresult rv = NS_OK; + + if (mInner) return NS_OK; + + mInner = do_CreateInstance(kSubscribableServerCID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + return SetIncomingServer(this); +} + +nsresult nsImapIncomingServer::ClearInner() { + nsresult rv = NS_OK; + if (mInner) { + rv = mInner->SetSubscribeListener(nullptr); + NS_ENSURE_SUCCESS(rv, rv); + rv = mInner->SetIncomingServer(nullptr); + NS_ENSURE_SUCCESS(rv, rv); + mInner = nullptr; + } + return NS_OK; +} + +NS_IMETHODIMP +nsImapIncomingServer::CommitSubscribeChanges() { + return ReDiscoverAllFolders(); +} + +NS_IMETHODIMP +nsImapIncomingServer::GetCanBeDefaultServer(bool* canBeDefaultServer) { + NS_ENSURE_ARG_POINTER(canBeDefaultServer); + *canBeDefaultServer = true; + return NS_OK; +} + +NS_IMETHODIMP +nsImapIncomingServer::GetCanCompactFoldersOnServer( + bool* canCompactFoldersOnServer) { + NS_ENSURE_ARG_POINTER(canCompactFoldersOnServer); + // Initialize canCompactFoldersOnServer true, a default value for IMAP + *canCompactFoldersOnServer = true; + GetPrefForServerAttribute("canCompactFoldersOnServer", + canCompactFoldersOnServer); + return NS_OK; +} + +NS_IMETHODIMP +nsImapIncomingServer::GetCanUndoDeleteOnServer(bool* canUndoDeleteOnServer) { + NS_ENSURE_ARG_POINTER(canUndoDeleteOnServer); + // Initialize canUndoDeleteOnServer true, a default value for IMAP + *canUndoDeleteOnServer = true; + GetPrefForServerAttribute("canUndoDeleteOnServer", canUndoDeleteOnServer); + return NS_OK; +} + +NS_IMETHODIMP +nsImapIncomingServer::GetCanSearchMessages(bool* canSearchMessages) { + NS_ENSURE_ARG_POINTER(canSearchMessages); + // Initialize canSearchMessages true, a default value for IMAP + *canSearchMessages = true; + GetPrefForServerAttribute("canSearchMessages", canSearchMessages); + return NS_OK; +} + +nsresult nsImapIncomingServer::CreateHostSpecificPrefName( + const char* prefPrefix, nsAutoCString& prefName) { + NS_ENSURE_ARG_POINTER(prefPrefix); + + nsCString hostName; + nsresult rv = GetHostName(hostName); + NS_ENSURE_SUCCESS(rv, rv); + + prefName = prefPrefix; + prefName.Append('.'); + prefName.Append(hostName); + return NS_OK; +} + +NS_IMETHODIMP +nsImapIncomingServer::GetSupportsDiskSpace(bool* aSupportsDiskSpace) { + NS_ENSURE_ARG_POINTER(aSupportsDiskSpace); + nsAutoCString prefName; + nsresult rv = + CreateHostSpecificPrefName("default_supports_diskspace", prefName); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIPrefBranch> prefBranch = + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv); + if (NS_SUCCEEDED(rv)) + rv = prefBranch->GetBoolPref(prefName.get(), aSupportsDiskSpace); + + // Couldn't get the default value with the hostname. + // Fall back on IMAP default value + if (NS_FAILED(rv)) // set default value + *aSupportsDiskSpace = true; + return NS_OK; +} + +// count number of non-busy connections in cache +NS_IMETHODIMP +nsImapIncomingServer::GetNumIdleConnections(int32_t* aNumIdleConnections) { + NS_ENSURE_ARG_POINTER(aNumIdleConnections); + *aNumIdleConnections = 0; + + nsresult rv = NS_OK; + nsCOMPtr<nsIImapProtocol> connection; + bool isBusy = false; + bool isInboxConnection; + PR_CEnterMonitor(this); + + int32_t cnt = m_connectionCache.Count(); + + // loop counting idle connections + for (int32_t i = 0; i < cnt; ++i) { + connection = m_connectionCache[i]; + if (connection) { + rv = connection->IsBusy(&isBusy, &isInboxConnection); + if (NS_FAILED(rv)) continue; + if (!isBusy) (*aNumIdleConnections)++; + } + } + PR_CExitMonitor(this); + return rv; +} + +/** + * Get the preference that tells us whether the imap server in question allows + * us to create subfolders. Some ISPs might not want users to create any folders + * besides the existing ones. + * We do want to identify all those servers that don't allow creation of + * subfolders and take them out of the account picker in the Copies and Folder + * panel. + */ +NS_IMETHODIMP +nsImapIncomingServer::GetCanCreateFoldersOnServer( + bool* aCanCreateFoldersOnServer) { + NS_ENSURE_ARG_POINTER(aCanCreateFoldersOnServer); + // Initialize aCanCreateFoldersOnServer true, a default value for IMAP + *aCanCreateFoldersOnServer = true; + GetPrefForServerAttribute("canCreateFolders", aCanCreateFoldersOnServer); + return NS_OK; +} + +NS_IMETHODIMP +nsImapIncomingServer::GetOfflineSupportLevel(int32_t* aSupportLevel) { + NS_ENSURE_ARG_POINTER(aSupportLevel); + nsresult rv = NS_OK; + + rv = GetIntValue("offline_support_level", aSupportLevel); + if (*aSupportLevel != OFFLINE_SUPPORT_LEVEL_UNDEFINED) return rv; + + nsAutoCString prefName; + rv = CreateHostSpecificPrefName("default_offline_support_level", prefName); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIPrefBranch> prefBranch = + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv); + if (NS_SUCCEEDED(rv)) + rv = prefBranch->GetIntPref(prefName.get(), aSupportLevel); + + // Couldn't get the pref value with the hostname. + // Fall back on IMAP default value + if (NS_FAILED(rv)) // set default value + *aSupportLevel = OFFLINE_SUPPORT_LEVEL_REGULAR; + return NS_OK; +} + +// Called only during the migration process. This routine enables the generation +// of unique account name based on the username, hostname and the port. If the +// port is valid and not a default one, it will be appended to the account name. +NS_IMETHODIMP +nsImapIncomingServer::GeneratePrettyNameForMigration(nsAString& aPrettyName) { + nsCString userName; + nsCString hostName; + + /** + * Pretty name for migrated account is of format username@hostname:<port>, + * provided the port is valid and not the default + */ + // Get user name to construct pretty name + nsresult rv = GetUsername(userName); + NS_ENSURE_SUCCESS(rv, rv); + + // Get host name to construct pretty name + rv = GetHostName(hostName); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t defaultServerPort; + int32_t defaultSecureServerPort; + + // Here, the final contract ID is already known, so use it directly for + // efficiency. + nsCOMPtr<nsIMsgProtocolInfo> protocolInfo = + do_GetService("@mozilla.org/messenger/protocol/info;1?type=imap", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // Get the default port + rv = protocolInfo->GetDefaultServerPort(false, &defaultServerPort); + NS_ENSURE_SUCCESS(rv, rv); + + // Get the default secure port + rv = protocolInfo->GetDefaultServerPort(true, &defaultSecureServerPort); + NS_ENSURE_SUCCESS(rv, rv); + + // Get the current server port + int32_t serverPort = PORT_NOT_SET; + rv = GetPort(&serverPort); + NS_ENSURE_SUCCESS(rv, rv); + + // Is the server secure ? + int32_t socketType; + rv = GetSocketType(&socketType); + NS_ENSURE_SUCCESS(rv, rv); + bool isSecure = (socketType == nsMsgSocketType::SSL); + + // Is server port a default port ? + bool isItDefaultPort = false; + if (((serverPort == defaultServerPort) && !isSecure) || + ((serverPort == defaultSecureServerPort) && isSecure)) + isItDefaultPort = true; + + // Construct pretty name from username and hostname + nsAutoString constructedPrettyName; + CopyASCIItoUTF16(userName, constructedPrettyName); + constructedPrettyName.Append('@'); + constructedPrettyName.Append(NS_ConvertASCIItoUTF16(hostName)); + + // If the port is valid and not default, add port value to the pretty name + if ((serverPort > 0) && (!isItDefaultPort)) { + constructedPrettyName.Append(':'); + constructedPrettyName.AppendInt(serverPort); + } + + // Format the pretty name + return GetFormattedStringFromName(constructedPrettyName, + "imapDefaultAccountName", aPrettyName); +} + +nsresult nsImapIncomingServer::GetFormattedStringFromName( + const nsAString& aValue, const char* aName, nsAString& aResult) { + nsresult rv = GetStringBundle(); + if (m_stringBundle) { + nsString tmpVal(aValue); + AutoTArray<nsString, 1> formatStrings = {tmpVal}; + + nsString result; + rv = m_stringBundle->FormatStringFromName(aName, formatStrings, result); + aResult.Assign(result); + } + return rv; +} + +nsresult nsImapIncomingServer::GetPrefForServerAttribute(const char* prefSuffix, + bool* prefValue) { + // Any caller of this function must initialize prefValue with a default value + // as this code will not set prefValue when the pref does not exist and return + // NS_OK anyway + + if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED; + + NS_ENSURE_ARG_POINTER(prefValue); + + if (NS_FAILED(mPrefBranch->GetBoolPref(prefSuffix, prefValue))) + mDefPrefBranch->GetBoolPref(prefSuffix, prefValue); + + return NS_OK; +} + +NS_IMETHODIMP +nsImapIncomingServer::GetCanFileMessagesOnServer( + bool* aCanFileMessagesOnServer) { + NS_ENSURE_ARG_POINTER(aCanFileMessagesOnServer); + // Initialize aCanFileMessagesOnServer true, a default value for IMAP + *aCanFileMessagesOnServer = true; + GetPrefForServerAttribute("canFileMessages", aCanFileMessagesOnServer); + return NS_OK; +} + +NS_IMETHODIMP +nsImapIncomingServer::SetSearchValue(const nsAString& searchValue) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsImapIncomingServer::GetSupportsSubscribeSearch(bool* retVal) { + NS_ENSURE_ARG_POINTER(retVal); + *retVal = false; + return NS_OK; +} + +NS_IMETHODIMP +nsImapIncomingServer::GetFolderView(nsITreeView** aView) { + nsresult rv = EnsureInner(); + NS_ENSURE_SUCCESS(rv, rv); + return mInner->GetFolderView(aView); +} + +NS_IMETHODIMP +nsImapIncomingServer::GetFilterScope(nsMsgSearchScopeValue* filterScope) { + NS_ENSURE_ARG_POINTER(filterScope); + // If the inbox is enabled for offline use, then use the offline filter + // scope, else use the online filter scope. + // + // XXX We use the same scope for all folders with the same incoming server, + // yet it is possible to set the offline flag separately for each folder. + // Manual filters could perhaps check the offline status of each folder, + // though it's hard to see how to make that work since we only store filters + // per server. + // + nsCOMPtr<nsIMsgFolder> rootMsgFolder; + nsresult rv = GetRootMsgFolder(getter_AddRefs(rootMsgFolder)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgFolder> offlineInboxMsgFolder; + rv = rootMsgFolder->GetFolderWithFlags( + nsMsgFolderFlags::Inbox | nsMsgFolderFlags::Offline, + getter_AddRefs(offlineInboxMsgFolder)); + + *filterScope = offlineInboxMsgFolder ? nsMsgSearchScope::offlineMailFilter + : nsMsgSearchScope::onlineMailFilter; + return NS_OK; +} + +NS_IMETHODIMP +nsImapIncomingServer::GetSearchScope(nsMsgSearchScopeValue* searchScope) { + NS_ENSURE_ARG_POINTER(searchScope); + *searchScope = WeAreOffline() ? nsMsgSearchScope::offlineMail + : nsMsgSearchScope::onlineMail; + return NS_OK; +} + +// This is a recursive function. It gets new messages for current folder +// first if it is marked, then calls itself recursively for each subfolder. +NS_IMETHODIMP +nsImapIncomingServer::GetNewMessagesForNonInboxFolders(nsIMsgFolder* aFolder, + nsIMsgWindow* aWindow, + bool forceAllFolders, + bool performingBiff) { + NS_ENSURE_ARG_POINTER(aFolder); + static bool gGotStatusPref = false; + static bool gUseStatus = false; + + bool isServer; + (void)aFolder->GetIsServer(&isServer); + // Check this folder for new messages if it is marked to be checked + // or if we are forced to check all folders + uint32_t flags = 0; + aFolder->GetFlags(&flags); + nsresult rv; + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(aFolder, &rv); + NS_ENSURE_SUCCESS(rv, rv); + bool canOpen; + imapFolder->GetCanOpenFolder(&canOpen); + if (canOpen && + ((forceAllFolders && + !(flags & (nsMsgFolderFlags::Inbox | nsMsgFolderFlags::Trash | + nsMsgFolderFlags::Junk | nsMsgFolderFlags::Virtual))) || + flags & nsMsgFolderFlags::CheckNew)) { + // Get new messages for this folder. + aFolder->SetGettingNewMessages(true); + if (performingBiff) imapFolder->SetPerformingBiff(true); + bool isOpen = false; + nsCOMPtr<nsIMsgMailSession> mailSession = + do_GetService("@mozilla.org/messenger/services/session;1"); + if (mailSession && aFolder) + mailSession->IsFolderOpenInWindow(aFolder, &isOpen); + // eventually, the gGotStatusPref should go away, once we work out the kinks + // from using STATUS. + if (!gGotStatusPref) { + nsCOMPtr<nsIPrefBranch> prefBranch = + do_GetService(NS_PREFSERVICE_CONTRACTID); + if (prefBranch) + prefBranch->GetBoolPref("mail.imap.use_status_for_biff", &gUseStatus); + gGotStatusPref = true; + } + if (gUseStatus && !isOpen) { + if (!isServer && m_foldersToStat.IndexOf(imapFolder) == -1) + m_foldersToStat.AppendObject(imapFolder); + } else + aFolder->UpdateFolder(aWindow); + } + + // Loop through all subfolders to get new messages for them. + nsTArray<RefPtr<nsIMsgFolder>> subFolders; + rv = aFolder->GetSubFolders(subFolders); + NS_ENSURE_SUCCESS(rv, rv); + for (nsIMsgFolder* msgFolder : subFolders) { + GetNewMessagesForNonInboxFolders(msgFolder, aWindow, forceAllFolders, + performingBiff); + } + if (isServer && m_foldersToStat.Count() > 0) + m_foldersToStat[0]->UpdateStatus(this, nullptr); + return NS_OK; +} + +NS_IMETHODIMP +nsImapIncomingServer::GetArbitraryHeaders(nsACString& aResult) { + nsCOMPtr<nsIMsgFilterList> filterList; + nsresult rv = GetFilterList(nullptr, getter_AddRefs(filterList)); + NS_ENSURE_SUCCESS(rv, rv); + return filterList->GetArbitraryHeaders(aResult); +} + +NS_IMETHODIMP +nsImapIncomingServer::GetShowAttachmentsInline(bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + *aResult = true; // true per default + nsresult rv; + nsCOMPtr<nsIPrefBranch> prefBranch = + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + prefBranch->GetBoolPref("mail.inline_attachments", aResult); + return NS_OK; // In case this pref is not set we need to return NS_OK. +} + +NS_IMETHODIMP nsImapIncomingServer::SetSocketType(int32_t aSocketType) { + int32_t oldSocketType; + nsresult rv = GetSocketType(&oldSocketType); + if (NS_SUCCEEDED(rv) && oldSocketType != aSocketType) + CloseCachedConnections(); + return nsMsgIncomingServer::SetSocketType(aSocketType); +} + +// use canonical format in originalUri & convertedUri +NS_IMETHODIMP +nsImapIncomingServer::GetUriWithNamespacePrefixIfNecessary( + int32_t namespaceType, const nsACString& originalUri, + nsACString& convertedUri) { + nsresult rv = NS_OK; + nsAutoCString serverKey; + rv = GetKey(serverKey); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIImapHostSessionList> hostSessionList = + do_GetService(kCImapHostSessionListCID, &rv); + nsImapNamespace* ns = nullptr; + rv = hostSessionList->GetDefaultNamespaceOfTypeForHost( + serverKey.get(), (EIMAPNamespaceType)namespaceType, ns); + if (ns) { + nsAutoCString namespacePrefix(ns->GetPrefix()); + if (!namespacePrefix.IsEmpty()) { + // check if namespacePrefix is the same as the online directory; if so, + // ignore it. + nsAutoCString onlineDir; + rv = GetServerDirectory(onlineDir); + NS_ENSURE_SUCCESS(rv, rv); + if (!onlineDir.IsEmpty()) { + char delimiter = ns->GetDelimiter(); + if (onlineDir.Last() != delimiter) onlineDir += delimiter; + if (onlineDir.Equals(namespacePrefix)) return NS_OK; + } + + namespacePrefix.ReplaceChar(ns->GetDelimiter(), + '/'); // use canonical format + nsCString uri(originalUri); + int32_t index = uri.Find("//"); // find scheme + index = uri.FindChar('/', index + 2); // find '/' after scheme + // it may be the case that this is the INBOX uri, in which case + // we don't want to prepend the namespace. In that case, the uri ends with + // "INBOX", but the namespace is "INBOX/", so they don't match. + if (uri.Find(namespacePrefix, index + 1) != index + 1 && + !Substring(uri, index + 1).LowerCaseEqualsLiteral("inbox")) + uri.Insert(namespacePrefix, index + 1); // insert namespace prefix + convertedUri = uri; + } + } + return rv; +} + +NS_IMETHODIMP nsImapIncomingServer::GetTrashFolderName(nsAString& retval) { + // Despite its name, this returns a path, for example INBOX/Trash. + nsresult rv = GetUnicharValue(PREF_TRASH_FOLDER_PATH, retval); + if (NS_FAILED(rv)) return rv; + if (retval.IsEmpty()) + retval = NS_LITERAL_STRING_FROM_CSTRING(DEFAULT_TRASH_FOLDER_PATH); + return NS_OK; +} + +NS_IMETHODIMP nsImapIncomingServer::SetTrashFolderName( + const nsAString& chvalue) { + // Clear trash flag from the old pref. + // Despite its name, this returns the trash folder path, for example + // INBOX/Trash. + bool useUTF8 = false; + GetUtf8AcceptEnabled(&useUTF8); + nsAutoString oldTrashName; + nsresult rv = GetTrashFolderName(oldTrashName); + if (NS_SUCCEEDED(rv)) { + nsAutoCString oldTrashNameUtf7or8; + nsCOMPtr<nsIMsgFolder> oldFolder; + // 'trashFolderName' being a path here works well since this is appended + // to the server's root folder in GetFolder(). + if (useUTF8) { + CopyUTF16toUTF8(oldTrashName, oldTrashNameUtf7or8); + } else { + CopyUTF16toMUTF7(oldTrashName, oldTrashNameUtf7or8); + } + rv = GetFolder(oldTrashNameUtf7or8, getter_AddRefs(oldFolder)); + if (NS_SUCCEEDED(rv) && oldFolder) + oldFolder->ClearFlag(nsMsgFolderFlags::Trash); + } + + // If the user configured delete mode (model) is currently "move to trash", + // mark the newly designated trash folder name as the active trash + // destination folder. + int32_t deleteModel; + rv = GetDeleteModel(&deleteModel); + if (NS_SUCCEEDED(rv) && (deleteModel == nsMsgImapDeleteModels::MoveToTrash)) { + nsAutoCString newTrashNameUtf7or8; + if (useUTF8) { + CopyUTF16toUTF8(PromiseFlatString(chvalue), newTrashNameUtf7or8); + } else { + CopyUTF16toMUTF7(PromiseFlatString(chvalue), newTrashNameUtf7or8); + } + nsCOMPtr<nsIMsgFolder> newTrashFolder; + rv = GetFolder(newTrashNameUtf7or8, getter_AddRefs(newTrashFolder)); + if (NS_SUCCEEDED(rv) && newTrashFolder) + newTrashFolder->SetFlag(nsMsgFolderFlags::Trash); + } + + return SetUnicharValue(PREF_TRASH_FOLDER_PATH, chvalue); +} + +NS_IMETHODIMP +nsImapIncomingServer::GetMsgFolderFromURI(nsIMsgFolder* aFolderResource, + const nsACString& aURI, + nsIMsgFolder** aFolder) { + nsCOMPtr<nsIMsgFolder> msgFolder; + bool namespacePrefixAdded = false; + nsCString folderUriWithNamespace; + + // clang-format off + // Check if the folder exists as is... + nsresult rv = GetExistingMsgFolder(aURI, folderUriWithNamespace, + namespacePrefixAdded, false, + getter_AddRefs(msgFolder)); + + // Or try again with a case-insensitive lookup + if (NS_FAILED(rv) || !msgFolder) + rv = GetExistingMsgFolder(aURI, folderUriWithNamespace, + namespacePrefixAdded, true, + getter_AddRefs(msgFolder)); + // clang-format on + + if (NS_FAILED(rv) || !msgFolder) { + // we didn't find the folder so we will have to create a new one. + if (namespacePrefixAdded) { + nsCOMPtr<nsIMsgFolder> folder; + rv = GetOrCreateFolder(folderUriWithNamespace, getter_AddRefs(folder)); + NS_ENSURE_SUCCESS(rv, rv); + msgFolder = folder; + } else + msgFolder = aFolderResource; + } + + msgFolder.forget(aFolder); + return (aFolder ? NS_OK : NS_ERROR_FAILURE); +} + +nsresult nsImapIncomingServer::GetExistingMsgFolder( + const nsACString& aURI, nsACString& aFolderUriWithNamespace, + bool& aNamespacePrefixAdded, bool aCaseInsensitive, + nsIMsgFolder** aFolder) { + nsCOMPtr<nsIMsgFolder> rootMsgFolder; + nsresult rv = GetRootMsgFolder(getter_AddRefs(rootMsgFolder)); + NS_ENSURE_SUCCESS(rv, rv); + + aNamespacePrefixAdded = false; + // Check if the folder exists as is...Even if we have a personal namespace, + // it might be in another namespace (e.g., shared) and this will catch that. + rv = rootMsgFolder->GetChildWithURI(aURI, true, aCaseInsensitive, aFolder); + + // If we couldn't find the folder as is, check if we need to prepend the + // personal namespace + if (!*aFolder) { + GetUriWithNamespacePrefixIfNecessary(kPersonalNamespace, aURI, + aFolderUriWithNamespace); + if (!aFolderUriWithNamespace.IsEmpty()) { + aNamespacePrefixAdded = true; + rv = rootMsgFolder->GetChildWithURI(aFolderUriWithNamespace, true, + aCaseInsensitive, aFolder); + } + } + return rv; +} + +NS_IMETHODIMP +nsImapIncomingServer::CramMD5Hash(const char* decodedChallenge, const char* key, + char** result) { + NS_ENSURE_ARG_POINTER(decodedChallenge); + NS_ENSURE_ARG_POINTER(key); + + unsigned char resultDigest[DIGEST_LENGTH]; + nsresult rv = MSGCramMD5(decodedChallenge, strlen(decodedChallenge), key, + strlen(key), resultDigest); + NS_ENSURE_SUCCESS(rv, rv); + *result = (char*)malloc(DIGEST_LENGTH); + if (*result) memcpy(*result, resultDigest, DIGEST_LENGTH); + return (*result) ? NS_OK : NS_ERROR_OUT_OF_MEMORY; +} + +NS_IMETHODIMP +nsImapIncomingServer::GetLoginUsername(nsACString& aLoginUsername) { + return GetUsername(aLoginUsername); +} + +NS_IMETHODIMP +nsImapIncomingServer::GetOriginalUsername(nsACString& aUsername) { + return GetUsername(aUsername); +} + +NS_IMETHODIMP +nsImapIncomingServer::GetServerKey(nsACString& aServerKey) { + return GetKey(aServerKey); +} + +NS_IMETHODIMP +nsImapIncomingServer::GetServerPassword(nsAString& aPassword) { + return GetPassword(aPassword); +} + +NS_IMETHODIMP +nsImapIncomingServer::RemoveServerConnection(nsIImapProtocol* aProtocol) { + return RemoveConnection(aProtocol); +} + +NS_IMETHODIMP +nsImapIncomingServer::GetServerShuttingDown(bool* aShuttingDown) { + return GetShuttingDown(aShuttingDown); +} + +NS_IMETHODIMP +nsImapIncomingServer::ResetServerConnection(const nsACString& aFolderName) { + return ResetConnection(aFolderName); +} + +NS_IMETHODIMP +nsImapIncomingServer::SetServerDoingLsub(bool aDoingLsub) { + return SetDoingLsub(aDoingLsub); +} + +NS_IMETHODIMP +nsImapIncomingServer::SetServerUtf8AcceptEnabled(bool enabled) { + return SetUtf8AcceptEnabled(enabled); +} diff --git a/comm/mailnews/imap/src/nsImapIncomingServer.h b/comm/mailnews/imap/src/nsImapIncomingServer.h new file mode 100644 index 0000000000..03f4f29ca6 --- /dev/null +++ b/comm/mailnews/imap/src/nsImapIncomingServer.h @@ -0,0 +1,150 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef __nsImapIncomingServer_h +#define __nsImapIncomingServer_h + +#include "mozilla/Attributes.h" +#include "msgCore.h" +#include "nsImapCore.h" +#include "nsIImapIncomingServer.h" +#include "nsMsgIncomingServer.h" +#include "nsIImapServerSink.h" +#include "nsIStringBundle.h" +#include "nsISubscribableServer.h" +#include "nsIUrlListener.h" +#include "nsIMsgImapMailFolder.h" +#include "nsCOMArray.h" +#include "nsTArray.h" +#include "mozilla/Mutex.h" + +/* get some implementation from nsMsgIncomingServer */ +class nsImapIncomingServer : public nsMsgIncomingServer, + public nsIImapIncomingServer, + public nsIImapServerSink, + public nsISubscribableServer, + public nsIUrlListener { + public: + NS_DECL_ISUPPORTS_INHERITED + + nsImapIncomingServer(); + + // overriding nsMsgIncomingServer methods + NS_IMETHOD SetKey(const nsACString& aKey) + override; // override nsMsgIncomingServer's implementation... + NS_IMETHOD GetLocalStoreType(nsACString& type) override; + NS_IMETHOD GetLocalDatabaseType(nsACString& type) override; + + NS_DECL_NSIIMAPINCOMINGSERVER + NS_DECL_NSIIMAPSERVERSINK + NS_DECL_NSISUBSCRIBABLESERVER + NS_DECL_NSIURLLISTENER + + NS_IMETHOD PerformBiff(nsIMsgWindow* aMsgWindow) override; + NS_IMETHOD PerformExpand(nsIMsgWindow* aMsgWindow) override; + NS_IMETHOD CloseCachedConnections() override; + NS_IMETHOD GetConstructedPrettyName(nsAString& retval) override; + NS_IMETHOD GetCanBeDefaultServer(bool* canBeDefaultServer) override; + NS_IMETHOD GetCanCompactFoldersOnServer( + bool* canCompactFoldersOnServer) override; + NS_IMETHOD GetCanUndoDeleteOnServer(bool* canUndoDeleteOnServer) override; + NS_IMETHOD GetCanSearchMessages(bool* canSearchMessages) override; + NS_IMETHOD GetOfflineSupportLevel(int32_t* aSupportLevel) override; + NS_IMETHOD GeneratePrettyNameForMigration(nsAString& aPrettyName) override; + NS_IMETHOD GetSupportsDiskSpace(bool* aSupportsDiskSpace) override; + NS_IMETHOD GetCanCreateFoldersOnServer( + bool* aCanCreateFoldersOnServer) override; + NS_IMETHOD GetCanFileMessagesOnServer( + bool* aCanFileMessagesOnServer) override; + NS_IMETHOD GetFilterScope(nsMsgSearchScopeValue* filterScope) override; + NS_IMETHOD GetSearchScope(nsMsgSearchScopeValue* searchScope) override; + NS_IMETHOD GetServerRequiresPasswordForBiff( + bool* aServerRequiresPasswordForBiff) override; + NS_IMETHOD GetNumIdleConnections(int32_t* aNumIdleConnections); + NS_IMETHOD ForgetSessionPassword(bool modifyLogin) override; + NS_IMETHOD GetMsgFolderFromURI(nsIMsgFolder* aFolderResource, + const nsACString& aURI, + nsIMsgFolder** aFolder) override; + NS_IMETHOD SetSocketType(int32_t aSocketType) override; + NS_IMETHOD VerifyLogon(nsIUrlListener* aUrlListener, nsIMsgWindow* aMsgWindow, + nsIURI** aURL) override; + + protected: + virtual ~nsImapIncomingServer(); + nsresult GetFolder(const nsACString& name, nsIMsgFolder** pFolder); + virtual nsresult CreateRootFolderFromUri(const nsACString& serverUri, + nsIMsgFolder** rootFolder) override; + nsresult ResetFoldersToUnverified(nsIMsgFolder* parentFolder); + void GetUnverifiedSubFolders(nsIMsgFolder* parentFolder, + nsCOMArray<nsIMsgImapMailFolder>& aFoldersArray); + void GetUnverifiedFolders(nsCOMArray<nsIMsgImapMailFolder>& aFolderArray); + bool NoDescendentsAreVerified(nsIMsgFolder* parentFolder); + bool AllDescendentsAreNoSelect(nsIMsgFolder* parentFolder); + + nsresult GetStringBundle(); + static nsresult AlertUser(const nsAString& aString, nsIMsgMailNewsUrl* aUrl); + + private: + nsresult SubscribeToFolder(const char16_t* aName, bool subscribe); + nsresult GetImapConnection(nsIImapUrl* aImapUrl, + nsIImapProtocol** aImapConnection); + nsresult CreateProtocolInstance(nsIImapProtocol** aImapConnection); + nsresult CreateHostSpecificPrefName(const char* prefPrefix, + nsAutoCString& prefName); + + nsresult DoomUrlIfChannelHasError(nsIImapUrl* aImapUrl, bool* urlDoomed); + bool ConnectionTimeOut(nsIImapProtocol* aImapConnection); + nsresult GetFormattedStringFromName(const nsAString& aValue, + const char* aName, nsAString& aResult); + nsresult GetPrefForServerAttribute(const char* prefSuffix, bool* prefValue); + bool CheckSpecialFolder(nsCString& folderUri, uint32_t folderFlag, + nsCString& existingUri); + + nsCOMArray<nsIImapProtocol> m_connectionCache; + + /** + * All requests waiting for a real connection. + * Each URL object holds a reference to the nsIImapMockChannel that + * represents the request. + */ + nsCOMArray<nsIImapUrl> m_urlQueue; + + /** + * Consumers for the queued urls. The number of elements here should match + * that of m_urlQueue. So requests with no consumer should have a nullptr + * entry here. + */ + nsTArray<nsISupports*> m_urlConsumers; + + nsCOMPtr<nsIStringBundle> m_stringBundle; + nsCOMArray<nsIMsgFolder> + m_subscribeFolders; // used to keep folder resources around while + // subscribe UI is up. + nsCOMArray<nsIMsgImapMailFolder> + m_foldersToStat; // folders to check for new mail with Status + eIMAPCapabilityFlags m_capability; + nsCString m_manageMailAccountUrl; + bool m_userAuthenticated; + bool mDoingSubscribeDialog; + bool mDoingLsub; + bool m_shuttingDown; + bool mUtf8AcceptEnabled; + + mozilla::Mutex mLock; + // subscribe dialog stuff + nsresult AddFolderToSubscribeDialog(const char* parentUri, const char* uri, + const char* folderName); + nsCOMPtr<nsISubscribableServer> mInner; + nsresult EnsureInner(); + nsresult ClearInner(); + + // Utility function for checking folder existence + nsresult GetExistingMsgFolder(const nsACString& aURI, + nsACString& folderUriWithNamespace, + bool& namespacePrefixAdded, + bool caseInsensitive, nsIMsgFolder** aFolder); +}; + +#endif diff --git a/comm/mailnews/imap/src/nsImapMailFolder.cpp b/comm/mailnews/imap/src/nsImapMailFolder.cpp new file mode 100644 index 0000000000..1ec8482383 --- /dev/null +++ b/comm/mailnews/imap/src/nsImapMailFolder.cpp @@ -0,0 +1,9095 @@ +/* -*- Mode: + C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" +#include "prmem.h" +#include "nsImapMailFolder.h" +#include "nsIImapService.h" +#include "nsIFile.h" +#include "nsAnonymousTemporaryFile.h" +#include "nsIUrlListener.h" +#include "nsCOMPtr.h" +#include "nsMsgFolderFlags.h" +#include "nsISeekableStream.h" +#include "nsThreadUtils.h" +#include "nsIImapUrl.h" +#include "nsImapUtils.h" +#include "nsMsgUtils.h" +#include "nsIMsgMailSession.h" +#include "nsITransactionManager.h" +#include "nsImapUndoTxn.h" +#include "../public/nsIImapHostSessionList.h" +#include "nsIMsgCopyService.h" +#include "nsICopyMessageStreamListener.h" +#include "nsImapStringBundle.h" +#include "nsIMsgFolderCacheElement.h" +#include "nsTextFormatter.h" +#include "nsIPrefBranch.h" +#include "nsIPrefService.h" +#include "nsMsgI18N.h" +#include "nsIMsgFilter.h" +#include "nsIMsgFilterService.h" +#include "nsIMsgSearchCustomTerm.h" +#include "nsIMsgSearchTerm.h" +#include "nsImapMoveCoalescer.h" +#include "nsIPrompt.h" +#include "nsIDocShell.h" +#include "nsUnicharUtils.h" +#include "nsIImapFlagAndUidState.h" +#include "nsIImapHeaderXferInfo.h" +#include "nsIMessenger.h" +#include "nsIMsgSearchAdapter.h" +#include "nsIImapMockChannel.h" +#include "nsIProgressEventSink.h" +#include "nsIMsgWindow.h" +#include "nsIMsgFolder.h" // TO include biffState enum. Change to bool later... +#include "nsIMsgLocalMailFolder.h" +#include "nsIMsgOfflineImapOperation.h" +#include "nsImapOfflineSync.h" +#include "nsIImapMailFolderSink.h" +#include "nsIImapServerSink.h" +#include "nsIMsgAccountManager.h" +#include "nsQuickSort.h" +#include "nsIImapMockChannel.h" +#include "nsNetUtil.h" +#include "nsImapNamespace.h" +#include "nsIMsgFolderCompactor.h" +#include "nsMsgMessageFlags.h" +#include "nsISpamSettings.h" +#include <time.h> +#include "nsIMsgMailNewsUrl.h" +#include "nsEmbedCID.h" +#include "nsIMsgComposeService.h" +#include "nsDirectoryServiceDefs.h" +#include "nsIDirectoryEnumerator.h" +#include "nsIMsgIdentity.h" +#include "nsIMsgFolderNotificationService.h" +#include "nsNativeCharsetUtils.h" +#include "nsIExternalProtocolService.h" +#include "nsCExternalHandlerService.h" +#include "prprf.h" +#include "nsAutoSyncManager.h" +#include "nsIMsgFilterCustomAction.h" +#include "nsMsgReadStateTxn.h" +#include "nsStringEnumerator.h" +#include "nsIMsgStatusFeedback.h" +#include "nsMsgLineBuffer.h" +#include "mozilla/Logging.h" +#include "mozilla/Attributes.h" +#include "mozilla/SlicedInputStream.h" +#include "nsStringStream.h" +#include "nsIStreamListener.h" +#include "nsITimer.h" +#include "nsReadableUtils.h" +#include "UrlListener.h" +#include "nsIObserverService.h" + +#define NS_PARSEMAILMSGSTATE_CID \ + { /* 2B79AC51-1459-11d3-8097-006008128C4E */ \ + 0x2b79ac51, 0x1459, 0x11d3, { \ + 0x80, 0x97, 0x0, 0x60, 0x8, 0x12, 0x8c, 0x4e \ + } \ + } +static NS_DEFINE_CID(kParseMailMsgStateCID, NS_PARSEMAILMSGSTATE_CID); + +#define NS_IIMAPHOSTSESSIONLIST_CID \ + { \ + 0x479ce8fc, 0xe725, 0x11d2, { \ + 0xa5, 0x05, 0x00, 0x60, 0xb0, 0xfc, 0x04, 0xb7 \ + } \ + } +static NS_DEFINE_CID(kCImapHostSessionList, NS_IIMAPHOSTSESSIONLIST_CID); + +#define MAILNEWS_CUSTOM_HEADERS "mailnews.customHeaders" + +using namespace mozilla; + +extern LazyLogModule gAutoSyncLog; // defined in nsAutoSyncManager.cpp +extern LazyLogModule IMAP; // defined in nsImapProtocol.cpp +extern LazyLogModule IMAP_CS; // For CONDSTORE, defined in nsImapProtocol.cpp +extern LazyLogModule FILTERLOGMODULE; // defined in nsMsgFilterService.cpp +LazyLogModule IMAP_KW("IMAP_KW"); // for logging keyword (tag) processing + +/* + Copies the contents of srcDir into destDir. + destDir will be created if it doesn't exist. +*/ + +static nsresult RecursiveCopy(nsIFile* srcDir, nsIFile* destDir) { + bool isDir; + nsresult rv = srcDir->IsDirectory(&isDir); + NS_ENSURE_SUCCESS(rv, rv); + if (!isDir) return NS_ERROR_INVALID_ARG; + + bool exists; + rv = destDir->Exists(&exists); + NS_ENSURE_SUCCESS(rv, rv); + if (!exists) { + rv = destDir->Create(nsIFile::DIRECTORY_TYPE, 0775); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr<nsIDirectoryEnumerator> dirIterator; + rv = srcDir->GetDirectoryEntries(getter_AddRefs(dirIterator)); + NS_ENSURE_SUCCESS(rv, rv); + + bool hasMore = false; + while (NS_SUCCEEDED(dirIterator->HasMoreElements(&hasMore)) && hasMore) { + nsCOMPtr<nsIFile> dirEntry; + rv = dirIterator->GetNextFile(getter_AddRefs(dirEntry)); + NS_ENSURE_SUCCESS(rv, rv); + if (!dirEntry) continue; + rv = dirEntry->IsDirectory(&isDir); + NS_ENSURE_SUCCESS(rv, rv); + if (isDir) { + nsCOMPtr<nsIFile> newChild; + rv = destDir->Clone(getter_AddRefs(newChild)); + NS_ENSURE_SUCCESS(rv, rv); + nsAutoString leafName; + dirEntry->GetLeafName(leafName); + newChild->AppendRelativePath(leafName); + rv = newChild->Exists(&exists); + NS_ENSURE_SUCCESS(rv, rv); + if (!exists) { + rv = newChild->Create(nsIFile::DIRECTORY_TYPE, 0775); + NS_ENSURE_SUCCESS(rv, rv); + } + rv = RecursiveCopy(dirEntry, newChild); + } else { + rv = dirEntry->CopyTo(destDir, EmptyString()); + } + NS_ENSURE_SUCCESS(rv, rv); + } + return rv; +} + +// +// nsMsgQuota +// +NS_IMPL_ISUPPORTS(nsMsgQuota, nsIMsgQuota) + +nsMsgQuota::nsMsgQuota(const nsACString& aName, const uint64_t& aUsage, + const uint64_t& aLimit) + : mName(aName), mUsage(aUsage), mLimit(aLimit) {} + +nsMsgQuota::~nsMsgQuota() {} + +/** + * Note: These quota access function are not called but still must be defined + * for the linker. + */ +NS_IMETHODIMP nsMsgQuota::GetName(nsACString& aName) { + aName = mName; + return NS_OK; +} + +NS_IMETHODIMP nsMsgQuota::SetName(const nsACString& aName) { + mName = aName; + return NS_OK; +} + +NS_IMETHODIMP nsMsgQuota::GetUsage(uint64_t* aUsage) { + *aUsage = mUsage; + return NS_OK; +} + +NS_IMETHODIMP nsMsgQuota::SetUsage(uint64_t aUsage) { + mUsage = aUsage; + return NS_OK; +} + +NS_IMETHODIMP nsMsgQuota::GetLimit(uint64_t* aLimit) { + *aLimit = mLimit; + return NS_OK; +} + +NS_IMETHODIMP nsMsgQuota::SetLimit(uint64_t aLimit) { + mLimit = aLimit; + return NS_OK; +} + +// +// nsImapMailFolder +// +nsImapMailFolder::nsImapMailFolder() + : m_initialized(false), + m_haveDiscoveredAllFolders(false), + m_curMsgUid(0), + m_nextMessageByteLength(0), + m_urlRunning(false), + m_verifiedAsOnlineFolder(false), + m_explicitlyVerify(false), + m_folderIsNamespace(false), + m_folderNeedsSubscribing(false), + m_folderNeedsAdded(false), + m_folderNeedsACLListed(true), + m_performingBiff(false), + m_updatingFolder(false), + m_applyIncomingFilters(false), + m_downloadingFolderForOfflineUse(false), + m_filterListRequiresBody(false), + m_folderQuotaCommandIssued(false), + m_folderQuotaDataIsValid(false) { + m_boxFlags = 0; + m_uidValidity = kUidUnknown; + m_numServerRecentMessages = 0; + m_numServerUnseenMessages = 0; + m_numServerTotalMessages = 0; + m_nextUID = nsMsgKey_None; + m_hierarchyDelimiter = kOnlineHierarchySeparatorUnknown; + m_folderACL = nullptr; + m_aclFlags = 0; + m_supportedUserFlags = 0; + m_namespace = nullptr; + m_pendingPlaybackReq = nullptr; +} + +nsImapMailFolder::~nsImapMailFolder() { + delete m_folderACL; + + // cleanup any pending request + delete m_pendingPlaybackReq; +} + +NS_IMPL_ADDREF_INHERITED(nsImapMailFolder, nsMsgDBFolder) +NS_IMPL_RELEASE_INHERITED(nsImapMailFolder, nsMsgDBFolder) +NS_IMPL_QUERY_HEAD(nsImapMailFolder) +NS_IMPL_QUERY_BODY(nsIMsgImapMailFolder) +NS_IMPL_QUERY_BODY(nsICopyMessageListener) +NS_IMPL_QUERY_BODY(nsIImapMailFolderSink) +NS_IMPL_QUERY_BODY(nsIImapMessageSink) +NS_IMPL_QUERY_BODY(nsIUrlListener) +NS_IMPL_QUERY_BODY(nsIMsgFilterHitNotify) +NS_IMPL_QUERY_TAIL_INHERITING(nsMsgDBFolder) + +nsresult nsImapMailFolder::AddDirectorySeparator(nsIFile* path) { + if (mURI.Equals(kImapRootURI)) { + // don't concat the full separator with .sbd + } else { + // see if there's a dir with the same name ending with .sbd + nsAutoString leafName; + path->GetLeafName(leafName); + leafName.AppendLiteral(FOLDER_SUFFIX); + path->SetLeafName(leafName); + } + + return NS_OK; +} + +static bool nsShouldIgnoreFile(nsString& name) { + if (StringEndsWith(name, NS_LITERAL_STRING_FROM_CSTRING(SUMMARY_SUFFIX), + nsCaseInsensitiveStringComparator)) { + name.SetLength(name.Length() - + SUMMARY_SUFFIX_LENGTH); // truncate the string + return false; + } + return true; +} + +nsresult nsImapMailFolder::CreateChildFromURI(const nsACString& uri, + nsIMsgFolder** folder) { + nsImapMailFolder* newFolder = new nsImapMailFolder; + newFolder->Init(uri); + NS_ADDREF(*folder = newFolder); + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::AddSubfolder(const nsAString& aName, + nsIMsgFolder** aChild) { + NS_ENSURE_ARG_POINTER(aChild); + + int32_t flags = 0; + nsresult rv; + + nsAutoCString uri(mURI); + uri.Append('/'); + + nsAutoCString escapedName; + rv = NS_MsgEscapeEncodeURLPath(aName, escapedName); + NS_ENSURE_SUCCESS(rv, rv); + + uri += escapedName.get(); + + nsCOMPtr<nsIMsgFolder> msgFolder; + rv = GetChildWithURI(uri, false /*deep*/, true /*case Insensitive*/, + getter_AddRefs(msgFolder)); + if (NS_SUCCEEDED(rv) && msgFolder) return NS_MSG_FOLDER_EXISTS; + + nsCOMPtr<nsIMsgFolder> folder; + rv = GetOrCreateFolder(uri, getter_AddRefs(folder)); + NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); + + // Ensure the containing dir exists. + nsCOMPtr<nsIFile> path; + rv = CreateDirectoryForFolder(getter_AddRefs(path)); + NS_ENSURE_SUCCESS(rv, rv); + + folder->GetFlags((uint32_t*)&flags); + + flags |= nsMsgFolderFlags::Mail; + + nsCOMPtr<nsIImapIncomingServer> imapServer; + GetImapIncomingServer(getter_AddRefs(imapServer)); + if (imapServer) { + bool setNewFoldersForOffline = false; + rv = imapServer->GetOfflineDownload(&setNewFoldersForOffline); + if (NS_SUCCEEDED(rv) && setNewFoldersForOffline) + flags |= nsMsgFolderFlags::Offline; + } + + folder->SetParent(this); + + folder->SetFlags(flags); + + mSubFolders.AppendObject(folder); + folder.forget(aChild); + + // New child needs to inherit hierarchyDelimiter. + nsCOMPtr<nsIMsgImapMailFolder> imapChild = do_QueryInterface(*aChild); + if (imapChild) { + imapChild->SetHierarchyDelimiter(m_hierarchyDelimiter); + } + NotifyFolderAdded(*aChild); + return rv; +} + +// Creates a new child nsIMsgFolder locally, with no IMAP traffic. +nsresult nsImapMailFolder::AddSubfolderWithPath(nsAString& name, + nsIFile* dbPath, + nsIMsgFolder** child, + bool brandNew) { + NS_ENSURE_ARG_POINTER(child); + nsresult rv; + + nsAutoCString uri(mURI); + uri.Append('/'); + AppendUTF16toUTF8(name, uri); + + bool isServer; + rv = GetIsServer(&isServer); + NS_ENSURE_SUCCESS(rv, rv); + + bool isInbox = isServer && name.LowerCaseEqualsLiteral("inbox"); + + // will make sure mSubFolders does not have duplicates because of bogus msf + // files. + nsCOMPtr<nsIMsgFolder> msgFolder; + rv = GetChildWithURI(uri, false /*deep*/, isInbox /*case Insensitive*/, + getter_AddRefs(msgFolder)); + if (NS_SUCCEEDED(rv) && msgFolder) return NS_MSG_FOLDER_EXISTS; + + nsCOMPtr<nsIMsgFolder> folder; + rv = GetOrCreateFolder(uri, getter_AddRefs(folder)); + NS_ENSURE_SUCCESS(rv, rv); + + folder->SetFilePath(dbPath); + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(folder, &rv); + mozilla::Unused << imapFolder; + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t flags = 0; + folder->GetFlags(&flags); + + folder->SetParent(this); + flags |= nsMsgFolderFlags::Mail; + + uint32_t pFlags; + GetFlags(&pFlags); + bool isParentInbox = pFlags & nsMsgFolderFlags::Inbox; + + nsCOMPtr<nsIImapIncomingServer> imapServer; + rv = GetImapIncomingServer(getter_AddRefs(imapServer)); + NS_ENSURE_SUCCESS(rv, rv); + + // Only set these if these are top level children or parent is inbox + if (isInbox) + flags |= nsMsgFolderFlags::Inbox; + else if (isServer || isParentInbox) { + nsMsgImapDeleteModel deleteModel; + imapServer->GetDeleteModel(&deleteModel); + if (deleteModel == nsMsgImapDeleteModels::MoveToTrash) { + nsAutoString trashName; + GetTrashFolderName(trashName); + if (name.Equals(trashName)) flags |= nsMsgFolderFlags::Trash; + } + } + + // Make the folder offline if it is newly created and the offline_download + // pref is true, unless it's the Trash or Junk folder. + if (brandNew && + !(flags & (nsMsgFolderFlags::Trash | nsMsgFolderFlags::Junk))) { + bool setNewFoldersForOffline = false; + rv = imapServer->GetOfflineDownload(&setNewFoldersForOffline); + if (NS_SUCCEEDED(rv) && setNewFoldersForOffline) + flags |= nsMsgFolderFlags::Offline; + } + + folder->SetFlags(flags); + + if (folder) mSubFolders.AppendObject(folder); + folder.forget(child); + return NS_OK; +} + +// Create child nsIMsgFolders by scanning the filesystem to find .msf files. +// No IMAP traffic. +nsresult nsImapMailFolder::CreateSubFolders(nsIFile* path) { + nsresult rv = NS_OK; + nsCOMPtr<nsIMsgIncomingServer> server; + rv = GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIDirectoryEnumerator> directoryEnumerator; + rv = path->GetDirectoryEntries(getter_AddRefs(directoryEnumerator)); + NS_ENSURE_SUCCESS(rv, rv); + + // For each .msf file in the directory... + bool hasMore = false; + while (NS_SUCCEEDED(directoryEnumerator->HasMoreElements(&hasMore)) && + hasMore) { + nsCOMPtr<nsIFile> currentFolderPath; + rv = directoryEnumerator->GetNextFile(getter_AddRefs(currentFolderPath)); + if (NS_FAILED(rv) || !currentFolderPath) continue; + + nsAutoString currentFolderNameStr; // online name + nsAutoString currentFolderDBNameStr; // possibly munged name + currentFolderPath->GetLeafName(currentFolderNameStr); + // Skip if not an .msf file. + // (NOTE: nsShouldIgnoreFile() strips the trailing ".msf" here) + if (nsShouldIgnoreFile(currentFolderNameStr)) continue; + + // OK, here we need to get the online name from the folder cache if we can. + // If we can, use that to create the sub-folder + nsCOMPtr<nsIFile> curFolder = + do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIFile> dbFile = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + dbFile->InitWithFile(currentFolderPath); + curFolder->InitWithFile(currentFolderPath); + // don't strip off the .msf in currentFolderPath. + currentFolderPath->SetLeafName(currentFolderNameStr); + currentFolderDBNameStr = currentFolderNameStr; + nsAutoString utfLeafName = currentFolderNameStr; + + if (curFolder) { + nsCOMPtr<nsIMsgFolderCacheElement> cacheElement; + rv = GetFolderCacheElemFromFile(dbFile, getter_AddRefs(cacheElement)); + if (NS_SUCCEEDED(rv) && cacheElement) { + nsCString onlineFullUtfName; + + uint32_t folderFlags; + rv = cacheElement->GetCachedUInt32("flags", &folderFlags); + if (NS_SUCCEEDED(rv) && + folderFlags & nsMsgFolderFlags::Virtual) // ignore virtual folders + continue; + int32_t hierarchyDelimiter; + rv = cacheElement->GetCachedInt32("hierDelim", &hierarchyDelimiter); + if (NS_SUCCEEDED(rv) && + hierarchyDelimiter == kOnlineHierarchySeparatorUnknown) { + currentFolderPath->Remove(false); + continue; // blow away .msf files for folders with unknown delimiter. + } + rv = cacheElement->GetCachedString("onlineName", onlineFullUtfName); + if (NS_SUCCEEDED(rv) && !onlineFullUtfName.IsEmpty()) { + CopyFolderNameToUTF16(onlineFullUtfName, currentFolderNameStr); + char delimiter = 0; + GetHierarchyDelimiter(&delimiter); + int32_t leafPos = currentFolderNameStr.RFindChar(delimiter); + if (leafPos > 0) currentFolderNameStr.Cut(0, leafPos + 1); + + // Take the full online name, and determine the leaf name. + CopyUTF8toUTF16(onlineFullUtfName, utfLeafName); + leafPos = utfLeafName.RFindChar(delimiter); + if (leafPos > 0) utfLeafName.Cut(0, leafPos + 1); + } + } + } + // make the imap folder remember the file spec it was created with. + nsCOMPtr<nsIFile> msfFilePath = + do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + msfFilePath->InitWithFile(currentFolderPath); + if (NS_SUCCEEDED(rv) && msfFilePath) { + // leaf name is the db name w/o .msf (nsShouldIgnoreFile strips it off) + // so this trims the .msf off the file spec. + msfFilePath->SetLeafName(currentFolderDBNameStr); + } + // Use the name as the uri for the folder. + nsCOMPtr<nsIMsgFolder> child; + AddSubfolderWithPath(utfLeafName, msfFilePath, getter_AddRefs(child)); + if (child) { + // use the unicode name as the "pretty" name. Set it so it won't be + // automatically computed from the URI. + if (!currentFolderNameStr.IsEmpty()) + child->SetPrettyName(currentFolderNameStr); + child->SetMsgDatabase(nullptr); + } + } + return rv; +} + +NS_IMETHODIMP nsImapMailFolder::GetSubFolders( + nsTArray<RefPtr<nsIMsgFolder>>& folders) { + bool isServer; + nsresult rv = GetIsServer(&isServer); + NS_ENSURE_SUCCESS(rv, rv); + + if (!m_initialized) { + nsCOMPtr<nsIFile> pathFile; + rv = GetFilePath(getter_AddRefs(pathFile)); + if (NS_FAILED(rv)) return rv; + + // host directory does not need .sbd tacked on + if (!isServer) { + rv = AddDirectorySeparator(pathFile); + if (NS_FAILED(rv)) return rv; + } + + m_initialized = true; // need to set this here to avoid infinite recursion + // from CreateSubfolders. + // we have to treat the root folder specially, because it's name + // doesn't end with .sbd + + int32_t newFlags = nsMsgFolderFlags::Mail; + bool isDirectory = false; + pathFile->IsDirectory(&isDirectory); + if (isDirectory) { + newFlags |= (nsMsgFolderFlags::Directory | nsMsgFolderFlags::Elided); + if (!mIsServer) SetFlag(newFlags); + rv = CreateSubFolders(pathFile); + } + if (isServer) { + nsCOMPtr<nsIMsgFolder> inboxFolder; + + GetFolderWithFlags(nsMsgFolderFlags::Inbox, getter_AddRefs(inboxFolder)); + if (!inboxFolder) { + // create an inbox if we don't have one. + CreateClientSubfolderInfo("INBOX"_ns, kOnlineHierarchySeparatorUnknown, + 0, true); + } + } + + // Force initialisation recursively. + for (nsIMsgFolder* f : mSubFolders) { + nsTArray<RefPtr<nsIMsgFolder>> dummy; + rv = f->GetSubFolders(dummy); + if (NS_FAILED(rv)) { + break; + } + } + + UpdateSummaryTotals(false); + if (NS_FAILED(rv)) return rv; + } + + return nsMsgDBFolder::GetSubFolders(folders); +} + +// Makes sure the database is open and exists. If the database is valid then +// returns NS_OK. Otherwise returns a failure error value. +nsresult nsImapMailFolder::GetDatabase() { + nsresult rv = NS_OK; + if (!mDatabase) { + nsCOMPtr<nsIMsgDBService> msgDBService = + do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // Create the database, blowing it away if it needs to be rebuilt + rv = msgDBService->OpenFolderDB(this, false, getter_AddRefs(mDatabase)); + if (NS_FAILED(rv)) + rv = msgDBService->CreateNewDB(this, getter_AddRefs(mDatabase)); + + NS_ENSURE_SUCCESS(rv, rv); + + // UpdateNewMessages/UpdateSummaryTotals can null mDatabase, so we save a + // local copy + nsCOMPtr<nsIMsgDatabase> database(mDatabase); + UpdateNewMessages(); + if (mAddListener) database->AddListener(this); + UpdateSummaryTotals(true); + mDatabase = database; + } + return rv; +} + +NS_IMETHODIMP nsImapMailFolder::UpdateFolder(nsIMsgWindow* inMsgWindow) { + return UpdateFolderWithListener(inMsgWindow, nullptr); +} + +NS_IMETHODIMP nsImapMailFolder::UpdateFolderWithListener( + nsIMsgWindow* aMsgWindow, nsIUrlListener* aUrlListener) { + nsresult rv; + // If this is the inbox, filters will be applied. Otherwise, we test the + // inherited folder property "applyIncomingFilters" (which defaults to empty). + // If this inherited property has the string value "true", we will apply + // filters even if this is not the inbox folder. + nsCString applyIncomingFilters; + GetInheritedStringProperty("applyIncomingFilters", applyIncomingFilters); + m_applyIncomingFilters = applyIncomingFilters.EqualsLiteral("true"); + + nsString folderName; + GetPrettyName(folderName); + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, + ("(Imap) nsImapMailFolder::UpdateFolderWithListener() on folder '%s'", + NS_ConvertUTF16toUTF8(folderName).get())); + if (mFlags & nsMsgFolderFlags::Inbox || m_applyIncomingFilters) { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Imap) Preparing filter run on folder '%s'", + NS_ConvertUTF16toUTF8(folderName).get())); + + if (!m_filterList) { + rv = GetFilterList(aMsgWindow, getter_AddRefs(m_filterList)); + if (NS_FAILED(rv)) { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Error, + ("(Imap) Loading of filter list failed")); + } + } + + // if there's no msg window, but someone is updating the inbox, we're + // doing something biff-like, and may download headers, so make biff notify. + if (!aMsgWindow && mFlags & nsMsgFolderFlags::Inbox) + SetPerformingBiff(true); + } + + if (m_filterList) { + nsCString listId; + m_filterList->GetListId(listId); + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Imap) Preparing filter list %s", listId.get())); + nsCOMPtr<nsIMsgIncomingServer> server; + rv = GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + + bool canFileMessagesOnServer = true; + rv = server->GetCanFileMessagesOnServer(&canFileMessagesOnServer); + // the mdn filter is for filing return receipts into the sent folder + // some servers (like AOL mail servers) + // can't file to the sent folder, so we don't add the filter for those + // servers + if (canFileMessagesOnServer) { + rv = server->ConfigureTemporaryFilters(m_filterList); + NS_ENSURE_SUCCESS(rv, rv); + } + + // If a body filter is enabled for an offline folder, delay the filter + // application until after message has been downloaded. + m_filterListRequiresBody = false; + + if (mFlags & nsMsgFolderFlags::Offline) { + nsCOMPtr<nsIMsgFilterService> filterService = + do_GetService("@mozilla.org/messenger/services/filters;1", &rv); + uint32_t filterCount = 0; + m_filterList->GetFilterCount(&filterCount); + for (uint32_t index = 0; index < filterCount && !m_filterListRequiresBody; + ++index) { + nsCOMPtr<nsIMsgFilter> filter; + m_filterList->GetFilterAt(index, getter_AddRefs(filter)); + if (!filter) continue; + nsMsgFilterTypeType filterType; + filter->GetFilterType(&filterType); + if (!(filterType & nsMsgFilterType::Incoming)) continue; + bool enabled = false; + filter->GetEnabled(&enabled); + if (!enabled) continue; + nsTArray<RefPtr<nsIMsgSearchTerm>> searchTerms; + filter->GetSearchTerms(searchTerms); + for (nsIMsgSearchTerm* term : searchTerms) { + nsMsgSearchAttribValue attrib; + rv = term->GetAttrib(&attrib); + NS_ENSURE_SUCCESS(rv, rv); + if (attrib == nsMsgSearchAttrib::Body) + m_filterListRequiresBody = true; + else if (attrib == nsMsgSearchAttrib::Custom) { + nsAutoCString customId; + rv = term->GetCustomId(customId); + nsCOMPtr<nsIMsgSearchCustomTerm> customTerm; + if (NS_SUCCEEDED(rv) && filterService) + rv = filterService->GetCustomTerm(customId, + getter_AddRefs(customTerm)); + bool needsBody = false; + if (NS_SUCCEEDED(rv) && customTerm) + rv = customTerm->GetNeedsBody(&needsBody); + if (NS_SUCCEEDED(rv) && needsBody) m_filterListRequiresBody = true; + } + if (m_filterListRequiresBody) { + break; + } + } + + // Also check if filter actions need the body, as this + // is supported in custom actions. + uint32_t numActions = 0; + filter->GetActionCount(&numActions); + for (uint32_t actionIndex = 0; + actionIndex < numActions && !m_filterListRequiresBody; + actionIndex++) { + nsCOMPtr<nsIMsgRuleAction> action; + rv = filter->GetActionAt(actionIndex, getter_AddRefs(action)); + if (NS_FAILED(rv) || !action) continue; + + nsCOMPtr<nsIMsgFilterCustomAction> customAction; + rv = action->GetCustomAction(getter_AddRefs(customAction)); + if (NS_FAILED(rv) || !customAction) continue; + + bool needsBody = false; + customAction->GetNeedsBody(&needsBody); + if (needsBody) m_filterListRequiresBody = true; + } + } + } + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Imap) Filters require the message body: %s", + (m_filterListRequiresBody ? "true" : "false"))); + } + + bool isServer; + rv = GetIsServer(&isServer); + if (NS_SUCCEEDED(rv) && isServer) { + if (!m_haveDiscoveredAllFolders) { + bool hasSubFolders = false; + GetHasSubFolders(&hasSubFolders); + if (!hasSubFolders) { + rv = CreateClientSubfolderInfo( + "Inbox"_ns, kOnlineHierarchySeparatorUnknown, 0, false); + NS_ENSURE_SUCCESS(rv, rv); + } + m_haveDiscoveredAllFolders = true; + } + } + + rv = GetDatabase(); + if (NS_FAILED(rv)) { + ThrowAlertMsg("errorGettingDB", aMsgWindow); + return rv; + } + + bool hasOfflineEvents = false; + GetFlag(nsMsgFolderFlags::OfflineEvents, &hasOfflineEvents); + + if (!WeAreOffline()) { + if (hasOfflineEvents) { + // hold a reference to the offline sync object. If ProcessNextOperation + // runs a url, a reference will be added to it. Otherwise, it will get + // destroyed when the refptr goes out of scope. + RefPtr<nsImapOfflineSync> goOnline = new nsImapOfflineSync(); + goOnline->Init(aMsgWindow, this, this, false); + if (goOnline) { + m_urlListener = aUrlListener; + return goOnline->ProcessNextOperation(); + } + } + } + + // Check it we're password protecting the local store. + if (!PromptForMasterPasswordIfNecessary()) return NS_ERROR_FAILURE; + + bool canOpenThisFolder = true; + GetCanOpenFolder(&canOpenThisFolder); + // Don't run select if we can't select the folder... + if (!m_urlRunning && canOpenThisFolder && !isServer) { + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + /* Do a discovery in its own url if needed. Do before SELECT url. */ + nsCOMPtr<nsIImapHostSessionList> hostSession = + do_GetService(kCImapHostSessionList, &rv); + if (NS_SUCCEEDED(rv) && hostSession) { + bool foundMailboxesAlready = false; + nsCString serverKey; + GetServerKey(serverKey); + hostSession->GetHaveWeEverDiscoveredFoldersForHost(serverKey.get(), + foundMailboxesAlready); + if (!foundMailboxesAlready) { + bool discoveryInProgress = false; + // See if discovery in progress and not yet finished. + hostSession->GetDiscoveryForHostInProgress(serverKey.get(), + discoveryInProgress); + if (!discoveryInProgress) { + nsCOMPtr<nsIMsgFolder> rootFolder; + rv = GetRootFolder(getter_AddRefs(rootFolder)); + if (NS_SUCCEEDED(rv) && rootFolder) { + rv = imapService->DiscoverAllFolders(rootFolder, this, aMsgWindow); + if (NS_SUCCEEDED(rv)) + hostSession->SetDiscoveryForHostInProgress(serverKey.get(), true); + } + } + } + } + + nsCOMPtr<nsIURI> url; + rv = imapService->SelectFolder(this, m_urlListener, aMsgWindow, + getter_AddRefs(url)); + if (NS_SUCCEEDED(rv)) { + m_urlRunning = true; + m_updatingFolder = true; + } + if (url) { + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(url, &rv); + NS_ENSURE_SUCCESS(rv, rv); + mailnewsUrl->RegisterListener(this); + m_urlListener = aUrlListener; + } + + // Allow IMAP folder auto-compact to occur when online or offline. + if (aMsgWindow) AutoCompact(aMsgWindow); + + if (rv == NS_MSG_ERROR_OFFLINE || rv == NS_BINDING_ABORTED) { + rv = NS_OK; + NotifyFolderEvent(kFolderLoaded); + } + } else { + // Tell the front end that the folder is loaded if we're not going to + // actually run a url. + if (!m_updatingFolder) // if we're already running an update url, we'll let + // that one send the folder loaded + NotifyFolderEvent(kFolderLoaded); + } + return rv; +} + +NS_IMETHODIMP nsImapMailFolder::CreateSubfolder(const nsAString& folderName, + nsIMsgWindow* msgWindow) { + if (folderName.IsEmpty()) return NS_MSG_ERROR_INVALID_FOLDER_NAME; + + nsresult rv; + nsAutoString trashName; + GetTrashFolderName(trashName); + if (folderName.Equals(trashName)) // Trash , a special folder + { + ThrowAlertMsg("folderExists", msgWindow); + return NS_MSG_FOLDER_EXISTS; + } + if (mIsServer && + folderName.LowerCaseEqualsLiteral("inbox")) // Inbox, a special folder + { + ThrowAlertMsg("folderExists", msgWindow); + return NS_MSG_FOLDER_EXISTS; + } + + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIURI> url; + return imapService->CreateFolder(this, folderName, this, getter_AddRefs(url)); +} + +NS_IMETHODIMP nsImapMailFolder::CreateClientSubfolderInfo( + const nsACString& folderName, char hierarchyDelimiter, int32_t flags, + bool suppressNotification) { + nsresult rv = NS_OK; + + // Get a directory based on our current path. + nsCOMPtr<nsIFile> path; + rv = CreateDirectoryForFolder(getter_AddRefs(path)); + if (NS_FAILED(rv)) return rv; + + NS_ConvertUTF8toUTF16 leafName(folderName); + nsAutoString folderNameStr; + nsAutoString parentName = leafName; + // use RFind, because folder can start with a delimiter and + // not be a leaf folder. + int32_t folderStart = leafName.RFindChar('/'); + if (folderStart > 0) { + nsCOMPtr<nsIMsgImapMailFolder> imapFolder; + nsAutoCString uri(mURI); + leafName.Assign(Substring(parentName, folderStart + 1)); + parentName.SetLength(folderStart); + + rv = CreateDirectoryForFolder(getter_AddRefs(path)); + NS_ENSURE_SUCCESS(rv, rv); + uri.Append('/'); + uri.Append(NS_ConvertUTF16toUTF8(parentName)); + nsCOMPtr<nsIMsgFolder> folder; + rv = GetOrCreateFolder(uri, getter_AddRefs(folder)); + NS_ENSURE_SUCCESS(rv, rv); + imapFolder = do_QueryInterface(folder, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsAutoCString leafnameC; + CopyUTF16toUTF8(leafName, leafnameC); + return imapFolder->CreateClientSubfolderInfo(leafnameC, hierarchyDelimiter, + flags, suppressNotification); + } + + // if we get here, it's really a leaf, and "this" is the parent. + folderNameStr = leafName; + + // Create an empty database for this mail folder, set its name from the user + nsCOMPtr<nsIMsgDatabase> mailDBFactory; + nsCOMPtr<nsIMsgFolder> child; + + nsCOMPtr<nsIMsgDBService> msgDBService = + do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgDatabase> unusedDB; + nsCOMPtr<nsIFile> dbFile; + + // warning, path will be changed + rv = CreateFileForDB(folderNameStr, path, getter_AddRefs(dbFile)); + NS_ENSURE_SUCCESS(rv, rv); + + // Now let's create the actual new folder + rv = AddSubfolderWithPath(folderNameStr, dbFile, getter_AddRefs(child), true); + NS_ENSURE_SUCCESS(rv, rv); + rv = msgDBService->OpenMailDBFromFile(dbFile, child, true, true, + getter_AddRefs(unusedDB)); + if (rv == NS_MSG_ERROR_FOLDER_SUMMARY_MISSING) rv = NS_OK; + + if (NS_SUCCEEDED(rv) && unusedDB) { + // need to set the folder name + nsCOMPtr<nsIDBFolderInfo> folderInfo; + rv = unusedDB->GetDBFolderInfo(getter_AddRefs(folderInfo)); + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(child, &rv); + if (NS_SUCCEEDED(rv)) { + nsAutoCString onlineName(m_onlineFolderName); + if (!onlineName.IsEmpty()) onlineName.Append(hierarchyDelimiter); + onlineName.Append(NS_ConvertUTF16toUTF8(folderNameStr)); + imapFolder->SetVerifiedAsOnlineFolder(true); + imapFolder->SetOnlineName(onlineName); + imapFolder->SetHierarchyDelimiter(hierarchyDelimiter); + imapFolder->SetBoxFlags(flags); + + // Now that the child is created and the boxflags are set we can be sure + // all special folder flags are known. The child may get its flags already + // in AddSubfolderWithPath if they were in FolderCache, but that's + // not always the case. + uint32_t flags = 0; + child->GetFlags(&flags); + + // Set the offline use flag for the newly created folder if the + // offline_download preference is true, unless it's the Trash or Junk + // folder. + if (!(flags & (nsMsgFolderFlags::Trash | nsMsgFolderFlags::Junk))) { + nsCOMPtr<nsIImapIncomingServer> imapServer; + rv = GetImapIncomingServer(getter_AddRefs(imapServer)); + NS_ENSURE_SUCCESS(rv, rv); + bool setNewFoldersForOffline = false; + rv = imapServer->GetOfflineDownload(&setNewFoldersForOffline); + if (NS_SUCCEEDED(rv) && setNewFoldersForOffline) + flags |= nsMsgFolderFlags::Offline; + } else { + flags &= ~nsMsgFolderFlags::Offline; // clear offline flag if set + } + + flags |= nsMsgFolderFlags::Elided; + child->SetFlags(flags); + + nsString unicodeName; + rv = CopyFolderNameToUTF16(nsCString(folderName), unicodeName); + if (NS_SUCCEEDED(rv)) child->SetPrettyName(unicodeName); + + // store the online name as the mailbox name in the db folder info + // I don't think anyone uses the mailbox name, so we'll use it + // to restore the online name when blowing away an imap db. + if (folderInfo) + folderInfo->SetMailboxName(NS_ConvertUTF8toUTF16(onlineName)); + } + + unusedDB->SetSummaryValid(true); + unusedDB->Commit(nsMsgDBCommitType::kLargeCommit); + unusedDB->Close(true); + // don't want to hold onto this newly created db. + child->SetMsgDatabase(nullptr); + } + + if (!suppressNotification) { + if (NS_SUCCEEDED(rv) && child) { + NotifyFolderAdded(child); + child->NotifyFolderEvent(kFolderCreateCompleted); + nsCOMPtr<nsIMsgFolderNotificationService> notifier( + do_GetService("@mozilla.org/messenger/msgnotificationservice;1")); + if (notifier) notifier->NotifyFolderAdded(child); + } else { + NotifyFolderEvent(kFolderCreateFailed); + } + } + return rv; +} + +NS_IMETHODIMP nsImapMailFolder::List() { + nsresult rv; + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + return imapService->ListFolder(this, this); +} + +NS_IMETHODIMP nsImapMailFolder::RemoveLocalSelf() { + // Kill the local folder and its storage. + return nsMsgDBFolder::DeleteSelf(nullptr); +} + +NS_IMETHODIMP nsImapMailFolder::CreateStorageIfMissing( + nsIUrlListener* urlListener) { + nsresult rv = NS_OK; + nsCOMPtr<nsIMsgFolder> msgParent; + GetParent(getter_AddRefs(msgParent)); + + // parent is probably not set because *this* was probably created by rdf + // and not by folder discovery. So, we have to compute the parent. + if (!msgParent) { + nsAutoCString folderName(mURI); + + int32_t leafPos = folderName.RFindChar('/'); + nsAutoCString parentName(folderName); + + if (leafPos > 0) { + // If there is a hierarchy, there is a parent. + // Don't strip off slash if it's the first character + parentName.SetLength(leafPos); + rv = GetOrCreateFolder(parentName, getter_AddRefs(msgParent)); + NS_ENSURE_SUCCESS(rv, rv); + } + } + if (msgParent) { + nsString folderName; + GetName(folderName); + nsresult rv; + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + imapService->EnsureFolderExists(msgParent, folderName, nullptr, + urlListener); + } + return rv; +} + +NS_IMETHODIMP nsImapMailFolder::GetVerifiedAsOnlineFolder( + bool* aVerifiedAsOnlineFolder) { + NS_ENSURE_ARG_POINTER(aVerifiedAsOnlineFolder); + *aVerifiedAsOnlineFolder = m_verifiedAsOnlineFolder; + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::SetVerifiedAsOnlineFolder( + bool aVerifiedAsOnlineFolder) { + m_verifiedAsOnlineFolder = aVerifiedAsOnlineFolder; + // mark ancestors as verified as well + if (aVerifiedAsOnlineFolder) { + nsCOMPtr<nsIMsgFolder> parent; + do { + GetParent(getter_AddRefs(parent)); + if (parent) { + nsCOMPtr<nsIMsgImapMailFolder> imapParent = do_QueryInterface(parent); + if (imapParent) { + bool verifiedOnline; + imapParent->GetVerifiedAsOnlineFolder(&verifiedOnline); + if (verifiedOnline) break; + imapParent->SetVerifiedAsOnlineFolder(true); + } + } + } while (parent); + } + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::GetOnlineDelimiter(char* onlineDelimiter) { + return GetHierarchyDelimiter(onlineDelimiter); +} + +NS_IMETHODIMP nsImapMailFolder::SetHierarchyDelimiter( + char aHierarchyDelimiter) { + m_hierarchyDelimiter = aHierarchyDelimiter; + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::GetHierarchyDelimiter( + char* aHierarchyDelimiter) { + NS_ENSURE_ARG_POINTER(aHierarchyDelimiter); + if (mIsServer) { + // if it's the root folder, we don't know the delimiter. So look at the + // first child. + int32_t count = mSubFolders.Count(); + if (count > 0) { + nsCOMPtr<nsIMsgImapMailFolder> childFolder( + do_QueryInterface(mSubFolders[0])); + if (childFolder) { + nsresult rv = childFolder->GetHierarchyDelimiter(aHierarchyDelimiter); + // some code uses m_hierarchyDelimiter directly, so we should set it. + m_hierarchyDelimiter = *aHierarchyDelimiter; + return rv; + } + } + } + ReadDBFolderInfo(false); // update cache first. + *aHierarchyDelimiter = m_hierarchyDelimiter; + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::SetBoxFlags(int32_t aBoxFlags) { + ReadDBFolderInfo(false); + + m_boxFlags = aBoxFlags; + uint32_t newFlags = mFlags; + + newFlags |= nsMsgFolderFlags::ImapBox; + + if (m_boxFlags & kNoinferiors) + newFlags |= nsMsgFolderFlags::ImapNoinferiors; + else + newFlags &= ~nsMsgFolderFlags::ImapNoinferiors; + if (m_boxFlags & kNoselect) + newFlags |= nsMsgFolderFlags::ImapNoselect; + else + newFlags &= ~nsMsgFolderFlags::ImapNoselect; + if (m_boxFlags & kPublicMailbox) + newFlags |= nsMsgFolderFlags::ImapPublic; + else + newFlags &= ~nsMsgFolderFlags::ImapPublic; + if (m_boxFlags & kOtherUsersMailbox) + newFlags |= nsMsgFolderFlags::ImapOtherUser; + else + newFlags &= ~nsMsgFolderFlags::ImapOtherUser; + if (m_boxFlags & kPersonalMailbox) + newFlags |= nsMsgFolderFlags::ImapPersonal; + else + newFlags &= ~nsMsgFolderFlags::ImapPersonal; + + // The following are all flags returned by XLIST. + // nsImapIncomingServer::DiscoveryDone checks for these folders. + if (m_boxFlags & kImapDrafts) newFlags |= nsMsgFolderFlags::Drafts; + + if (m_boxFlags & kImapSpam) newFlags |= nsMsgFolderFlags::Junk; + + if (m_boxFlags & kImapSent) newFlags |= nsMsgFolderFlags::SentMail; + + if (m_boxFlags & kImapInbox) newFlags |= nsMsgFolderFlags::Inbox; + + if (m_boxFlags & kImapXListTrash) { + nsCOMPtr<nsIImapIncomingServer> imapServer; + nsMsgImapDeleteModel deleteModel = nsMsgImapDeleteModels::MoveToTrash; + (void)GetImapIncomingServer(getter_AddRefs(imapServer)); + if (imapServer) imapServer->GetDeleteModel(&deleteModel); + if (deleteModel == nsMsgImapDeleteModels::MoveToTrash) + newFlags |= nsMsgFolderFlags::Trash; + } + // Treat the GMail all mail folder as the archive folder. + if (m_boxFlags & (kImapAllMail | kImapArchive)) + newFlags |= nsMsgFolderFlags::Archive; + + SetFlags(newFlags); + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::GetBoxFlags(int32_t* aBoxFlags) { + NS_ENSURE_ARG_POINTER(aBoxFlags); + *aBoxFlags = m_boxFlags; + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::GetExplicitlyVerify(bool* aExplicitlyVerify) { + NS_ENSURE_ARG_POINTER(aExplicitlyVerify); + *aExplicitlyVerify = m_explicitlyVerify; + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::SetExplicitlyVerify(bool aExplicitlyVerify) { + m_explicitlyVerify = aExplicitlyVerify; + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::GetNoSelect(bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + return GetFlag(nsMsgFolderFlags::ImapNoselect, aResult); +} + +NS_IMETHODIMP nsImapMailFolder::ApplyRetentionSettings() { + int32_t numDaysToKeepOfflineMsgs = -1; + + // Check if we've limited the offline storage by age. + nsCOMPtr<nsIImapIncomingServer> imapServer; + nsresult rv = GetImapIncomingServer(getter_AddRefs(imapServer)); + NS_ENSURE_SUCCESS(rv, rv); + imapServer->GetAutoSyncMaxAgeDays(&numDaysToKeepOfflineMsgs); + + nsCOMPtr<nsIMsgDatabase> holdDBOpen; + if (numDaysToKeepOfflineMsgs > 0) { + bool dbWasCached = mDatabase != nullptr; + rv = GetDatabase(); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgEnumerator> hdrs; + rv = mDatabase->EnumerateMessages(getter_AddRefs(hdrs)); + NS_ENSURE_SUCCESS(rv, rv); + bool hasMore = false; + + PRTime cutOffDay = + MsgConvertAgeInDaysToCutoffDate(numDaysToKeepOfflineMsgs); + + // so now cutOffDay is the PRTime cut-off point. Any offline msg with + // a date less than that will get marked for pending removal. + while (NS_SUCCEEDED(rv = hdrs->HasMoreElements(&hasMore)) && hasMore) { + nsCOMPtr<nsIMsgDBHdr> header; + rv = hdrs->GetNext(getter_AddRefs(header)); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t msgFlags; + PRTime msgDate; + header->GetFlags(&msgFlags); + if (msgFlags & nsMsgMessageFlags::Offline) { + header->GetDate(&msgDate); + MarkPendingRemoval(header, msgDate < cutOffDay); + // I'm horribly tempted to break out of the loop if we've found + // a message after the cut-off date, because messages will most likely + // be in date order in the db, but there are always edge cases. + } + } + if (!dbWasCached) { + holdDBOpen = mDatabase; + mDatabase = nullptr; + } + } + return nsMsgDBFolder::ApplyRetentionSettings(); +} + +/** + * The listener will get called when both the online expunge and the offline + * store compaction are finished (if the latter is needed). + */ +nsresult nsImapMailFolder::ExpungeAndCompact(nsIUrlListener* aListener, + nsIMsgWindow* aMsgWindow) { + GetDatabase(); + // now's a good time to apply the retention settings. If we do delete any + // messages, the expunge is going to have to wait until the delete to + // finish before it can run, but the multiple-connection protection code + // should handle that. + if (mDatabase) ApplyRetentionSettings(); + + // Things to hold in existence until both expunge and compact are complete. + RefPtr<nsImapMailFolder> folder = this; + nsCOMPtr<nsIUrlListener> finalListener = aListener; + nsCOMPtr<nsIMsgWindow> msgWindow = aMsgWindow; + + // doCompact implements OnStopRunningUrl() + // NOTE: The caller will be expecting that their listener will be invoked, so + // we need to be careful that all execution paths in here do that. We either + // call it directly, or pass it along to the foldercompactor to call. + auto doCompact = [folder, finalListener, msgWindow]( + nsIURI* url, nsresult status) -> nsresult { + nsCOMPtr<nsIMsgPluggableStore> msgStore; + nsresult rv = folder->GetMsgStore(getter_AddRefs(msgStore)); + if (NS_FAILED(rv)) { + if (finalListener) { + return finalListener->OnStopRunningUrl(nullptr, rv); + } + return rv; + } + bool storeSupportsCompaction; + msgStore->GetSupportsCompaction(&storeSupportsCompaction); + if (storeSupportsCompaction && folder->mFlags & nsMsgFolderFlags::Offline) { + nsCOMPtr<nsIMsgFolderCompactor> folderCompactor = + do_CreateInstance("@mozilla.org/messenger/foldercompactor;1", &rv); + if (NS_FAILED(rv)) { + if (finalListener) { + return finalListener->OnStopRunningUrl(nullptr, rv); + } + return rv; + } + return folderCompactor->CompactFolders({folder}, finalListener, + msgWindow); + } + // Not going to run a compaction, so signal that we're all done. + if (finalListener) { + return finalListener->OnStopRunningUrl(nullptr, NS_OK); + } + return NS_OK; + }; + + if (WeAreOffline()) { + // Can't run an expunge. Kick off the next stage (compact) immediately. + return doCompact(nullptr, NS_OK); + } + + // Run the expunge, followed by the compaction. + RefPtr<UrlListener> expungeListener = new UrlListener(); + expungeListener->mStopFn = doCompact; + return Expunge(expungeListener, aMsgWindow); +} + +// IMAP compact implies an Expunge. +NS_IMETHODIMP nsImapMailFolder::Compact(nsIUrlListener* aListener, + nsIMsgWindow* aMsgWindow) { + return ExpungeAndCompact(aListener, aMsgWindow); +} + +NS_IMETHODIMP +nsImapMailFolder::NotifyCompactCompleted() { return NS_OK; } + +NS_IMETHODIMP nsImapMailFolder::MarkPendingRemoval(nsIMsgDBHdr* aHdr, + bool aMark) { + NS_ENSURE_ARG_POINTER(aHdr); + uint32_t offlineMessageSize; + aHdr->GetOfflineMessageSize(&offlineMessageSize); + aHdr->SetStringProperty("pendingRemoval", aMark ? "1"_ns : ""_ns); + if (!aMark) return NS_OK; + nsresult rv = GetDatabase(); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIDBFolderInfo> dbFolderInfo; + rv = mDatabase->GetDBFolderInfo(getter_AddRefs(dbFolderInfo)); + NS_ENSURE_SUCCESS(rv, rv); + return dbFolderInfo->ChangeExpungedBytes(offlineMessageSize); +} + +NS_IMETHODIMP nsImapMailFolder::Expunge(nsIUrlListener* aListener, + nsIMsgWindow* aMsgWindow) { + nsresult rv; + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + return imapService->Expunge(this, aListener, aMsgWindow); +} + +NS_IMETHODIMP nsImapMailFolder::CompactAll(nsIUrlListener* aListener, + nsIMsgWindow* aMsgWindow) { + nsresult rv; + + nsCOMPtr<nsIMsgFolderCompactor> folderCompactor = + do_CreateInstance("@mozilla.org/messenger/foldercompactor;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgFolder> rootFolder; + rv = GetRootFolder(getter_AddRefs(rootFolder)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgWindow> msgWindow = aMsgWindow; + + // Set up a callable which will start the compaction phase. + auto doCompact = [folderCompactor, rootFolder, + listener = nsCOMPtr<nsIUrlListener>(aListener), + msgWindow]() { + // Collect all the compactable folders. + nsTArray<RefPtr<nsIMsgFolder>> foldersToCompact; + nsTArray<RefPtr<nsIMsgFolder>> allDescendants; + rootFolder->GetDescendants(allDescendants); + for (auto folder : allDescendants) { + uint32_t flags; + folder->GetFlags(&flags); + if (flags & + (nsMsgFolderFlags::Virtual | nsMsgFolderFlags::ImapNoselect)) { + continue; + } + // Folder can be compacted? + nsCOMPtr<nsIMsgPluggableStore> msgStore; + folder->GetMsgStore(getter_AddRefs(msgStore)); + if (!msgStore) { + continue; + } + bool storeSupportsCompaction; + msgStore->GetSupportsCompaction(&storeSupportsCompaction); + if (storeSupportsCompaction) { + foldersToCompact.AppendElement(folder); + } + } + nsresult rv = + folderCompactor->CompactFolders(foldersToCompact, listener, msgWindow); + if (NS_FAILED(rv) && listener) { + // Make sure the listener hears about the failure. + listener->OnStopRunningUrl(nullptr, rv); + } + }; + + // Collect all the expungeable folders. + nsTArray<RefPtr<nsIMsgImapMailFolder>> foldersToExpunge; + nsTArray<RefPtr<nsIMsgFolder>> allDescendants; + rootFolder->GetDescendants(allDescendants); + for (auto folder : allDescendants) { + nsCOMPtr<nsIMsgImapMailFolder> imapFolder(do_QueryInterface(folder)); + if (!imapFolder) { + continue; + } + uint32_t folderFlags; + folder->GetFlags(&folderFlags); + if (!(folderFlags & + (nsMsgFolderFlags::Virtual | nsMsgFolderFlags::ImapNoselect))) { + foldersToExpunge.AppendElement(imapFolder); + } + } + + if (!WeAreOffline() && !foldersToExpunge.IsEmpty()) { + // Kick off expunge on all the folders (the IMAP protocol will handle + // queuing them up as needed). + + // A listener to track the completed expunges. + RefPtr<UrlListener> l = new UrlListener(); + l->mStopFn = [expungeCount = foldersToExpunge.Length(), doCompact]( + nsIURI* url, nsresult status) mutable -> nsresult { + // NOTE: we're ignoring expunge result code - nothing much we can do + // here to recover, so just plough on. + --expungeCount; + if (expungeCount == 0) { + // All the expunges are done so start compacting. + doCompact(); + } + return NS_OK; + }; + // Go! + for (auto& imapFolder : foldersToExpunge) { + rv = imapFolder->Expunge(l, aMsgWindow); + if (NS_FAILED(rv)) { + // Make sure expungeCount is kept in sync! + l->OnStopRunningUrl(nullptr, rv); + } + } + } else { + // No expunging. Start the compaction immediately. + doCompact(); + } + + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::UpdateStatus(nsIUrlListener* aListener, + nsIMsgWindow* aMsgWindow) { + nsresult rv; + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIURI> uri; + rv = imapService->UpdateFolderStatus(this, aListener, getter_AddRefs(uri)); + if (uri && !aMsgWindow) { + nsCOMPtr<nsIMsgMailNewsUrl> mailNewsUrl = do_QueryInterface(uri, &rv); + NS_ENSURE_SUCCESS(rv, rv); + // if no msg window, we won't put up error messages (this is almost + // certainly a biff-inspired status) + mailNewsUrl->SetSuppressErrorMsgs(true); + } + return rv; +} + +NS_IMETHODIMP nsImapMailFolder::EmptyTrash(nsIUrlListener* aListener) { + nsCOMPtr<nsIMsgFolder> trashFolder; + nsresult rv = GetTrashFolder(getter_AddRefs(trashFolder)); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIMsgAccountManager> accountManager = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + // if we are emptying trash on exit and we are an aol server then don't + // perform this operation because it's causing a hang that we haven't been + // able to figure out yet this is an rtm fix and we'll look for the right + // solution post rtm. + bool empytingOnExit = false; + accountManager->GetEmptyTrashInProgress(&empytingOnExit); + if (empytingOnExit) { + nsCOMPtr<nsIImapIncomingServer> imapServer; + rv = GetImapIncomingServer(getter_AddRefs(imapServer)); + if (imapServer) { + bool isAOLServer = false; + imapServer->GetIsAOLServer(&isAOLServer); + if (isAOLServer) + return NS_ERROR_FAILURE; // we will not be performing an empty + // trash.... + } // if we fetched an imap server + } // if emptying trash on exit which is done through the account manager. + + if (WeAreOffline()) { + nsCOMPtr<nsIMsgDatabase> trashDB; + rv = trashFolder->GetMsgDatabase(getter_AddRefs(trashDB)); + if (trashDB) { + nsCOMPtr<nsIMsgOfflineOpsDatabase> opsDb = + do_QueryInterface(trashDB, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsMsgKey fakeKey; + opsDb->GetNextFakeOfflineMsgKey(&fakeKey); + + nsCOMPtr<nsIMsgOfflineImapOperation> op; + rv = opsDb->GetOfflineOpForKey(fakeKey, true, getter_AddRefs(op)); + trashFolder->SetFlag(nsMsgFolderFlags::OfflineEvents); + op->SetOperation(nsIMsgOfflineImapOperation::kDeleteAllMsgs); + } + return rv; + } + + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + if (aListener) + rv = imapService->DeleteAllMessages(trashFolder, aListener); + else { + nsCOMPtr<nsIUrlListener> urlListener = do_QueryInterface(trashFolder); + rv = imapService->DeleteAllMessages(trashFolder, urlListener); + } + // Return an error if this failed. We want the empty trash on exit code + // to know if this fails so that it doesn't block waiting for empty trash to + // finish. + NS_ENSURE_SUCCESS(rv, rv); + + // Delete any subfolders under Trash. + nsTArray<RefPtr<nsIMsgFolder>> subFolders; + rv = trashFolder->GetSubFolders(subFolders); + NS_ENSURE_SUCCESS(rv, rv); + while (!subFolders.IsEmpty()) { + RefPtr<nsIMsgFolder> f = subFolders.PopLastElement(); + rv = trashFolder->PropagateDelete(f, true); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr<nsIDBFolderInfo> transferInfo; + rv = trashFolder->GetDBTransferInfo(getter_AddRefs(transferInfo)); + NS_ENSURE_SUCCESS(rv, rv); + // Bulk-delete all the messages by deleting the msf file and storage. + // This is a little kludgy. + rv = trashFolder->DeleteStorage(); + NS_ENSURE_SUCCESS(rv, rv); + if (transferInfo) trashFolder->SetDBTransferInfo(transferInfo); + trashFolder->SetSizeOnDisk(0); + + // The trash folder has effectively been deleted. + nsCOMPtr<nsIMsgFolderNotificationService> notifier( + do_GetService("@mozilla.org/messenger/msgnotificationservice;1")); + if (notifier) notifier->NotifyFolderDeleted(trashFolder); + + return NS_OK; + } + return rv; +} + +NS_IMETHODIMP nsImapMailFolder::DeleteStorage() { + nsresult rv = nsMsgDBFolder::DeleteStorage(); + + // Should notify nsIMsgFolderListeners about the folder getting deleted? + return rv; +} + +NS_IMETHODIMP nsImapMailFolder::Rename(const nsAString& newName, + nsIMsgWindow* msgWindow) { + if (mFlags & nsMsgFolderFlags::Virtual) + return nsMsgDBFolder::Rename(newName, msgWindow); + nsresult rv; + nsAutoString newNameStr(newName); + if (newNameStr.FindChar(m_hierarchyDelimiter, 0) != kNotFound) { + nsCOMPtr<nsIDocShell> docShell; + if (msgWindow) msgWindow->GetRootDocShell(getter_AddRefs(docShell)); + if (docShell) { + nsCOMPtr<nsIStringBundle> bundle; + rv = IMAPGetStringBundle(getter_AddRefs(bundle)); + if (NS_SUCCEEDED(rv) && bundle) { + AutoTArray<nsString, 1> formatStrings; + formatStrings.AppendElement()->Append(m_hierarchyDelimiter); + nsString alertString; + rv = bundle->FormatStringFromName("imapSpecialChar2", formatStrings, + alertString); + nsCOMPtr<nsIPrompt> dialog(do_GetInterface(docShell)); + // setting up the dialog title + nsCOMPtr<nsIMsgIncomingServer> server; + rv = GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + nsString dialogTitle; + nsString accountName; + rv = server->GetPrettyName(accountName); + NS_ENSURE_SUCCESS(rv, rv); + AutoTArray<nsString, 1> titleParams = {accountName}; + rv = bundle->FormatStringFromName("imapAlertDialogTitle", titleParams, + dialogTitle); + + if (dialog && !alertString.IsEmpty()) + dialog->Alert(dialogTitle.get(), alertString.get()); + } + } + return NS_ERROR_FAILURE; + } + nsCOMPtr<nsIImapIncomingServer> incomingImapServer; + GetImapIncomingServer(getter_AddRefs(incomingImapServer)); + if (incomingImapServer) RecursiveCloseActiveConnections(incomingImapServer); + + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + return imapService->RenameLeaf(this, newName, this, msgWindow); +} + +NS_IMETHODIMP nsImapMailFolder::RecursiveCloseActiveConnections( + nsIImapIncomingServer* incomingImapServer) { + NS_ENSURE_ARG(incomingImapServer); + + nsCOMPtr<nsIMsgImapMailFolder> folder; + int32_t count = mSubFolders.Count(); + for (int32_t i = 0; i < count; i++) { + folder = do_QueryInterface(mSubFolders[i]); + if (folder) folder->RecursiveCloseActiveConnections(incomingImapServer); + + incomingImapServer->CloseConnectionForFolder(mSubFolders[i]); + } + return NS_OK; +} + +// this is called *after* we've done the rename on the server. +NS_IMETHODIMP nsImapMailFolder::PrepareToRename() { + nsCOMPtr<nsIMsgImapMailFolder> folder; + int32_t count = mSubFolders.Count(); + for (int32_t i = 0; i < count; i++) { + folder = do_QueryInterface(mSubFolders[i]); + if (folder) folder->PrepareToRename(); + } + + SetOnlineName(EmptyCString()); + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::RenameLocal(const nsACString& newName, + nsIMsgFolder* parent) { + nsAutoCString leafname(newName); + nsAutoCString parentName; + // newName always in the canonical form "greatparent/parentname/leafname" + int32_t leafpos = leafname.RFindChar('/'); + if (leafpos > 0) leafname.Cut(0, leafpos + 1); + m_msgParser = nullptr; + PrepareToRename(); + CloseAndBackupFolderDB(leafname); + + nsresult rv = NS_OK; + nsCOMPtr<nsIFile> oldPathFile; + rv = GetFilePath(getter_AddRefs(oldPathFile)); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIFile> parentPathFile; + rv = parent->GetFilePath(getter_AddRefs(parentPathFile)); + NS_ENSURE_SUCCESS(rv, rv); + + bool isDirectory = false; + parentPathFile->IsDirectory(&isDirectory); + if (!isDirectory) AddDirectorySeparator(parentPathFile); + + nsCOMPtr<nsIFile> dirFile; + + int32_t count = mSubFolders.Count(); + if (count > 0) { + rv = CreateDirectoryForFolder(getter_AddRefs(dirFile)); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr<nsIFile> oldSummaryFile; + rv = GetSummaryFileLocation(oldPathFile, getter_AddRefs(oldSummaryFile)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString newNameStr; + oldSummaryFile->Remove(false); + if (count > 0) { + newNameStr = leafname; + NS_MsgHashIfNecessary(newNameStr); + newNameStr.AppendLiteral(FOLDER_SUFFIX8); + nsAutoCString leafName; + dirFile->GetNativeLeafName(leafName); + if (!leafName.Equals(newNameStr)) + return dirFile->MoveToNative( + nullptr, + newNameStr); // in case of rename operation leaf names will differ + + parentPathFile->AppendNative( + newNameStr); // only for move we need to progress further in case the + // parent differs + bool isDirectory = false; + parentPathFile->IsDirectory(&isDirectory); + if (!isDirectory) { + rv = parentPathFile->Create(nsIFile::DIRECTORY_TYPE, 0700); + NS_ENSURE_SUCCESS(rv, rv); + } else { + NS_ERROR("Directory already exists."); + } + rv = RecursiveCopy(dirFile, parentPathFile); + NS_ENSURE_SUCCESS(rv, rv); + dirFile->Remove(true); // moving folders + } + return rv; +} + +NS_IMETHODIMP nsImapMailFolder::GetPrettyName(nsAString& prettyName) { + return GetName(prettyName); +} + +NS_IMETHODIMP nsImapMailFolder::UpdateSummaryTotals(bool force) { + // bug 72871 inserted the mIsServer check for IMAP + return mIsServer ? NS_OK : nsMsgDBFolder::UpdateSummaryTotals(force); +} + +NS_IMETHODIMP nsImapMailFolder::GetDeletable(bool* deletable) { + NS_ENSURE_ARG_POINTER(deletable); + + bool isServer; + GetIsServer(&isServer); + + *deletable = !(isServer || (mFlags & nsMsgFolderFlags::SpecialUse)); + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::GetSizeOnDisk(int64_t* size) { + NS_ENSURE_ARG_POINTER(size); + + bool isServer = false; + nsresult rv = GetIsServer(&isServer); + // If this is the rootFolder, return 0 as a safe value. + if (NS_FAILED(rv) || isServer) mFolderSize = 0; + + *size = mFolderSize; + return NS_OK; +} + +NS_IMETHODIMP +nsImapMailFolder::GetCanCreateSubfolders(bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + *aResult = !(mFlags & + (nsMsgFolderFlags::ImapNoinferiors | nsMsgFolderFlags::Virtual)); + + bool isServer = false; + GetIsServer(&isServer); + if (!isServer) { + nsCOMPtr<nsIImapIncomingServer> imapServer; + nsresult rv = GetImapIncomingServer(getter_AddRefs(imapServer)); + bool dualUseFolders = true; + if (NS_SUCCEEDED(rv) && imapServer) + imapServer->GetDualUseFolders(&dualUseFolders); + if (!dualUseFolders && *aResult) + *aResult = (mFlags & nsMsgFolderFlags::ImapNoselect); + } + return NS_OK; +} + +NS_IMETHODIMP +nsImapMailFolder::GetCanSubscribe(bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + *aResult = false; + + bool isImapServer = false; + nsresult rv = GetIsServer(&isImapServer); + if (NS_FAILED(rv)) return rv; + // you can only subscribe to imap servers, not imap folders + *aResult = isImapServer; + return NS_OK; +} + +nsresult nsImapMailFolder::GetServerKey(nsACString& serverKey) { + // look for matching imap folders, then pop folders + nsCOMPtr<nsIMsgIncomingServer> server; + nsresult rv = GetServer(getter_AddRefs(server)); + if (NS_SUCCEEDED(rv)) rv = server->GetKey(serverKey); + return rv; +} + +NS_IMETHODIMP +nsImapMailFolder::GetImapIncomingServer( + nsIImapIncomingServer** aImapIncomingServer) { + NS_ENSURE_ARG(aImapIncomingServer); + nsCOMPtr<nsIMsgIncomingServer> server; + if (NS_SUCCEEDED(GetServer(getter_AddRefs(server))) && server) { + nsCOMPtr<nsIImapIncomingServer> incomingServer = do_QueryInterface(server); + NS_ENSURE_TRUE(incomingServer, NS_ERROR_NO_INTERFACE); + incomingServer.forget(aImapIncomingServer); + return NS_OK; + } + return NS_ERROR_NULL_POINTER; +} + +NS_IMETHODIMP +nsImapMailFolder::AddMessageDispositionState( + nsIMsgDBHdr* aMessage, nsMsgDispositionState aDispositionFlag) { + nsMsgDBFolder::AddMessageDispositionState(aMessage, aDispositionFlag); + + // set the mark message answered flag on the server for this message... + if (aMessage) { + nsMsgKey msgKey; + aMessage->GetMessageKey(&msgKey); + + if (aDispositionFlag == nsIMsgFolder::nsMsgDispositionState_Replied) + StoreImapFlags(kImapMsgAnsweredFlag, true, {msgKey}, nullptr); + else if (aDispositionFlag == nsIMsgFolder::nsMsgDispositionState_Forwarded) + StoreImapFlags(kImapMsgForwardedFlag, true, {msgKey}, nullptr); + } + return NS_OK; +} + +NS_IMETHODIMP +nsImapMailFolder::MarkMessagesRead( + const nsTArray<RefPtr<nsIMsgDBHdr>>& messages, bool markRead) { + // tell the folder to do it, which will mark them read in the db. + nsresult rv = nsMsgDBFolder::MarkMessagesRead(messages, markRead); + if (NS_SUCCEEDED(rv)) { + nsAutoCString messageIds; + nsTArray<nsMsgKey> keysToMarkRead; + rv = BuildIdsAndKeyArray(messages, messageIds, keysToMarkRead); + NS_ENSURE_SUCCESS(rv, rv); + + StoreImapFlags(kImapMsgSeenFlag, markRead, keysToMarkRead, nullptr); + rv = GetDatabase(); + if (NS_SUCCEEDED(rv)) mDatabase->Commit(nsMsgDBCommitType::kLargeCommit); + } + return rv; +} + +NS_IMETHODIMP +nsImapMailFolder::MarkAllMessagesRead(nsIMsgWindow* aMsgWindow) { + nsresult rv = GetDatabase(); + if (NS_SUCCEEDED(rv)) { + nsTArray<nsMsgKey> thoseMarked; + EnableNotifications(allMessageCountNotifications, false); + rv = mDatabase->MarkAllRead(thoseMarked); + EnableNotifications(allMessageCountNotifications, true); + if (NS_SUCCEEDED(rv) && thoseMarked.Length() > 0) { + rv = StoreImapFlags(kImapMsgSeenFlag, true, thoseMarked, nullptr); + mDatabase->Commit(nsMsgDBCommitType::kLargeCommit); + + // Setup a undo-state + if (aMsgWindow) + rv = AddMarkAllReadUndoAction(aMsgWindow, thoseMarked.Elements(), + thoseMarked.Length()); + } + } + return rv; +} + +NS_IMETHODIMP nsImapMailFolder::MarkThreadRead(nsIMsgThread* thread) { + nsresult rv = GetDatabase(); + if (NS_SUCCEEDED(rv)) { + nsTArray<nsMsgKey> keys; + rv = mDatabase->MarkThreadRead(thread, nullptr, keys); + if (NS_SUCCEEDED(rv) && keys.Length() > 0) { + rv = StoreImapFlags(kImapMsgSeenFlag, true, keys, nullptr); + mDatabase->Commit(nsMsgDBCommitType::kLargeCommit); + } + } + return rv; +} + +NS_IMETHODIMP nsImapMailFolder::ReadFromFolderCacheElem( + nsIMsgFolderCacheElement* element) { + nsresult rv = nsMsgDBFolder::ReadFromFolderCacheElem(element); + int32_t hierarchyDelimiter = kOnlineHierarchySeparatorUnknown; + nsCString onlineName; + + element->GetCachedUInt32("boxFlags", (uint32_t*)&m_boxFlags); + if (NS_SUCCEEDED(element->GetCachedInt32("hierDelim", &hierarchyDelimiter)) && + hierarchyDelimiter != kOnlineHierarchySeparatorUnknown) + m_hierarchyDelimiter = (char)hierarchyDelimiter; + rv = element->GetCachedString("onlineName", onlineName); + if (NS_SUCCEEDED(rv) && !onlineName.IsEmpty()) + m_onlineFolderName.Assign(onlineName); + + m_aclFlags = kAclInvalid; // init to invalid value. + element->GetCachedUInt32("aclFlags", &m_aclFlags); + element->GetCachedInt32("serverTotal", &m_numServerTotalMessages); + element->GetCachedInt32("serverUnseen", &m_numServerUnseenMessages); + element->GetCachedInt32("serverRecent", &m_numServerRecentMessages); + element->GetCachedInt32("nextUID", &m_nextUID); + int32_t lastSyncTimeInSec; + if (NS_FAILED(element->GetCachedInt32("lastSyncTimeInSec", + (int32_t*)&lastSyncTimeInSec))) + lastSyncTimeInSec = 0U; + + // make sure that auto-sync state object is created + InitAutoSyncState(); + m_autoSyncStateObj->SetLastSyncTimeInSec(lastSyncTimeInSec); + + return rv; +} + +NS_IMETHODIMP nsImapMailFolder::WriteToFolderCacheElem( + nsIMsgFolderCacheElement* element) { + nsresult rv = nsMsgDBFolder::WriteToFolderCacheElem(element); + element->SetCachedUInt32("boxFlags", (uint32_t)m_boxFlags); + element->SetCachedInt32("hierDelim", (int32_t)m_hierarchyDelimiter); + element->SetCachedString("onlineName", m_onlineFolderName); + element->SetCachedUInt32("aclFlags", m_aclFlags); + element->SetCachedInt32("serverTotal", m_numServerTotalMessages); + element->SetCachedInt32("serverUnseen", m_numServerUnseenMessages); + element->SetCachedInt32("serverRecent", m_numServerRecentMessages); + if (m_nextUID != (int32_t)nsMsgKey_None) + element->SetCachedInt32("nextUID", m_nextUID); + + // store folder's last sync time + if (m_autoSyncStateObj) { + PRTime lastSyncTime; + m_autoSyncStateObj->GetLastSyncTime(&lastSyncTime); + // store in sec + element->SetCachedInt32("lastSyncTimeInSec", + (int32_t)(lastSyncTime / PR_USEC_PER_SEC)); + } + + return rv; +} + +NS_IMETHODIMP +nsImapMailFolder::MarkMessagesFlagged( + const nsTArray<RefPtr<nsIMsgDBHdr>>& messages, bool markFlagged) { + nsresult rv; + // tell the folder to do it, which will mark them read in the db. + rv = nsMsgDBFolder::MarkMessagesFlagged(messages, markFlagged); + if (NS_SUCCEEDED(rv)) { + nsAutoCString messageIds; + nsTArray<nsMsgKey> keysToMarkFlagged; + rv = BuildIdsAndKeyArray(messages, messageIds, keysToMarkFlagged); + if (NS_FAILED(rv)) return rv; + rv = StoreImapFlags(kImapMsgFlaggedFlag, markFlagged, keysToMarkFlagged, + nullptr); + NS_ENSURE_SUCCESS(rv, rv); + rv = GetDatabase(); + NS_ENSURE_SUCCESS(rv, rv); + mDatabase->Commit(nsMsgDBCommitType::kLargeCommit); + } + return rv; +} + +NS_IMETHODIMP nsImapMailFolder::SetOnlineName( + const nsACString& aOnlineFolderName) { + nsresult rv; + nsCOMPtr<nsIMsgDatabase> db; + nsCOMPtr<nsIDBFolderInfo> folderInfo; + rv = GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), getter_AddRefs(db)); + // do this after GetDBFolderInfoAndDB, because it crunches m_onlineFolderName + // (not sure why) + m_onlineFolderName = aOnlineFolderName; + if (NS_SUCCEEDED(rv) && folderInfo) { + nsAutoString onlineName; + CopyUTF8toUTF16(aOnlineFolderName, onlineName); + rv = folderInfo->SetProperty("onlineName", onlineName); + rv = folderInfo->SetMailboxName(onlineName); + // so, when are we going to commit this? Definitely not every time! + // We could check if the online name has changed. + db->Commit(nsMsgDBCommitType::kLargeCommit); + } + folderInfo = nullptr; + return rv; +} + +NS_IMETHODIMP nsImapMailFolder::GetOnlineName(nsACString& aOnlineFolderName) { + ReadDBFolderInfo(false); // update cache first. + aOnlineFolderName = m_onlineFolderName; + return NS_OK; +} + +NS_IMETHODIMP +nsImapMailFolder::GetDBFolderInfoAndDB(nsIDBFolderInfo** folderInfo, + nsIMsgDatabase** db) { + NS_ENSURE_ARG_POINTER(folderInfo); + NS_ENSURE_ARG_POINTER(db); + + nsresult rv = GetDatabase(); + if (NS_FAILED(rv)) return rv; + + NS_ADDREF(*db = mDatabase); + + rv = (*db)->GetDBFolderInfo(folderInfo); + if (NS_FAILED(rv)) + return rv; // GetDBFolderInfo can't return NS_OK if !folderInfo + + nsCString onlineName; + rv = (*folderInfo)->GetCharProperty("onlineName", onlineName); + if (NS_FAILED(rv)) return rv; + + if (!onlineName.IsEmpty()) + m_onlineFolderName.Assign(onlineName); + else { + nsAutoString autoOnlineName; + (*folderInfo)->GetMailboxName(autoOnlineName); + if (autoOnlineName.IsEmpty()) { + nsCString uri; + rv = GetURI(uri); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString hostname; + rv = GetHostname(hostname); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString onlineCName; + rv = nsImapURI2FullName(kImapRootURI, hostname.get(), uri.get(), + getter_Copies(onlineCName)); + // Note: check for unknown separator '^' only became needed + // with UTF8=ACCEPT modification and haven't found why. Online name + // contained the '^' delimiter and gmail said "NO" when folder under + // [Gmail] is created and selected. + if ((m_hierarchyDelimiter != '/') && + (m_hierarchyDelimiter != kOnlineHierarchySeparatorUnknown)) + onlineCName.ReplaceChar('/', m_hierarchyDelimiter); + // XXX: What if online name contains slashes? Breaks? + m_onlineFolderName.Assign(onlineCName); + CopyUTF8toUTF16(onlineCName, autoOnlineName); + } + (*folderInfo)->SetProperty("onlineName", autoOnlineName); + } + return rv; +} + +/* static */ +nsresult nsImapMailFolder::BuildIdsAndKeyArray( + const nsTArray<RefPtr<nsIMsgDBHdr>>& messages, nsCString& msgIds, + nsTArray<nsMsgKey>& keyArray) { + keyArray.Clear(); + keyArray.SetCapacity(messages.Length()); + // build up message keys. + for (auto msgDBHdr : messages) { + nsMsgKey key; + nsresult rv = msgDBHdr->GetMessageKey(&key); + if (NS_SUCCEEDED(rv)) keyArray.AppendElement(key); + } + return AllocateUidStringFromKeys(keyArray, msgIds); +} + +/* static */ +nsresult nsImapMailFolder::AllocateUidStringFromKeys( + const nsTArray<nsMsgKey>& keys, nsCString& msgIds) { + if (keys.IsEmpty()) return NS_ERROR_INVALID_ARG; + nsresult rv = NS_OK; + uint32_t startSequence; + startSequence = keys[0]; + uint32_t curSequenceEnd = startSequence; + uint32_t total = keys.Length(); + // sort keys and then generate ranges instead of singletons! + nsTArray<nsMsgKey> sorted(keys.Clone()); + sorted.Sort(); + for (uint32_t keyIndex = 0; keyIndex < total; keyIndex++) { + uint32_t curKey = sorted[keyIndex]; + uint32_t nextKey = + (keyIndex + 1 < total) ? sorted[keyIndex + 1] : 0xFFFFFFFF; + bool lastKey = (nextKey == 0xFFFFFFFF); + + if (lastKey) curSequenceEnd = curKey; + if (nextKey == (uint32_t)curSequenceEnd + 1 && !lastKey) { + curSequenceEnd = nextKey; + continue; + } + if (curSequenceEnd > startSequence) { + AppendUid(msgIds, startSequence); + msgIds += ':'; + AppendUid(msgIds, curSequenceEnd); + if (!lastKey) msgIds += ','; + startSequence = nextKey; + curSequenceEnd = startSequence; + } else { + startSequence = nextKey; + curSequenceEnd = startSequence; + AppendUid(msgIds, sorted[keyIndex]); + if (!lastKey) msgIds += ','; + } + } + return rv; +} + +nsresult nsImapMailFolder::MarkMessagesImapDeleted(nsTArray<nsMsgKey>* keyArray, + bool deleted, + nsIMsgDatabase* db) { + for (uint32_t kindex = 0; kindex < keyArray->Length(); kindex++) { + nsMsgKey key = keyArray->ElementAt(kindex); + db->MarkImapDeleted(key, deleted, nullptr); + } + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::DeleteMessages( + nsTArray<RefPtr<nsIMsgDBHdr>> const& msgHeaders, nsIMsgWindow* msgWindow, + bool deleteStorage, bool isMove, nsIMsgCopyServiceListener* listener, + bool allowUndo) { + // *** jt - assuming delete is move to the trash folder for now + nsAutoCString uri; + bool deleteImmediatelyNoTrash = false; + nsAutoCString messageIds; + nsTArray<nsMsgKey> srcKeyArray; + bool deleteMsgs = true; // used for toggling delete status - default is true + nsMsgImapDeleteModel deleteModel = nsMsgImapDeleteModels::MoveToTrash; + imapMessageFlagsType messageFlags = kImapMsgDeletedFlag; + + nsCOMPtr<nsIImapIncomingServer> imapServer; + nsresult rv = GetFlag(nsMsgFolderFlags::Trash, &deleteImmediatelyNoTrash); + rv = GetImapIncomingServer(getter_AddRefs(imapServer)); + + if (NS_SUCCEEDED(rv) && imapServer) { + imapServer->GetDeleteModel(&deleteModel); + if (deleteModel != nsMsgImapDeleteModels::MoveToTrash || deleteStorage) + deleteImmediatelyNoTrash = true; + // if we're deleting a message, we should pseudo-interrupt the msg + // load of the current message. + bool interrupted = false; + imapServer->PseudoInterruptMsgLoad(this, msgWindow, &interrupted); + } + + rv = BuildIdsAndKeyArray(msgHeaders, messageIds, srcKeyArray); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIMsgFolder> rootFolder; + nsCOMPtr<nsIMsgFolder> trashFolder; + + if (!deleteImmediatelyNoTrash) { + rv = GetRootFolder(getter_AddRefs(rootFolder)); + if (NS_SUCCEEDED(rv) && rootFolder) { + rootFolder->GetFolderWithFlags(nsMsgFolderFlags::Trash, + getter_AddRefs(trashFolder)); + NS_ASSERTION(trashFolder, "couldn't find trash"); + // if we can't find the trash, we'll just have to do an imap delete and + // pretend this is the trash + if (!trashFolder) deleteImmediatelyNoTrash = true; + } + } + + if ((NS_SUCCEEDED(rv) && deleteImmediatelyNoTrash) || + deleteModel == nsMsgImapDeleteModels::IMAPDelete) { + if (allowUndo) { + // need to take care of these two delete models + RefPtr<nsImapMoveCopyMsgTxn> undoMsgTxn = new nsImapMoveCopyMsgTxn; + if (!undoMsgTxn || + NS_FAILED(undoMsgTxn->Init(this, &srcKeyArray, messageIds.get(), + nullptr, true, isMove))) + return NS_ERROR_OUT_OF_MEMORY; + + undoMsgTxn->SetTransactionType(nsIMessenger::eDeleteMsg); + // we're adding this undo action before the delete is successful. This is + // evil, but 4.5 did it as well. + nsCOMPtr<nsITransactionManager> txnMgr; + if (msgWindow) msgWindow->GetTransactionManager(getter_AddRefs(txnMgr)); + if (txnMgr) txnMgr->DoTransaction(undoMsgTxn); + } + + if (deleteModel == nsMsgImapDeleteModels::IMAPDelete && !deleteStorage) { + deleteMsgs = false; + for (nsIMsgDBHdr* msgHdr : msgHeaders) { + if (!msgHdr) { + continue; + } + uint32_t flags; + msgHdr->GetFlags(&flags); + if (!(flags & nsMsgMessageFlags::IMAPDeleted)) { + deleteMsgs = true; + break; + } + } + } + // if copy service listener is also a url listener, pass that + // url listener into StoreImapFlags. + nsCOMPtr<nsIUrlListener> urlListener = do_QueryInterface(listener); + if (deleteMsgs) messageFlags |= kImapMsgSeenFlag; + rv = StoreImapFlags(messageFlags, deleteMsgs, srcKeyArray, urlListener); + + if (NS_SUCCEEDED(rv)) { + if (mDatabase) { + nsCOMPtr<nsIMsgDatabase> database(mDatabase); + if (deleteModel == nsMsgImapDeleteModels::IMAPDelete) + MarkMessagesImapDeleted(&srcKeyArray, deleteMsgs, database); + else { + EnableNotifications(allMessageCountNotifications, + false); //"remove it immediately" model + // Notify if this is an actual delete. + if (!isMove) { + nsCOMPtr<nsIMsgFolderNotificationService> notifier(do_GetService( + "@mozilla.org/messenger/msgnotificationservice;1")); + if (notifier) notifier->NotifyMsgsDeleted(msgHeaders); + } + DeleteStoreMessages(msgHeaders); + database->DeleteMessages(srcKeyArray, nullptr); + EnableNotifications(allMessageCountNotifications, true); + } + if (listener) { + listener->OnStartCopy(); + listener->OnStopCopy(NS_OK); + } + NotifyFolderEvent(kDeleteOrMoveMsgCompleted); + } + } + return rv; + } + + // have to move the messages to the trash + if (trashFolder) { + nsCOMPtr<nsIMsgFolder> srcFolder; + nsCOMPtr<nsISupports> srcSupport; + + rv = QueryInterface(NS_GET_IID(nsIMsgFolder), getter_AddRefs(srcFolder)); + nsCOMPtr<nsIMsgCopyService> copyService = + do_GetService("@mozilla.org/messenger/messagecopyservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = copyService->CopyMessages(srcFolder, msgHeaders, trashFolder, true, + listener, msgWindow, allowUndo); + } + + return rv; +} + +// check if folder is the trash, or a descendent of the trash +// so we can tell if the folders we're deleting from it should +// be *really* deleted. +bool nsImapMailFolder::TrashOrDescendentOfTrash(nsIMsgFolder* folder) { + NS_ENSURE_TRUE(folder, false); + nsCOMPtr<nsIMsgFolder> parent; + nsCOMPtr<nsIMsgFolder> curFolder = folder; + nsresult rv; + uint32_t flags = 0; + do { + rv = curFolder->GetFlags(&flags); + if (NS_FAILED(rv)) return false; + if (flags & nsMsgFolderFlags::Trash) return true; + curFolder->GetParent(getter_AddRefs(parent)); + if (!parent) return false; + curFolder = parent; + } while (NS_SUCCEEDED(rv) && curFolder); + return false; +} +NS_IMETHODIMP +nsImapMailFolder::DeleteSelf(nsIMsgWindow* msgWindow) { + nsCOMPtr<nsIMsgFolder> trashFolder; + nsresult rv; + uint32_t folderFlags; + + // No IMAP shenanigans required for virtual folders. + GetFlags(&folderFlags); + if (folderFlags & nsMsgFolderFlags::Virtual) { + return nsMsgDBFolder::DeleteSelf(nullptr); + } + + // "this" is the folder we're deleting from + bool deleteNoTrash = TrashOrDescendentOfTrash(this) || !DeleteIsMoveToTrash(); + bool confirmDeletion = true; + + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + if (!deleteNoTrash) { + rv = GetTrashFolder(getter_AddRefs(trashFolder)); + // If we can't find the trash folder and we are supposed to move it to the + // trash return failure. + if (NS_FAILED(rv) || !trashFolder) return NS_ERROR_FAILURE; + bool canHaveSubFoldersOfTrash = true; + trashFolder->GetCanCreateSubfolders(&canHaveSubFoldersOfTrash); + if (canHaveSubFoldersOfTrash) // UW server doesn't set NOINFERIORS - check + // dual use pref + { + nsCOMPtr<nsIImapIncomingServer> imapServer; + rv = GetImapIncomingServer(getter_AddRefs(imapServer)); + NS_ENSURE_SUCCESS(rv, rv); + bool serverSupportsDualUseFolders; + imapServer->GetDualUseFolders(&serverSupportsDualUseFolders); + if (!serverSupportsDualUseFolders) canHaveSubFoldersOfTrash = false; + } + if (!canHaveSubFoldersOfTrash) deleteNoTrash = true; + nsCOMPtr<nsIPrefBranch> prefBranch( + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + prefBranch->GetBoolPref("mailnews.confirm.moveFoldersToTrash", + &confirmDeletion); + } + + // If we are deleting folder immediately, ask user for confirmation. + bool confirmed = false; + if (confirmDeletion || deleteNoTrash) { + nsCOMPtr<nsIStringBundle> bundle; + rv = IMAPGetStringBundle(getter_AddRefs(bundle)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString folderName; + rv = GetName(folderName); + NS_ENSURE_SUCCESS(rv, rv); + AutoTArray<nsString, 1> formatStrings = {folderName}; + + nsAutoString deleteFolderDialogTitle; + rv = bundle->GetStringFromName("imapDeleteFolderDialogTitle", + deleteFolderDialogTitle); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString deleteFolderButtonLabel; + rv = bundle->GetStringFromName("imapDeleteFolderButtonLabel", + deleteFolderButtonLabel); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString confirmationStr; + rv = bundle->FormatStringFromName( + (deleteNoTrash) ? "imapDeleteNoTrash" : "imapMoveFolderToTrash", + formatStrings, confirmationStr); + NS_ENSURE_SUCCESS(rv, rv); + if (!msgWindow) return NS_ERROR_NULL_POINTER; + nsCOMPtr<nsIDocShell> docShell; + msgWindow->GetRootDocShell(getter_AddRefs(docShell)); + nsCOMPtr<nsIPrompt> dialog; + if (docShell) dialog = do_GetInterface(docShell); + if (dialog) { + int32_t buttonPressed = 0; + // Default the dialog to "cancel". + const uint32_t buttonFlags = + (nsIPrompt::BUTTON_TITLE_IS_STRING * nsIPrompt::BUTTON_POS_0) + + (nsIPrompt::BUTTON_TITLE_CANCEL * nsIPrompt::BUTTON_POS_1); + + bool dummyValue = false; + rv = dialog->ConfirmEx(deleteFolderDialogTitle.get(), + confirmationStr.get(), buttonFlags, + deleteFolderButtonLabel.get(), nullptr, nullptr, + nullptr, &dummyValue, &buttonPressed); + NS_ENSURE_SUCCESS(rv, rv); + confirmed = !buttonPressed; // "ok" is in position 0 + } + } else { + confirmed = true; + } + + if (confirmed) { + if (deleteNoTrash) { + rv = imapService->DeleteFolder(this, this, msgWindow); + nsMsgDBFolder::DeleteSelf(msgWindow); + } else { + bool match = false; + rv = MatchOrChangeFilterDestination(nullptr, false, &match); + if (match) { + bool confirm = false; + ConfirmFolderDeletionForFilter(msgWindow, &confirm); + if (!confirm) return NS_OK; + } + rv = imapService->MoveFolder(this, trashFolder, this, msgWindow); + } + } + return rv; +} + +// FIXME: helper function to know whether we should check all IMAP folders +// for new mail; this is necessary because of a legacy hidden preference +// mail.check_all_imap_folders_for_new (now replaced by per-server preference +// mail.server.%serverkey%.check_all_folders_for_new), still present in some +// profiles. +/*static*/ +bool nsImapMailFolder::ShouldCheckAllFolders( + nsIImapIncomingServer* imapServer) { + // Check legacy global preference to see if we should check all folders for + // new messages, or just the inbox and marked ones. + bool checkAllFolders = false; + nsresult rv; + nsCOMPtr<nsIPrefBranch> prefBranch = + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, false); + // This pref might not exist, which is OK. + (void)prefBranch->GetBoolPref("mail.check_all_imap_folders_for_new", + &checkAllFolders); + + if (checkAllFolders) return true; + + // If the legacy preference doesn't exist or has its default value (False), + // the true preference is read. + imapServer->GetCheckAllFoldersForNew(&checkAllFolders); + return checkAllFolders; +} + +// Called by Biff, or when user presses GetMsg button. +NS_IMETHODIMP nsImapMailFolder::GetNewMessages(nsIMsgWindow* aWindow, + nsIUrlListener* aListener) { + nsCOMPtr<nsIMsgFolder> rootFolder; + nsresult rv = GetRootFolder(getter_AddRefs(rootFolder)); + if (NS_SUCCEEDED(rv) && rootFolder) { + nsCOMPtr<nsIImapIncomingServer> imapServer; + rv = GetImapIncomingServer(getter_AddRefs(imapServer)); + NS_ENSURE_SUCCESS(rv, rv); + bool performingBiff = false; + nsCOMPtr<nsIMsgIncomingServer> incomingServer = + do_QueryInterface(imapServer, &rv); + NS_ENSURE_SUCCESS(rv, rv); + incomingServer->GetPerformingBiff(&performingBiff); + m_urlListener = aListener; + + // See if we should check all folders for new messages, or just the inbox + // and marked ones + bool checkAllFolders = ShouldCheckAllFolders(imapServer); + + // Get new messages for inbox + nsCOMPtr<nsIMsgFolder> inbox; + rv = rootFolder->GetFolderWithFlags(nsMsgFolderFlags::Inbox, + getter_AddRefs(inbox)); + if (inbox) { + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(inbox, &rv); + NS_ENSURE_SUCCESS(rv, rv); + imapFolder->SetPerformingBiff(performingBiff); + inbox->SetGettingNewMessages(true); + rv = inbox->UpdateFolder(aWindow); + } + // Get new messages for other folders if marked, or all of them if the pref + // is set + rv = imapServer->GetNewMessagesForNonInboxFolders( + rootFolder, aWindow, checkAllFolders, performingBiff); + } + return rv; +} + +NS_IMETHODIMP nsImapMailFolder::Shutdown(bool shutdownChildren) { + m_filterList = nullptr; + m_initialized = false; + // mPath is used to decide if folder pathname needs to be reconstructed in + // GetPath(). + mPath = nullptr; + m_moveCoalescer = nullptr; + m_msgParser = nullptr; + if (m_playbackTimer) { + m_playbackTimer->Cancel(); + m_playbackTimer = nullptr; + } + m_pendingOfflineMoves.Clear(); + return nsMsgDBFolder::Shutdown(shutdownChildren); +} + +nsresult nsImapMailFolder::GetBodysToDownload( + nsTArray<nsMsgKey>* keysOfMessagesToDownload) { + NS_ENSURE_ARG(keysOfMessagesToDownload); + NS_ENSURE_TRUE(mDatabase, NS_ERROR_FAILURE); + + nsCOMPtr<nsIMsgEnumerator> enumerator; + nsresult rv = mDatabase->EnumerateMessages(getter_AddRefs(enumerator)); + if (NS_SUCCEEDED(rv) && enumerator) { + bool hasMore; + nsCOMPtr<nsIMsgDBHdr> header; + nsMsgKey msgKey; + while (NS_SUCCEEDED(rv = enumerator->HasMoreElements(&hasMore)) && + hasMore) { + rv = enumerator->GetNext(getter_AddRefs(header)); + NS_ENSURE_SUCCESS(rv, rv); + bool shouldStoreMsgOffline = false; + header->GetMessageKey(&msgKey); + // MsgFitsDownloadCriteria ignores nsMsgFolderFlags::Offline, which we + // want + if (m_downloadingFolderForOfflineUse) + MsgFitsDownloadCriteria(msgKey, &shouldStoreMsgOffline); + else + ShouldStoreMsgOffline(msgKey, &shouldStoreMsgOffline); + if (shouldStoreMsgOffline) + keysOfMessagesToDownload->AppendElement(msgKey); + } + if (MOZ_LOG_TEST(gAutoSyncLog, mozilla::LogLevel::Debug) && header) { + // Log this only if folder is not empty. + uint32_t msgFlags = 0; + header->GetFlags(&msgFlags); + MOZ_LOG(gAutoSyncLog, mozilla::LogLevel::Debug, + ("%s: num keys to download=%zu, last key=%d, last msg flag=0x%x " + "nsMsgMessageFlags::Offline=0x%x", + __func__, keysOfMessagesToDownload->Length(), msgKey, msgFlags, + nsMsgMessageFlags::Offline)); + } + } + return rv; +} + +NS_IMETHODIMP nsImapMailFolder::OnNewIdleMessages() { + nsresult rv; + nsCOMPtr<nsIImapIncomingServer> imapServer; + rv = GetImapIncomingServer(getter_AddRefs(imapServer)); + NS_ENSURE_SUCCESS(rv, rv); + + bool checkAllFolders = ShouldCheckAllFolders(imapServer); + + // only trigger biff if we're checking all new folders for new messages, or + // this particular folder, but excluding trash,junk, sent, and no select + // folders, by default. + if ((checkAllFolders && + !(mFlags & + (nsMsgFolderFlags::Trash | nsMsgFolderFlags::Junk | + nsMsgFolderFlags::SentMail | nsMsgFolderFlags::ImapNoselect))) || + (mFlags & (nsMsgFolderFlags::CheckNew | nsMsgFolderFlags::Inbox))) + SetPerformingBiff(true); + return UpdateFolder(nullptr); +} + +NS_IMETHODIMP nsImapMailFolder::UpdateImapMailboxInfo( + nsIImapProtocol* aProtocol, nsIMailboxSpec* aSpec) { + nsresult rv; + ChangeNumPendingTotalMessages(-mNumPendingTotalMessages); + ChangeNumPendingUnread(-mNumPendingUnreadMessages); + m_numServerRecentMessages = 0; // clear this since we selected the folder. + + if (!mDatabase) GetDatabase(); + + bool folderSelected; + rv = aSpec->GetFolderSelected(&folderSelected); + NS_ENSURE_SUCCESS(rv, rv); + nsTArray<nsMsgKey> existingKeys; + nsTArray<nsMsgKey> keysToDelete; + uint32_t numNewUnread; + nsCOMPtr<nsIDBFolderInfo> dbFolderInfo; + int32_t imapUIDValidity = 0; + if (mDatabase) { + rv = mDatabase->GetDBFolderInfo(getter_AddRefs(dbFolderInfo)); + if (NS_SUCCEEDED(rv) && dbFolderInfo) { + dbFolderInfo->GetImapUidValidity(&imapUIDValidity); + uint64_t mailboxHighestModSeq; + aSpec->GetHighestModSeq(&mailboxHighestModSeq); + MOZ_LOG(IMAP_CS, mozilla::LogLevel::Debug, + ("UpdateImapMailboxInfo(): Store highest MODSEQ=%" PRIu64 + " for folder=%s", + mailboxHighestModSeq, m_onlineFolderName.get())); + char intStrBuf[40]; + PR_snprintf(intStrBuf, sizeof(intStrBuf), "%llu", mailboxHighestModSeq); + dbFolderInfo->SetCharProperty(kModSeqPropertyName, + nsDependentCString(intStrBuf)); + } + nsTArray<nsMsgKey> keys; + rv = mDatabase->ListAllKeys(keys); + NS_ENSURE_SUCCESS(rv, rv); + existingKeys.AppendElements(keys); + nsCOMPtr<nsIMsgOfflineOpsDatabase> opsDb = + do_QueryInterface(mDatabase, &rv); + NS_ENSURE_SUCCESS(rv, rv); + opsDb->ListAllOfflineDeletes(existingKeys); + } + int32_t folderValidity; + aSpec->GetFolder_UIDVALIDITY(&folderValidity); + nsCOMPtr<nsIImapFlagAndUidState> flagState; + aSpec->GetFlagState(getter_AddRefs(flagState)); + + // remember what the supported user flags are. + uint32_t supportedUserFlags; + aSpec->GetSupportedUserFlags(&supportedUserFlags); + SetSupportedUserFlags(supportedUserFlags); + + m_uidValidity = folderValidity; + + if (imapUIDValidity != folderValidity) { + NS_ASSERTION(imapUIDValidity == kUidUnknown, + "uid validity seems to have changed, blowing away db"); + nsCOMPtr<nsIFile> pathFile; + rv = GetFilePath(getter_AddRefs(pathFile)); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIMsgDBService> msgDBService = + do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIDBFolderInfo> transferInfo; + if (dbFolderInfo) + dbFolderInfo->GetTransferInfo(getter_AddRefs(transferInfo)); + + // A backup message database might have been created earlier, for example + // if the user requested a reindex. We'll use the earlier one if we can, + // otherwise we'll try to backup at this point. + nsresult rvbackup = OpenBackupMsgDatabase(); + if (mDatabase) { + dbFolderInfo = nullptr; + if (NS_FAILED(rvbackup)) { + CloseAndBackupFolderDB(EmptyCString()); + if (NS_FAILED(OpenBackupMsgDatabase()) && mBackupDatabase) { + mBackupDatabase->RemoveListener(this); + mBackupDatabase = nullptr; + } + } else + mDatabase->ForceClosed(); + } + mDatabase = nullptr; + + nsCOMPtr<nsIFile> summaryFile; + rv = GetSummaryFileLocation(pathFile, getter_AddRefs(summaryFile)); + // Remove summary file. + if (NS_SUCCEEDED(rv) && summaryFile) summaryFile->Remove(false); + + // Create a new summary file, update the folder message counts, and + // Close the summary file db. + rv = msgDBService->CreateNewDB(this, getter_AddRefs(mDatabase)); + + if (NS_FAILED(rv) && mDatabase) { + mDatabase->ForceClosed(); + mDatabase = nullptr; + } else if (NS_SUCCEEDED(rv) && mDatabase) { + if (transferInfo) SetDBTransferInfo(transferInfo); + + SummaryChanged(); + if (mDatabase) { + if (mAddListener) mDatabase->AddListener(this); + rv = mDatabase->GetDBFolderInfo(getter_AddRefs(dbFolderInfo)); + } + } + // store the new UIDVALIDITY value + + if (NS_SUCCEEDED(rv) && dbFolderInfo) { + dbFolderInfo->SetImapUidValidity(folderValidity); + // need to forget highest mod seq when uid validity rolls. + MOZ_LOG(IMAP_CS, mozilla::LogLevel::Debug, + ("UpdateImapMailboxInfo(): UIDVALIDITY changed, reset highest " + "MODSEQ and UID for folder=%s", + m_onlineFolderName.get())); + dbFolderInfo->SetCharProperty(kModSeqPropertyName, EmptyCString()); + dbFolderInfo->SetUint32Property(kHighestRecordedUIDPropertyName, 0); + } + // delete all my msgs, the keys are bogus now + // add every message in this folder + existingKeys.Clear(); + // keysToDelete.CopyArray(&existingKeys); + + if (flagState) { + nsTArray<nsMsgKey> no_existingKeys; + FindKeysToAdd(no_existingKeys, m_keysToFetch, numNewUnread, flagState); + } + if (NS_FAILED(rv)) pathFile->Remove(false); + + } else if (!flagState /*&& !NET_IsOffline() */) // if there are no messages + // on the server + keysToDelete = existingKeys.Clone(); + else /* if ( !NET_IsOffline()) */ + { + uint32_t boxFlags; + aSpec->GetBox_flags(&boxFlags); + // FindKeysToDelete and FindKeysToAdd require sorted lists + existingKeys.Sort(); + FindKeysToDelete(existingKeys, keysToDelete, flagState, boxFlags); + // if this is the result of an expunge then don't grab headers + if (!(boxFlags & kJustExpunged)) + FindKeysToAdd(existingKeys, m_keysToFetch, numNewUnread, flagState); + } + m_totalKeysToFetch = m_keysToFetch.Length(); + if (!keysToDelete.IsEmpty() && mDatabase) { + nsTArray<RefPtr<nsIMsgDBHdr>> hdrsToDelete; + MsgGetHeadersFromKeys(mDatabase, keysToDelete, hdrsToDelete); + // Notify nsIMsgFolderListeners of a mass delete, but only if we actually + // have headers + if (!hdrsToDelete.IsEmpty()) { + nsCOMPtr<nsIMsgFolderNotificationService> notifier( + do_GetService("@mozilla.org/messenger/msgnotificationservice;1")); + if (notifier) notifier->NotifyMsgsDeleted(hdrsToDelete); + } + DeleteStoreMessages(hdrsToDelete); + EnableNotifications(nsIMsgFolder::allMessageCountNotifications, false); + mDatabase->DeleteMessages(keysToDelete, nullptr); + EnableNotifications(nsIMsgFolder::allMessageCountNotifications, true); + } + int32_t numUnreadFromServer; + aSpec->GetNumUnseenMessages(&numUnreadFromServer); + + bool partialUIDFetch; + flagState->GetPartialUIDFetch(&partialUIDFetch); + + // For partial UID fetches (i.e., occurs when CONDSTORE in effect), we can + // only trust the numUnread from the server. However, even that will only be + // correct if a recent imap STATUS occurred as indicated by + // numUnreadFromServer greater than -1. + if (partialUIDFetch) numNewUnread = numUnreadFromServer; + + // If we are performing biff for this folder, tell the + // stand-alone biff about the new high water mark + if (m_performingBiff && numNewUnread && + static_cast<int32_t>(numNewUnread) != -1) { + // We must ensure that the server knows that we are performing biff. + // Otherwise the stand-alone biff won't fire. + nsCOMPtr<nsIMsgIncomingServer> server; + if (NS_SUCCEEDED(GetServer(getter_AddRefs(server))) && server) + server->SetPerformingBiff(true); + SetNumNewMessages(numNewUnread); + } + SyncFlags(flagState); + if (mDatabase && numUnreadFromServer > -1 && + (int32_t)(mNumUnreadMessages + m_keysToFetch.Length()) > + numUnreadFromServer) + mDatabase->SyncCounts(); + + if (!m_keysToFetch.IsEmpty() && aProtocol) + PrepareToAddHeadersToMailDB(aProtocol); + else { + bool gettingNewMessages; + GetGettingNewMessages(&gettingNewMessages); + if (gettingNewMessages) + ProgressStatusString(aProtocol, "imapNoNewMessages", nullptr); + SetPerformingBiff(false); + } + aSpec->GetNumMessages(&m_numServerTotalMessages); + if (numUnreadFromServer > -1) m_numServerUnseenMessages = numUnreadFromServer; + aSpec->GetNumRecentMessages(&m_numServerRecentMessages); + + // some servers don't return UIDNEXT on SELECT - don't crunch + // existing values in that case. + int32_t nextUID; + aSpec->GetNextUID(&nextUID); + if (nextUID != (int32_t)nsMsgKey_None) m_nextUID = nextUID; + + return rv; +} + +/** + * Called after successful imap STATUS response occurs. Have valid unseen value + * if folderstatus URL produced an imap STATUS. If a NOOP occurs instead (doing + * folderstatus from a connection SELECTed on the same folder) there is no + * UNSEEN returned by NOOP. + */ +NS_IMETHODIMP nsImapMailFolder::UpdateImapMailboxStatus( + nsIImapProtocol* aProtocol, nsIMailboxSpec* aSpec) { + NS_ENSURE_ARG_POINTER(aSpec); + int32_t numUnread, numTotal; + aSpec->GetNumUnseenMessages(&numUnread); + aSpec->GetNumMessages(&numTotal); + aSpec->GetNumRecentMessages(&m_numServerRecentMessages); + int32_t prevNextUID = m_nextUID; + aSpec->GetNextUID(&m_nextUID); + bool summaryChanged = false; + + // If m_numServerUnseenMessages is 0, it means + // this is the first time we've done a Status. + // In that case, we count all the previous pending unread messages we know + // about as unread messages. We may want to do similar things with total + // messages, but the total messages include deleted messages if the folder + // hasn't been expunged. + int32_t previousUnreadMessages = + (m_numServerUnseenMessages) + ? m_numServerUnseenMessages + : mNumPendingUnreadMessages + mNumUnreadMessages; + if (numUnread == -1) { + // A noop occurred so don't know server's UNSEEN number, keep using the + // previously known unread count. + MOZ_LOG(IMAP, mozilla::LogLevel::Debug, + ("%s: folder=%s, unread was -1, set numUnread to previousUnread=%d", + __func__, m_onlineFolderName.get(), previousUnreadMessages)); + numUnread = previousUnreadMessages; + } + if (numUnread != previousUnreadMessages || m_nextUID != prevNextUID) { + int32_t unreadDelta = + numUnread - (mNumPendingUnreadMessages + mNumUnreadMessages); + if (numUnread - previousUnreadMessages != unreadDelta) + NS_WARNING("unread count should match server count"); + ChangeNumPendingUnread(unreadDelta); + if (unreadDelta > 0 && + !(mFlags & (nsMsgFolderFlags::Trash | nsMsgFolderFlags::Junk))) { + SetHasNewMessages(true); + SetNumNewMessages(unreadDelta); + SetBiffState(nsMsgBiffState_NewMail); + } + summaryChanged = true; + } + SetPerformingBiff(false); + if (m_numServerUnseenMessages != numUnread || + m_numServerTotalMessages != numTotal) { + if (numUnread > m_numServerUnseenMessages || + m_numServerTotalMessages > numTotal) + NotifyHasPendingMsgs(); + summaryChanged = true; + m_numServerUnseenMessages = numUnread; + m_numServerTotalMessages = numTotal; + } + if (summaryChanged) SummaryChanged(); + + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::ParseMsgHdrs( + nsIImapProtocol* aProtocol, nsIImapHeaderXferInfo* aHdrXferInfo) { + NS_ENSURE_ARG_POINTER(aHdrXferInfo); + int32_t numHdrs; + nsCOMPtr<nsIImapHeaderInfo> headerInfo; + nsCOMPtr<nsIImapUrl> aImapUrl; + nsImapAction imapAction = nsIImapUrl::nsImapTest; // unused value. + if (!mDatabase) GetDatabase(); + + nsresult rv = aHdrXferInfo->GetNumHeaders(&numHdrs); + if (aProtocol) { + (void)aProtocol->GetRunningImapURL(getter_AddRefs(aImapUrl)); + if (aImapUrl) aImapUrl->GetImapAction(&imapAction); + } + for (uint32_t i = 0; NS_SUCCEEDED(rv) && (int32_t)i < numHdrs; i++) { + rv = aHdrXferInfo->GetHeader(i, getter_AddRefs(headerInfo)); + NS_ENSURE_SUCCESS(rv, rv); + if (!headerInfo) break; + int32_t msgSize; + nsMsgKey msgKey; + bool containsKey; + nsCString msgHdrs; + headerInfo->GetMsgSize(&msgSize); + headerInfo->GetMsgUid(&msgKey); + if (msgKey == nsMsgKey_None) // not a valid uid. + continue; + if (imapAction == nsIImapUrl::nsImapMsgPreview) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + headerInfo->GetMsgHdrs(msgHdrs); + // create an input stream based on the hdr string. + nsCOMPtr<nsIStringInputStream> inputStream = + do_CreateInstance("@mozilla.org/io/string-input-stream;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + inputStream->ShareData(msgHdrs.get(), msgHdrs.Length()); + GetMessageHeader(msgKey, getter_AddRefs(msgHdr)); + if (msgHdr) { + GetMsgPreviewTextFromStream(msgHdr, inputStream); + } + continue; + } + if (mDatabase && + NS_SUCCEEDED(mDatabase->ContainsKey(msgKey, &containsKey)) && + containsKey) { + NS_ERROR("downloading hdrs for hdr we already have"); + continue; + } + nsresult rv = SetupHeaderParseStream(msgSize, EmptyCString(), nullptr); + NS_ENSURE_SUCCESS(rv, rv); + headerInfo->GetMsgHdrs(msgHdrs); + rv = ParseAdoptedHeaderLine(msgHdrs.get(), msgKey); + NS_ENSURE_SUCCESS(rv, rv); + rv = NormalEndHeaderParseStream(aProtocol, aImapUrl); + } + return rv; +} + +nsresult nsImapMailFolder::SetupHeaderParseStream( + uint32_t aSize, const nsACString& content_type, nsIMailboxSpec* boxSpec) { + if (!mDatabase) GetDatabase(); + m_nextMessageByteLength = aSize; + if (!m_msgParser) { + nsresult rv; + m_msgParser = do_CreateInstance(kParseMailMsgStateCID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + } else + m_msgParser->Clear(); + + m_msgParser->SetMailDB(mDatabase); + if (mBackupDatabase) m_msgParser->SetBackupMailDB(mBackupDatabase); + return m_msgParser->SetState(nsIMsgParseMailMsgState::ParseHeadersState); +} + +nsresult nsImapMailFolder::ParseAdoptedHeaderLine(const char* aMessageLine, + nsMsgKey aMsgKey) { + // we can get blocks that contain more than one line, + // but they never contain partial lines + const char* str = aMessageLine; + m_curMsgUid = aMsgKey; + m_msgParser->SetNewKey(m_curMsgUid); + // m_envelope_pos, for local folders, + // is the msg key. Setting this will set the msg key for the new header. + + int32_t len = strlen(str); + char* currentEOL = PL_strstr(str, MSG_LINEBREAK); + const char* currentLine = str; + while (currentLine < (str + len)) { + if (currentEOL) { + m_msgParser->ParseAFolderLine( + currentLine, (currentEOL + MSG_LINEBREAK_LEN) - currentLine); + currentLine = currentEOL + MSG_LINEBREAK_LEN; + currentEOL = PL_strstr(currentLine, MSG_LINEBREAK); + } else { + m_msgParser->ParseAFolderLine(currentLine, PL_strlen(currentLine)); + currentLine = str + len + 1; + } + } + return NS_OK; +} + +nsresult nsImapMailFolder::NormalEndHeaderParseStream( + nsIImapProtocol* aProtocol, nsIImapUrl* imapUrl) { + nsCOMPtr<nsIMsgDBHdr> newMsgHdr; + nsresult rv; + NS_ENSURE_TRUE(m_msgParser, NS_ERROR_NULL_POINTER); + + nsMailboxParseState parseState; + m_msgParser->GetState(&parseState); + if (parseState == nsIMsgParseMailMsgState::ParseHeadersState) + m_msgParser->ParseAFolderLine(CRLF, 2); + rv = m_msgParser->GetNewMsgHdr(getter_AddRefs(newMsgHdr)); + NS_ENSURE_SUCCESS(rv, rv); + + char* headers; + int32_t headersSize; + + nsCOMPtr<nsIMsgWindow> msgWindow; + nsCOMPtr<nsIMsgMailNewsUrl> msgUrl; + if (imapUrl) { + msgUrl = do_QueryInterface(imapUrl, &rv); + NS_ENSURE_SUCCESS(rv, rv); + msgUrl->GetMsgWindow(getter_AddRefs(msgWindow)); + } + + nsCOMPtr<nsIMsgIncomingServer> server; + rv = GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIImapIncomingServer> imapServer = do_QueryInterface(server); + rv = imapServer->GetIsGMailServer(&m_isGmailServer); + NS_ENSURE_SUCCESS(rv, rv); + + newMsgHdr->SetMessageKey(m_curMsgUid); + TweakHeaderFlags(aProtocol, newMsgHdr); + uint32_t messageSize; + if (NS_SUCCEEDED(newMsgHdr->GetMessageSize(&messageSize))) + mFolderSize += messageSize; + m_msgMovedByFilter = false; + + nsMsgKey highestUID = 0; + nsCOMPtr<nsIDBFolderInfo> dbFolderInfo; + if (mDatabase) mDatabase->GetDBFolderInfo(getter_AddRefs(dbFolderInfo)); + if (dbFolderInfo) + dbFolderInfo->GetUint32Property(kHighestRecordedUIDPropertyName, 0, + &highestUID); + + // If this is the inbox, try to apply filters. Otherwise, test the inherited + // folder property "applyIncomingFilters" (which defaults to empty). If this + // inherited property has the string value "true", then apply filters even + // if this is not the Inbox folder. + if (mFlags & nsMsgFolderFlags::Inbox || m_applyIncomingFilters) { + // Use highwater to determine whether to filter? + bool filterOnHighwater = false; + nsCOMPtr<nsIPrefBranch> prefBranch( + do_GetService(NS_PREFSERVICE_CONTRACTID)); + if (prefBranch) + prefBranch->GetBoolPref("mail.imap.filter_on_new", &filterOnHighwater); + + uint32_t msgFlags; + newMsgHdr->GetFlags(&msgFlags); + + // clang-format off + bool doFilter = filterOnHighwater + // Filter on largest UUID and not deleted. + ? m_curMsgUid > highestUID && !(msgFlags & nsMsgMessageFlags::IMAPDeleted) + // Filter on unread and not deleted. + : !(msgFlags & (nsMsgMessageFlags::Read | nsMsgMessageFlags::IMAPDeleted)); + // clang-format on + + if (doFilter) + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Imap) New message parsed, and filters will be run on it")); + else + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Imap) New message parsed, but filters will not be run on it")); + + if (doFilter) { + int32_t duplicateAction = nsIMsgIncomingServer::keepDups; + if (server) server->GetIncomingDuplicateAction(&duplicateAction); + if ((duplicateAction != nsIMsgIncomingServer::keepDups) && + mFlags & nsMsgFolderFlags::Inbox) { + bool isDup; + server->IsNewHdrDuplicate(newMsgHdr, &isDup); + if (isDup) { + // we want to do something similar to applying filter hits. + // if a dup is marked read, it shouldn't trigger biff. + // Same for deleting it or moving it to trash. + switch (duplicateAction) { + case nsIMsgIncomingServer::deleteDups: { + uint32_t newFlags; + newMsgHdr->OrFlags( + nsMsgMessageFlags::Read | nsMsgMessageFlags::IMAPDeleted, + &newFlags); + StoreImapFlags(kImapMsgSeenFlag | kImapMsgDeletedFlag, true, + {m_curMsgUid}, nullptr); + m_msgMovedByFilter = true; + } break; + case nsIMsgIncomingServer::moveDupsToTrash: { + nsCOMPtr<nsIMsgFolder> trash; + GetTrashFolder(getter_AddRefs(trash)); + if (trash) { + nsCString trashUri; + trash->GetURI(trashUri); + nsresult err = MoveIncorporatedMessage( + newMsgHdr, mDatabase, trashUri, nullptr, msgWindow); + if (NS_SUCCEEDED(err)) m_msgMovedByFilter = true; + } + } break; + case nsIMsgIncomingServer::markDupsRead: { + uint32_t newFlags; + newMsgHdr->OrFlags(nsMsgMessageFlags::Read, &newFlags); + StoreImapFlags(kImapMsgSeenFlag, true, {m_curMsgUid}, nullptr); + } break; + } + int32_t numNewMessages; + GetNumNewMessages(false, &numNewMessages); + SetNumNewMessages(numNewMessages - 1); + } + } + rv = m_msgParser->GetAllHeaders(&headers, &headersSize); + + if (NS_SUCCEEDED(rv) && headers && !m_msgMovedByFilter && + !m_filterListRequiresBody) { + if (m_filterList) { + GetMoveCoalescer(); // not sure why we're doing this here. + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, + ("(Imap) ApplyFilterToHdr from " + "nsImapMailFolder::NormalEndHeaderParseStream()")); + m_filterList->ApplyFiltersToHdr( + nsMsgFilterType::InboxRule, newMsgHdr, this, mDatabase, + nsDependentCSubstring(headers, headersSize), this, msgWindow); + NotifyFolderEvent(kFiltersApplied); + } + } + } + } + // here we need to tweak flags from uid state.. + if (mDatabase && (!m_msgMovedByFilter || ShowDeletedMessages())) { + nsCOMPtr<nsIMsgFolderNotificationService> notifier( + do_GetService("@mozilla.org/messenger/msgnotificationservice;1")); + // 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; + newMsgHdr->GetMessageId(getter_Copies(newMessageId)); + nsMsgKey pseudoKey = + m_pseudoHdrs.MaybeGet(newMessageId).valueOr(nsMsgKey_None); + if (notifier && pseudoKey != nsMsgKey_None) { + notifier->NotifyMsgKeyChanged(pseudoKey, newMsgHdr); + m_pseudoHdrs.Remove(newMessageId); + } + mDatabase->AddNewHdrToDB(newMsgHdr, true); + if (notifier) notifier->NotifyMsgAdded(newMsgHdr); + // mark the header as not yet reported classified + OrProcessingFlags(m_curMsgUid, nsMsgProcessingFlags::NotReportedClassified); + } + // adjust highestRecordedUID + if (dbFolderInfo) { + if (m_curMsgUid > highestUID) { + MOZ_LOG(IMAP_CS, mozilla::LogLevel::Debug, + ("NormalEndHeaderParseStream(): Store new highest UID=%" PRIu32 + " for folder=%s", + m_curMsgUid, m_onlineFolderName.get())); + dbFolderInfo->SetUint32Property(kHighestRecordedUIDPropertyName, + m_curMsgUid); + } + } + + if (m_isGmailServer) { + nsCOMPtr<nsIImapFlagAndUidState> flagState; + aProtocol->GetFlagAndUidState(getter_AddRefs(flagState)); + nsCString msgIDValue; + nsCString threadIDValue; + nsCString labelsValue; + flagState->GetCustomAttribute(m_curMsgUid, "X-GM-MSGID"_ns, msgIDValue); + flagState->GetCustomAttribute(m_curMsgUid, "X-GM-THRID"_ns, threadIDValue); + flagState->GetCustomAttribute(m_curMsgUid, "X-GM-LABELS"_ns, labelsValue); + newMsgHdr->SetStringProperty("X-GM-MSGID", msgIDValue); + newMsgHdr->SetStringProperty("X-GM-THRID", threadIDValue); + newMsgHdr->SetStringProperty("X-GM-LABELS", labelsValue); + } + + m_msgParser->Clear(); // clear out parser, because it holds onto a msg hdr. + m_msgParser->SetMailDB(nullptr); // tell it to let go of the db too. + // I don't think we want to do this - it does bad things like set the size + // incorrectly. + // m_msgParser->FinishHeader(); + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::AbortHeaderParseStream( + nsIImapProtocol* aProtocol) { + nsresult rv = NS_ERROR_FAILURE; + return rv; +} + +NS_IMETHODIMP nsImapMailFolder::BeginCopy() { + NS_ENSURE_TRUE(m_copyState, NS_ERROR_NULL_POINTER); + nsresult rv; + if (m_copyState->m_tmpFile) // leftover file spec nuke it + { + rv = m_copyState->m_tmpFile->Remove(false); + if (NS_FAILED(rv)) { + nsCString nativePath = m_copyState->m_tmpFile->HumanReadablePath(); + MOZ_LOG(IMAP, mozilla::LogLevel::Info, + ("couldn't remove prev temp file %s: %" PRIx32, nativePath.get(), + static_cast<uint32_t>(rv))); + } + m_copyState->m_tmpFile = nullptr; + } + + rv = NS_OpenAnonymousTemporaryNsIFile(getter_AddRefs(m_copyState->m_tmpFile)); + if (NS_FAILED(rv)) { + MOZ_LOG(IMAP, mozilla::LogLevel::Info, + ("Couldn't create temp file: %" PRIx32, static_cast<uint32_t>(rv))); + OnCopyCompleted(m_copyState->m_srcSupport, rv); + } + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIOutputStream> fileOutputStream; + rv = MsgNewBufferedFileOutputStream( + getter_AddRefs(m_copyState->m_msgFileStream), m_copyState->m_tmpFile, -1, + 00600); + if (NS_FAILED(rv)) + MOZ_LOG(IMAP, mozilla::LogLevel::Info, + ("couldn't create output file stream: %" PRIx32, + static_cast<uint32_t>(rv))); + + if (!m_copyState->m_dataBuffer) + m_copyState->m_dataBuffer = (char*)PR_CALLOC(COPY_BUFFER_SIZE + 1); + NS_ENSURE_TRUE(m_copyState->m_dataBuffer, NS_ERROR_OUT_OF_MEMORY); + m_copyState->m_dataBufferSize = COPY_BUFFER_SIZE; + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::CopyDataToOutputStreamForAppend( + nsIInputStream* aIStream, int32_t aLength, nsIOutputStream* outputStream) { + uint32_t readCount; + uint32_t writeCount; + if (!m_copyState) m_copyState = new nsImapMailCopyState(); + + if (aLength + m_copyState->m_leftOver > m_copyState->m_dataBufferSize) { + char* newBuffer = (char*)PR_REALLOC(m_copyState->m_dataBuffer, + aLength + m_copyState->m_leftOver + 1); + NS_ENSURE_TRUE(newBuffer, NS_ERROR_OUT_OF_MEMORY); + m_copyState->m_dataBuffer = newBuffer; + m_copyState->m_dataBufferSize = aLength + m_copyState->m_leftOver; + } + + char *start, *end; + uint32_t linebreak_len = 1; + + nsresult rv = aIStream->Read( + m_copyState->m_dataBuffer + m_copyState->m_leftOver, aLength, &readCount); + if (NS_FAILED(rv)) return rv; + + m_copyState->m_leftOver += readCount; + m_copyState->m_dataBuffer[m_copyState->m_leftOver] = '\0'; + + start = m_copyState->m_dataBuffer; + if (m_copyState->m_eatLF) { + if (*start == '\n') start++; + m_copyState->m_eatLF = false; + } + end = PL_strpbrk(start, "\r\n"); + if (end && *end == '\r' && *(end + 1) == '\n') linebreak_len = 2; + + while (start && end) { + if (PL_strncasecmp(start, "X-Mozilla-Status:", 17) && + PL_strncasecmp(start, "X-Mozilla-Status2:", 18) && + PL_strncmp(start, "From - ", 7)) { + rv = outputStream->Write(start, end - start, &writeCount); + rv = outputStream->Write(CRLF, 2, &writeCount); + } + start = end + linebreak_len; + if (start >= m_copyState->m_dataBuffer + m_copyState->m_leftOver) { + m_copyState->m_leftOver = 0; + break; + } + linebreak_len = 1; + + end = PL_strpbrk(start, "\r\n"); + if (end && *end == '\r') { + if (*(end + 1) == '\n') + linebreak_len = 2; + else if (!*(end + 1)) // block might have split CRLF so remember if + m_copyState->m_eatLF = true; // we should eat LF + } + + if (start && !end) { + m_copyState->m_leftOver -= (start - m_copyState->m_dataBuffer); + memcpy(m_copyState->m_dataBuffer, start, + m_copyState->m_leftOver + 1); // including null + } + } + return rv; +} + +NS_IMETHODIMP nsImapMailFolder::CopyDataDone() { + m_copyState = nullptr; + return NS_OK; +} + +// sICopyMessageListener methods, BeginCopy, CopyData, EndCopy, EndMove, +// StartMessage, EndMessage +NS_IMETHODIMP nsImapMailFolder::CopyData(nsIInputStream* aIStream, + int32_t aLength) { + NS_ENSURE_TRUE( + m_copyState && m_copyState->m_msgFileStream && m_copyState->m_dataBuffer, + NS_ERROR_NULL_POINTER); + nsresult rv = CopyDataToOutputStreamForAppend(aIStream, aLength, + m_copyState->m_msgFileStream); + if (NS_FAILED(rv)) { + MOZ_LOG(IMAP, mozilla::LogLevel::Info, + ("CopyData failed: %" PRIx32, static_cast<uint32_t>(rv))); + OnCopyCompleted(m_copyState->m_srcSupport, rv); + } + return rv; +} + +NS_IMETHODIMP nsImapMailFolder::EndCopy(bool copySucceeded) { + nsresult rv = copySucceeded ? NS_OK : NS_ERROR_FAILURE; + if (copySucceeded && m_copyState && m_copyState->m_msgFileStream) { + nsCOMPtr<nsIUrlListener> urlListener; + m_copyState->m_msgFileStream->Close(); + // m_tmpFile can be stale because we wrote to it + nsCOMPtr<nsIFile> tmpFile; + m_copyState->m_tmpFile->Clone(getter_AddRefs(tmpFile)); + m_copyState->m_tmpFile = tmpFile; + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = + QueryInterface(NS_GET_IID(nsIUrlListener), getter_AddRefs(urlListener)); + rv = imapService->AppendMessageFromFile( + m_copyState->m_tmpFile, this, EmptyCString(), true, + m_copyState->m_selectedState, urlListener, m_copyState, + m_copyState->m_msgWindow); + } + if (NS_FAILED(rv) || !copySucceeded) + MOZ_LOG(IMAP, mozilla::LogLevel::Info, + ("EndCopy failed: %" PRIx32, static_cast<uint32_t>(rv))); + return rv; +} + +NS_IMETHODIMP nsImapMailFolder::EndMove(bool moveSucceeded) { return NS_OK; } +// this is the beginning of the next message copied +NS_IMETHODIMP nsImapMailFolder::StartMessage() { return NS_OK; } + +// just finished the current message. +NS_IMETHODIMP nsImapMailFolder::EndMessage(nsMsgKey key) { return NS_OK; } + +NS_IMETHODIMP nsImapMailFolder::ApplyFilterHit(nsIMsgFilter* filter, + nsIMsgWindow* msgWindow, + bool* applyMore) { + // + // This routine is called indirectly from ApplyFiltersToHdr in two + // circumstances, controlled by m_filterListRequiresBody: + // + // If false, after headers are parsed in NormalEndHeaderParseStream. + // If true, after the message body is downloaded in NormalEndMsgWriteStream. + // + // In NormalEndHeaderParseStream, the message has not been added to the + // database, and it is important that database notifications and count + // updates do not occur. In NormalEndMsgWriteStream, the message has been + // added to the database, and database notifications and count updates + // should be performed. + // + + NS_ENSURE_ARG_POINTER(filter); + NS_ENSURE_ARG_POINTER(applyMore); + + nsresult rv = NS_OK; + + nsCOMPtr<nsIMsgDBHdr> msgHdr; + if (m_filterListRequiresBody) + GetMessageHeader(m_curMsgUid, getter_AddRefs(msgHdr)); + else if (m_msgParser) + m_msgParser->GetNewMsgHdr(getter_AddRefs(msgHdr)); + NS_ENSURE_TRUE(msgHdr, + NS_ERROR_NULL_POINTER); // fatal error, cannot apply filters + + bool deleteToTrash = DeleteIsMoveToTrash(); + + nsTArray<RefPtr<nsIMsgRuleAction>> filterActionList; + rv = filter->GetSortedActionList(filterActionList); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t numActions = filterActionList.Length(); + + nsCString msgId; + msgHdr->GetMessageId(getter_Copies(msgId)); + nsMsgKey msgKey; + msgHdr->GetMessageKey(&msgKey); + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Imap) Applying %" PRIu32 + " filter actions on message with key %" PRIu32, + numActions, msgKeyToInt(msgKey))); + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, + ("(Imap) Message ID: %s", msgId.get())); + + bool loggingEnabled = false; + if (m_filterList && numActions) + (void)m_filterList->GetLoggingEnabled(&loggingEnabled); + + bool msgIsNew = true; + + rv = GetDatabase(); + NS_ENSURE_SUCCESS(rv, rv); + + nsresult finalResult = NS_OK; // result of all actions + for (uint32_t actionIndex = 0; actionIndex < numActions; actionIndex++) { + nsCOMPtr<nsIMsgRuleAction> filterAction(filterActionList[actionIndex]); + if (!filterAction) { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Warning, + ("(Imap) Filter action at index %" PRIu32 " invalid, skipping", + actionIndex)); + continue; + } + + rv = NS_OK; // result of the current action + nsMsgRuleActionType actionType; + if (NS_SUCCEEDED(filterAction->GetType(&actionType))) { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Imap) Running filter action at index %" PRIu32 + ", action type = %i", + actionIndex, actionType)); + if (loggingEnabled) (void)filter->LogRuleHit(filterAction, msgHdr); + + nsCString actionTargetFolderUri; + if (actionType == nsMsgFilterAction::MoveToFolder || + actionType == nsMsgFilterAction::CopyToFolder) { + rv = filterAction->GetTargetFolderUri(actionTargetFolderUri); + if (NS_FAILED(rv) || actionTargetFolderUri.IsEmpty()) { + // clang-format off + MOZ_LOG(FILTERLOGMODULE, LogLevel::Warning, + ("(Imap) Target URI for Copy/Move action is empty, skipping")); + // clang-format on + NS_ASSERTION(false, "actionTargetFolderUri is empty"); + continue; + } + } + + uint32_t msgFlags; + msgHdr->GetFlags(&msgFlags); + bool isRead = (msgFlags & nsMsgMessageFlags::Read); + + switch (actionType) { + case nsMsgFilterAction::Delete: { + if (deleteToTrash) { + // set value to trash folder + nsCOMPtr<nsIMsgFolder> mailTrash; + rv = GetTrashFolder(getter_AddRefs(mailTrash)); + if (NS_SUCCEEDED(rv) && mailTrash) { + rv = mailTrash->GetURI(actionTargetFolderUri); + if (NS_FAILED(rv)) break; + } + // msgHdr->OrFlags(nsMsgMessageFlags::Read, &newFlags); // mark + // read in trash. + } else { + mDatabase->MarkHdrRead(msgHdr, true, nullptr); + mDatabase->MarkImapDeleted(msgKey, true, nullptr); + rv = StoreImapFlags(kImapMsgSeenFlag | kImapMsgDeletedFlag, true, + {msgKey}, nullptr); + if (NS_FAILED(rv)) break; + // this will prevent us from adding the header to the db. + m_msgMovedByFilter = true; + } + msgIsNew = false; + } + // note that delete falls through to move. + [[fallthrough]]; + case nsMsgFilterAction::MoveToFolder: { + // if moving to a different file, do it. + nsCString uri; + rv = GetURI(uri); + if (NS_FAILED(rv)) break; + + if (!actionTargetFolderUri.Equals(uri)) { + msgHdr->GetFlags(&msgFlags); + if (msgFlags & nsMsgMessageFlags::MDNReportNeeded && !isRead) { + mDatabase->MarkMDNNeeded(msgKey, false, nullptr); + mDatabase->MarkMDNSent(msgKey, true, nullptr); + } + nsresult rv = MoveIncorporatedMessage( + msgHdr, mDatabase, actionTargetFolderUri, filter, msgWindow); + if (NS_SUCCEEDED(rv)) { + m_msgMovedByFilter = true; + } else { + if (loggingEnabled) { + (void)filter->LogRuleHitFail(filterAction, msgHdr, rv, + "filterFailureMoveFailed"_ns); + } + } + } + // don't apply any more filters, even if it was a move to the same + // folder + *applyMore = false; + } break; + case nsMsgFilterAction::CopyToFolder: { + nsCString uri; + rv = GetURI(uri); + if (NS_FAILED(rv)) break; + + if (!actionTargetFolderUri.Equals(uri)) { + // XXXshaver I'm not actually 100% what the right semantics are for + // MDNs and copied messages, but I suspect deep down inside that + // we probably want to suppress them only on the copies. + msgHdr->GetFlags(&msgFlags); + if (msgFlags & nsMsgMessageFlags::MDNReportNeeded && !isRead) { + mDatabase->MarkMDNNeeded(msgKey, false, nullptr); + mDatabase->MarkMDNSent(msgKey, true, nullptr); + } + + nsCOMPtr<nsIMsgFolder> dstFolder; + rv = GetExistingFolder(actionTargetFolderUri, + getter_AddRefs(dstFolder)); + if (NS_FAILED(rv)) break; + + nsCOMPtr<nsIMsgCopyService> copyService = do_GetService( + "@mozilla.org/messenger/messagecopyservice;1", &rv); + if (NS_FAILED(rv)) break; + rv = copyService->CopyMessages(this, {&*msgHdr}, dstFolder, false, + nullptr, msgWindow, false); + if (NS_FAILED(rv)) { + if (loggingEnabled) { + (void)filter->LogRuleHitFail(filterAction, msgHdr, rv, + "filterFailureCopyFailed"_ns); + } + } + } + } break; + case nsMsgFilterAction::MarkRead: { + mDatabase->MarkHdrRead(msgHdr, true, nullptr); + rv = StoreImapFlags(kImapMsgSeenFlag, true, {msgKey}, nullptr); + msgIsNew = false; + } break; + case nsMsgFilterAction::MarkUnread: { + mDatabase->MarkHdrRead(msgHdr, false, nullptr); + rv = StoreImapFlags(kImapMsgSeenFlag, false, {msgKey}, nullptr); + msgIsNew = true; + } break; + case nsMsgFilterAction::MarkFlagged: { + mDatabase->MarkHdrMarked(msgHdr, true, nullptr); + rv = StoreImapFlags(kImapMsgFlaggedFlag, true, {msgKey}, nullptr); + } break; + case nsMsgFilterAction::KillThread: + case nsMsgFilterAction::WatchThread: { + nsCOMPtr<nsIMsgThread> msgThread; + nsMsgKey threadKey; + mDatabase->GetThreadContainingMsgHdr(msgHdr, + getter_AddRefs(msgThread)); + if (msgThread) { + msgThread->GetThreadKey(&threadKey); + if (actionType == nsMsgFilterAction::KillThread) + rv = mDatabase->MarkThreadIgnored(msgThread, threadKey, true, + nullptr); + else + rv = mDatabase->MarkThreadWatched(msgThread, threadKey, true, + nullptr); + } else { + if (actionType == nsMsgFilterAction::KillThread) + rv = msgHdr->SetUint32Property("ProtoThreadFlags", + nsMsgMessageFlags::Ignored); + else + rv = msgHdr->SetUint32Property("ProtoThreadFlags", + nsMsgMessageFlags::Watched); + } + if (actionType == nsMsgFilterAction::KillThread) { + mDatabase->MarkHdrRead(msgHdr, true, nullptr); + rv = StoreImapFlags(kImapMsgSeenFlag, true, {msgKey}, nullptr); + msgIsNew = false; + } + } break; + case nsMsgFilterAction::KillSubthread: { + mDatabase->MarkHeaderKilled(msgHdr, true, nullptr); + mDatabase->MarkHdrRead(msgHdr, true, nullptr); + rv = StoreImapFlags(kImapMsgSeenFlag, true, {msgKey}, nullptr); + msgIsNew = false; + } break; + case nsMsgFilterAction::ChangePriority: { + nsMsgPriorityValue filterPriority; // a int32_t + filterAction->GetPriority(&filterPriority); + rv = mDatabase->SetUint32PropertyByHdr( + msgHdr, "priority", static_cast<uint32_t>(filterPriority)); + } break; + case nsMsgFilterAction::AddTag: { + nsCString keyword; + filterAction->GetStrValue(keyword); + rv = AddKeywordsToMessages({&*msgHdr}, keyword); + } break; + case nsMsgFilterAction::JunkScore: { + nsAutoCString junkScoreStr; + int32_t junkScore; + filterAction->GetJunkScore(&junkScore); + junkScoreStr.AppendInt(junkScore); + rv = mDatabase->SetStringProperty(msgKey, "junkscore", junkScoreStr); + mDatabase->SetStringProperty(msgKey, "junkscoreorigin", "filter"_ns); + + // If score is available, set up to store junk status on server. + if (junkScore == nsIJunkMailPlugin::IS_SPAM_SCORE || + junkScore == nsIJunkMailPlugin::IS_HAM_SCORE) { + nsTArray<nsMsgKey>* keysToClassify = m_moveCoalescer->GetKeyBucket( + (junkScore == nsIJunkMailPlugin::IS_SPAM_SCORE) ? 0 : 1); + NS_ASSERTION(keysToClassify, "error getting key bucket"); + if (keysToClassify) keysToClassify->AppendElement(msgKey); + if (msgIsNew && junkScore == nsIJunkMailPlugin::IS_SPAM_SCORE) { + msgIsNew = false; + mDatabase->MarkHdrNotNew(msgHdr, nullptr); + // nsMsgDBFolder::SendFlagNotifications by the call to + // SetBiffState(nsMsgBiffState_NoMail) will reset numNewMessages + // only if the message is also read and database notifications + // are active, but we are not going to mark it read in this + // action, preferring to leave the choice to the user. + // So correct numNewMessages. + if (m_filterListRequiresBody) { + msgHdr->GetFlags(&msgFlags); + if (!(msgFlags & nsMsgMessageFlags::Read)) { + int32_t numNewMessages; + GetNumNewMessages(false, &numNewMessages); + SetNumNewMessages(--numNewMessages); + SetHasNewMessages(numNewMessages != 0); + } + } + } + } + } break; + case nsMsgFilterAction::Forward: { + nsCString forwardTo; + filterAction->GetStrValue(forwardTo); + nsCOMPtr<nsIMsgIncomingServer> server; + rv = GetServer(getter_AddRefs(server)); + if (NS_FAILED(rv)) break; + if (!forwardTo.IsEmpty()) { + nsCOMPtr<nsIMsgComposeService> compService = + do_GetService("@mozilla.org/messengercompose;1", &rv); + if (NS_FAILED(rv)) break; + rv = compService->ForwardMessage( + NS_ConvertUTF8toUTF16(forwardTo), msgHdr, msgWindow, server, + nsIMsgComposeService::kForwardAsDefault); + } + } break; + + case nsMsgFilterAction::Reply: { + nsCString replyTemplateUri; + filterAction->GetStrValue(replyTemplateUri); + nsCOMPtr<nsIMsgIncomingServer> server; + rv = GetServer(getter_AddRefs(server)); + if (NS_FAILED(rv)) break; + if (!replyTemplateUri.IsEmpty()) { + nsCOMPtr<nsIMsgComposeService> compService = + do_GetService("@mozilla.org/messengercompose;1", &rv); + if (NS_SUCCEEDED(rv) && compService) { + rv = compService->ReplyWithTemplate(msgHdr, replyTemplateUri, + msgWindow, server); + if (NS_FAILED(rv)) { + NS_WARNING("ReplyWithTemplate failed"); + if (rv == NS_ERROR_ABORT) { + (void)filter->LogRuleHitFail( + filterAction, msgHdr, rv, + "filterFailureSendingReplyAborted"_ns); + } else { + (void)filter->LogRuleHitFail( + filterAction, msgHdr, rv, + "filterFailureSendingReplyError"_ns); + } + } + } + } + } break; + + case nsMsgFilterAction::StopExecution: { + // don't apply any more filters + *applyMore = false; + rv = NS_OK; + } break; + + case nsMsgFilterAction::Custom: { + nsCOMPtr<nsIMsgFilterCustomAction> customAction; + rv = filterAction->GetCustomAction(getter_AddRefs(customAction)); + if (NS_FAILED(rv)) break; + + nsAutoCString value; + rv = filterAction->GetStrValue(value); + if (NS_FAILED(rv)) break; + + rv = customAction->ApplyAction({&*msgHdr}, value, nullptr, + nsMsgFilterType::InboxRule, msgWindow); + // allow custom action to affect new + msgHdr->GetFlags(&msgFlags); + if (!(msgFlags & nsMsgMessageFlags::New)) msgIsNew = false; + } break; + + default: + NS_ERROR("unexpected filter action"); + rv = NS_ERROR_UNEXPECTED; + break; + } + } + if (NS_FAILED(rv)) { + finalResult = rv; + MOZ_LOG(FILTERLOGMODULE, LogLevel::Error, + ("(Imap) Action execution failed with error: %" PRIx32, + static_cast<uint32_t>(rv))); + if (loggingEnabled) { + (void)filter->LogRuleHitFail(filterAction, msgHdr, rv, + "filterFailureAction"_ns); + } + } else { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Imap) Action execution succeeded")); + } + } + if (!msgIsNew) { + int32_t numNewMessages; + GetNumNewMessages(false, &numNewMessages); + // When database notifications are active, new counts will be reset + // to zero in nsMsgDBFolder::SendFlagNotifications by the call to + // SetBiffState(nsMsgBiffState_NoMail), so don't repeat them here. + if (!m_filterListRequiresBody) SetNumNewMessages(--numNewMessages); + if (mDatabase) mDatabase->MarkHdrNotNew(msgHdr, nullptr); + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Imap) Message will not be marked new")); + } + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Imap) Finished executing actions")); + return finalResult; +} + +NS_IMETHODIMP nsImapMailFolder::SetImapFlags(const char* uids, int32_t flags, + nsIURI** url) { + nsresult rv; + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + return imapService->SetMessageFlags(this, this, url, nsAutoCString(uids), + flags, true); +} + +// "this" is the parent folder +NS_IMETHODIMP nsImapMailFolder::PlaybackOfflineFolderCreate( + const nsAString& aFolderName, nsIMsgWindow* aWindow, nsIURI** url) { + nsresult rv; + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + return imapService->CreateFolder(this, aFolderName, this, url); +} + +NS_IMETHODIMP +nsImapMailFolder::ReplayOfflineMoveCopy(const nsTArray<nsMsgKey>& aMsgKeys, + bool isMove, nsIMsgFolder* aDstFolder, + nsIUrlListener* aUrlListener, + nsIMsgWindow* aWindow, + bool srcFolderOffline) { + nsresult rv; + + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(aDstFolder); + if (imapFolder) { + nsImapMailFolder* destImapFolder = + static_cast<nsImapMailFolder*>(aDstFolder); + nsCOMPtr<nsIMsgDatabase> dstFolderDB; + aDstFolder->GetMsgDatabase(getter_AddRefs(dstFolderDB)); + nsCOMPtr<nsIMsgOfflineOpsDatabase> opsDb = + do_QueryInterface(dstFolderDB, &rv); + NS_ENSURE_SUCCESS(rv, rv); + if (opsDb) { + // find the fake header in the destination db, and use that to + // set the pending attributes on the real headers. To do this, + // we need to iterate over the offline ops in the destination db, + // looking for ones with matching keys and source folder uri. + // If we find that offline op, its "key" will be the key of the fake + // header, so we just need to get the header for that key + // from the dest db. + nsTArray<nsMsgKey> offlineOps; + if (NS_SUCCEEDED(opsDb->ListAllOfflineOpIds(offlineOps))) { + nsTArray<RefPtr<nsIMsgDBHdr>> messages; + nsCString srcFolderUri; + GetURI(srcFolderUri); + nsCOMPtr<nsIMsgOfflineImapOperation> currentOp; + for (uint32_t opIndex = 0; opIndex < offlineOps.Length(); opIndex++) { + opsDb->GetOfflineOpForKey(offlineOps[opIndex], false, + getter_AddRefs(currentOp)); + if (currentOp) { + nsCString opSrcUri; + currentOp->GetSourceFolderURI(opSrcUri); + if (opSrcUri.Equals(srcFolderUri)) { + nsMsgKey srcMessageKey; + currentOp->GetSrcMessageKey(&srcMessageKey); + for (auto key : aMsgKeys) { + if (srcMessageKey == key) { + nsCOMPtr<nsIMsgDBHdr> fakeDestHdr; + dstFolderDB->GetMsgHdrForKey(offlineOps[opIndex], + getter_AddRefs(fakeDestHdr)); + if (fakeDestHdr) messages.AppendElement(fakeDestHdr); + break; + } + } + } + } + } + // 3rd parameter: Sets offline flag. + destImapFolder->SetPendingAttributes(messages, isMove, + srcFolderOffline); + } + } + // if we can't get the dst folder db, we should still try to playback + // the offline move/copy. + } + + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIURI> resultUrl; + nsAutoCString uids; + AllocateUidStringFromKeys(aMsgKeys, uids); + rv = imapService->OnlineMessageCopy(this, uids, aDstFolder, true, isMove, + aUrlListener, getter_AddRefs(resultUrl), + nullptr, aWindow); + if (resultUrl) { + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(resultUrl, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIUrlListener> folderListener = do_QueryInterface(aDstFolder); + if (folderListener) mailnewsUrl->RegisterListener(folderListener); + } + return rv; +} + +NS_IMETHODIMP nsImapMailFolder::AddMoveResultPseudoKey(nsMsgKey aMsgKey) { + nsresult rv = GetDatabase(); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgDBHdr> pseudoHdr; + rv = mDatabase->GetMsgHdrForKey(aMsgKey, getter_AddRefs(pseudoHdr)); + NS_ENSURE_SUCCESS(rv, rv); + 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; + m_pseudoHdrs.InsertOrUpdate(messageId, aMsgKey); + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::StoreImapFlags(int32_t flags, bool addFlags, + const nsTArray<nsMsgKey>& keys, + nsIUrlListener* aUrlListener) { + nsresult rv; + if (!WeAreOffline()) { + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsAutoCString msgIds; + AllocateUidStringFromKeys(keys, msgIds); + if (addFlags) + imapService->AddMessageFlags(this, aUrlListener ? aUrlListener : this, + msgIds, flags, true); + else + imapService->SubtractMessageFlags( + this, aUrlListener ? aUrlListener : this, msgIds, flags, true); + } else { + rv = GetDatabase(); + if (NS_SUCCEEDED(rv) && mDatabase) { + nsCOMPtr<nsIMsgOfflineOpsDatabase> opsDb = + do_QueryInterface(mDatabase, &rv); + NS_ENSURE_SUCCESS(rv, rv); + for (auto key : keys) { + nsCOMPtr<nsIMsgOfflineImapOperation> op; + rv = opsDb->GetOfflineOpForKey(key, true, getter_AddRefs(op)); + SetFlag(nsMsgFolderFlags::OfflineEvents); + if (NS_SUCCEEDED(rv) && op) { + imapMessageFlagsType newFlags; + op->GetNewFlags(&newFlags); + op->SetFlagOperation(addFlags ? newFlags | flags : newFlags & ~flags); + } + } + opsDb->Commit(nsMsgDBCommitType::kLargeCommit); // flush offline flags + } + } + return rv; +} + +NS_IMETHODIMP nsImapMailFolder::LiteSelect(nsIUrlListener* aUrlListener, + nsIMsgWindow* aMsgWindow) { + nsresult rv; + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIURI> outUri; + return imapService->LiteSelectFolder(this, aUrlListener, aMsgWindow, + getter_AddRefs(outUri)); +} + +nsresult nsImapMailFolder::GetFolderOwnerUserName(nsACString& userName) { + if ((mFlags & nsMsgFolderFlags::ImapPersonal) || + !(mFlags & + (nsMsgFolderFlags::ImapPublic | nsMsgFolderFlags::ImapOtherUser))) { + // this is one of our personal mail folders + // return our username on this host + nsCOMPtr<nsIMsgIncomingServer> server; + nsresult rv = GetServer(getter_AddRefs(server)); + return NS_FAILED(rv) ? rv : server->GetUsername(userName); + } + + // the only other type of owner is if it's in the other users' namespace + if (!(mFlags & nsMsgFolderFlags::ImapOtherUser)) return NS_OK; + + if (m_ownerUserName.IsEmpty()) { + nsCString onlineName; + GetOnlineName(onlineName); + m_ownerUserName = nsImapNamespaceList::GetFolderOwnerNameFromPath( + GetNamespaceForFolder(), onlineName.get()); + } + userName = m_ownerUserName; + return NS_OK; +} + +nsImapNamespace* nsImapMailFolder::GetNamespaceForFolder() { + if (!m_namespace) { +#ifdef DEBUG_bienvenu + // Make sure this isn't causing us to open the database + NS_ASSERTION(m_hierarchyDelimiter != kOnlineHierarchySeparatorUnknown, + "haven't set hierarchy delimiter"); +#endif + nsCString serverKey; + nsCString onlineName; + GetServerKey(serverKey); + GetOnlineName(onlineName); + char hierarchyDelimiter; + GetHierarchyDelimiter(&hierarchyDelimiter); + + m_namespace = nsImapNamespaceList::GetNamespaceForFolder( + serverKey.get(), onlineName.get(), hierarchyDelimiter); + NS_ASSERTION(m_namespace, "didn't get namespace for folder"); + if (m_namespace) { + nsImapNamespaceList::SuggestHierarchySeparatorForNamespace( + m_namespace, hierarchyDelimiter); + m_folderIsNamespace = nsImapNamespaceList::GetFolderIsNamespace( + serverKey.get(), onlineName.get(), hierarchyDelimiter, m_namespace); + } + } + return m_namespace; +} + +void nsImapMailFolder::SetNamespaceForFolder(nsImapNamespace* ns) { +#ifdef DEBUG_bienvenu + NS_ASSERTION(ns, "null namespace"); +#endif + m_namespace = ns; +} + +NS_IMETHODIMP nsImapMailFolder::FolderPrivileges(nsIMsgWindow* window) { + NS_ENSURE_ARG_POINTER(window); + nsresult rv = NS_OK; // if no window... + if (!m_adminUrl.IsEmpty()) { + nsCOMPtr<nsIExternalProtocolService> extProtService = + do_GetService(NS_EXTERNALPROTOCOLSERVICE_CONTRACTID); + if (extProtService) { + nsAutoCString scheme; + nsCOMPtr<nsIURI> uri; + if (NS_FAILED(rv = NS_NewURI(getter_AddRefs(uri), m_adminUrl.get()))) + return rv; + uri->GetScheme(scheme); + if (!scheme.IsEmpty()) { + // if the URL scheme does not correspond to an exposed protocol, then we + // need to hand this link click over to the external protocol handler. + bool isExposed; + rv = extProtService->IsExposedProtocol(scheme.get(), &isExposed); + if (NS_SUCCEEDED(rv) && !isExposed) + return extProtService->LoadURI(uri, nullptr, nullptr, nullptr, false, + false); + } + } + } else { + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = imapService->GetFolderAdminUrl(this, window, this, nullptr); + if (NS_SUCCEEDED(rv)) m_urlRunning = true; + } + return rv; +} + +NS_IMETHODIMP nsImapMailFolder::GetHasAdminUrl(bool* aBool) { + NS_ENSURE_ARG_POINTER(aBool); + nsCOMPtr<nsIImapIncomingServer> imapServer; + nsresult rv = GetImapIncomingServer(getter_AddRefs(imapServer)); + nsCString manageMailAccountUrl; + if (NS_SUCCEEDED(rv) && imapServer) + rv = imapServer->GetManageMailAccountUrl(manageMailAccountUrl); + *aBool = (NS_SUCCEEDED(rv) && !manageMailAccountUrl.IsEmpty()); + return rv; +} + +NS_IMETHODIMP nsImapMailFolder::GetAdminUrl(nsACString& aResult) { + aResult = m_adminUrl; + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::SetAdminUrl(const nsACString& adminUrl) { + m_adminUrl = adminUrl; + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::GetHdrParser( + nsIMsgParseMailMsgState** aHdrParser) { + NS_ENSURE_ARG_POINTER(aHdrParser); + NS_IF_ADDREF(*aHdrParser = m_msgParser); + return NS_OK; +} + +// this is used to issue an arbitrary imap command on the passed in msgs. +// It assumes the command needs to be run in the selected state. +NS_IMETHODIMP nsImapMailFolder::IssueCommandOnMsgs(const nsACString& command, + const char* uids, + nsIMsgWindow* aWindow, + nsIURI** url) { + nsresult rv; + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + return imapService->IssueCommandOnMsgs(this, aWindow, command, + nsDependentCString(uids), url); +} + +NS_IMETHODIMP nsImapMailFolder::FetchCustomMsgAttribute( + const nsACString& attribute, const char* uids, nsIMsgWindow* aWindow, + nsIURI** url) { + nsresult rv; + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + return imapService->FetchCustomMsgAttribute(this, aWindow, attribute, + nsDependentCString(uids), url); +} + +nsresult nsImapMailFolder::MoveIncorporatedMessage( + nsIMsgDBHdr* mailHdr, nsIMsgDatabase* sourceDB, + const nsACString& destFolderUri, nsIMsgFilter* filter, + nsIMsgWindow* msgWindow) { + nsresult rv; + if (m_moveCoalescer) { + nsCOMPtr<nsIMsgFolder> destIFolder; + rv = GetOrCreateFolder(destFolderUri, getter_AddRefs(destIFolder)); + NS_ENSURE_SUCCESS(rv, rv); + + if (destIFolder) { + // check if the destination is a real folder (by checking for null parent) + // and if it can file messages (e.g., servers or news folders can't file + // messages). Or read only imap folders... + bool canFileMessages = true; + nsCOMPtr<nsIMsgFolder> parentFolder; + destIFolder->GetParent(getter_AddRefs(parentFolder)); + if (parentFolder) destIFolder->GetCanFileMessages(&canFileMessages); + if (filter && (!parentFolder || !canFileMessages)) { + filter->SetEnabled(false); + m_filterList->SaveToDefaultFile(); + destIFolder->ThrowAlertMsg("filterDisabled", msgWindow); + return NS_MSG_NOT_A_MAIL_FOLDER; + } + // put the header into the source db, since it needs to be there when we + // copy it and we need a valid header to pass to + // StartAsyncCopyMessagesInto + nsMsgKey keyToFilter; + mailHdr->GetMessageKey(&keyToFilter); + + if (sourceDB && destIFolder) { + bool imapDeleteIsMoveToTrash = DeleteIsMoveToTrash(); + m_moveCoalescer->AddMove(destIFolder, keyToFilter); + // For each folder, we need to keep track of the ids we want to move to + // that folder - we used to store them in the MSG_FolderInfo and then + // when we'd finished downloading headers, we'd iterate through all the + // folders looking for the ones that needed messages moved into them - + // perhaps instead we could keep track of nsIMsgFolder, + // nsTArray<nsMsgKey> pairs here in the imap code. nsTArray<nsMsgKey> + // *idsToMoveFromInbox = msgFolder->GetImapIdsToMoveFromInbox(); + // idsToMoveFromInbox->AppendElement(keyToFilter); + if (imapDeleteIsMoveToTrash) { + } + bool isRead = false; + mailHdr->GetIsRead(&isRead); + if (imapDeleteIsMoveToTrash) rv = NS_OK; + } + } + } else + rv = NS_ERROR_UNEXPECTED; + + // we have to return an error because we do not actually move the message + // it is done async and that can fail + return rv; +} + +/** + * This method assumes that key arrays and flag states are sorted by increasing + * key. + */ +void nsImapMailFolder::FindKeysToDelete(const nsTArray<nsMsgKey>& existingKeys, + nsTArray<nsMsgKey>& keysToDelete, + nsIImapFlagAndUidState* flagState, + uint32_t boxFlags) { + bool showDeletedMessages = ShowDeletedMessages(); + int32_t numMessageInFlagState; + bool partialUIDFetch; + uint32_t uidOfMessage; + imapMessageFlagsType flags; + + flagState->GetNumberOfMessages(&numMessageInFlagState); + flagState->GetPartialUIDFetch(&partialUIDFetch); + + // if we're doing a partialUIDFetch, just delete the keys from the db + // that have the deleted flag set (if not using imap delete model) + // and return. + if (partialUIDFetch) { + if (!showDeletedMessages) { + for (uint32_t i = 0; (int32_t)i < numMessageInFlagState; i++) { + flagState->GetUidOfMessage(i, &uidOfMessage); + // flag state will be zero filled up to first real uid, so ignore those. + if (uidOfMessage) { + flagState->GetMessageFlags(i, &flags); + if (flags & kImapMsgDeletedFlag) + keysToDelete.AppendElement(uidOfMessage); + } + } + } else if (boxFlags & kJustExpunged) { + // we've just issued an expunge with a partial flag state. We should + // delete headers with the imap deleted flag set, because we can't + // tell from the expunge response which messages were deleted. + nsCOMPtr<nsIMsgEnumerator> hdrs; + nsresult rv = GetMessages(getter_AddRefs(hdrs)); + NS_ENSURE_SUCCESS_VOID(rv); + bool hasMore = false; + while (NS_SUCCEEDED(rv = hdrs->HasMoreElements(&hasMore)) && hasMore) { + nsCOMPtr<nsIMsgDBHdr> header; + rv = hdrs->GetNext(getter_AddRefs(header)); + NS_ENSURE_SUCCESS_VOID(rv); + uint32_t msgFlags; + header->GetFlags(&msgFlags); + if (msgFlags & nsMsgMessageFlags::IMAPDeleted) { + nsMsgKey msgKey; + header->GetMessageKey(&msgKey); + keysToDelete.AppendElement(msgKey); + } + } + } + return; + } + // otherwise, we have a complete set of uid's and flags, so we delete + // anything that's in existingKeys but not in the flag state, as well + // as messages with the deleted flag set. + uint32_t total = existingKeys.Length(); + int onlineIndex = 0; // current index into flagState + for (uint32_t keyIndex = 0; keyIndex < total; keyIndex++) { + while ( + (onlineIndex < numMessageInFlagState) && + NS_SUCCEEDED(flagState->GetUidOfMessage(onlineIndex, &uidOfMessage)) && + (existingKeys[keyIndex] > uidOfMessage)) + onlineIndex++; + + flagState->GetUidOfMessage(onlineIndex, &uidOfMessage); + flagState->GetMessageFlags(onlineIndex, &flags); + // delete this key if it is not there or marked deleted + if ((onlineIndex >= numMessageInFlagState) || + (existingKeys[keyIndex] != uidOfMessage) || + ((flags & kImapMsgDeletedFlag) && !showDeletedMessages)) { + nsMsgKey doomedKey = existingKeys[keyIndex]; + if ((int32_t)doomedKey <= 0 && doomedKey != nsMsgKey_None) continue; + + keysToDelete.AppendElement(existingKeys[keyIndex]); + } + + flagState->GetUidOfMessage(onlineIndex, &uidOfMessage); + if (existingKeys[keyIndex] == uidOfMessage) onlineIndex++; + } +} + +void nsImapMailFolder::FindKeysToAdd(const nsTArray<nsMsgKey>& existingKeys, + nsTArray<nsMsgKey>& keysToFetch, + uint32_t& numNewUnread, + nsIImapFlagAndUidState* flagState) { + bool showDeletedMessages = ShowDeletedMessages(); + int dbIndex = 0; // current index into existingKeys + int32_t existTotal, numberOfKnownKeys; + int32_t messageIndex; + + numNewUnread = 0; + existTotal = numberOfKnownKeys = existingKeys.Length(); + flagState->GetNumberOfMessages(&messageIndex); + bool partialUIDFetch; + flagState->GetPartialUIDFetch(&partialUIDFetch); + + for (int32_t flagIndex = 0; flagIndex < messageIndex; flagIndex++) { + uint32_t uidOfMessage; + flagState->GetUidOfMessage(flagIndex, &uidOfMessage); + while ((flagIndex < numberOfKnownKeys) && (dbIndex < existTotal) && + existingKeys[dbIndex] < uidOfMessage) + dbIndex++; + + if ((flagIndex >= numberOfKnownKeys) || (dbIndex >= existTotal) || + (existingKeys[dbIndex] != uidOfMessage)) { + numberOfKnownKeys++; + + imapMessageFlagsType flags; + flagState->GetMessageFlags(flagIndex, &flags); + NS_ASSERTION(uidOfMessage != nsMsgKey_None, "got invalid msg key"); + if (uidOfMessage && uidOfMessage != nsMsgKey_None && + (showDeletedMessages || !(flags & kImapMsgDeletedFlag))) { + if (mDatabase) { + bool dbContainsKey; + if (NS_SUCCEEDED( + mDatabase->ContainsKey(uidOfMessage, &dbContainsKey)) && + dbContainsKey) { + // this is expected in the partial uid fetch case because the + // flag state does not contain all messages, so the db has + // messages the flag state doesn't know about. + if (!partialUIDFetch) NS_ERROR("db has key - flagState messed up?"); + continue; + } + } + keysToFetch.AppendElement(uidOfMessage); + if (!(flags & kImapMsgSeenFlag)) numNewUnread++; + } + } + } +} + +NS_IMETHODIMP nsImapMailFolder::GetMsgHdrsToDownload( + bool* aMoreToDownload, int32_t* aTotalCount, nsTArray<nsMsgKey>& aKeys) { + NS_ENSURE_ARG_POINTER(aMoreToDownload); + NS_ENSURE_ARG_POINTER(aTotalCount); + aKeys.Clear(); + + *aMoreToDownload = false; + *aTotalCount = m_totalKeysToFetch; + if (m_keysToFetch.IsEmpty()) { + return NS_OK; + } + + // if folder isn't open in a window, no reason to limit the number of headers + // we download. + nsCOMPtr<nsIMsgMailSession> session = + do_GetService("@mozilla.org/messenger/services/session;1"); + bool folderOpen = false; + if (session) session->IsFolderOpenInWindow(this, &folderOpen); + + int32_t hdrChunkSize = 200; + if (folderOpen) { + nsresult rv; + nsCOMPtr<nsIPrefBranch> prefBranch( + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + if (prefBranch) + prefBranch->GetIntPref("mail.imap.hdr_chunk_size", &hdrChunkSize); + } + int32_t numKeysToFetch = m_keysToFetch.Length(); + int32_t startIndex = 0; + if (folderOpen && hdrChunkSize > 0 && + (int32_t)m_keysToFetch.Length() > hdrChunkSize) { + numKeysToFetch = hdrChunkSize; + *aMoreToDownload = true; + startIndex = m_keysToFetch.Length() - hdrChunkSize; + } + aKeys.AppendElements(&m_keysToFetch[startIndex], numKeysToFetch); + // Remove these for the incremental header download case, so that + // we know we don't have to download them again. + m_keysToFetch.RemoveElementsAt(startIndex, numKeysToFetch); + + return NS_OK; +} + +void nsImapMailFolder::PrepareToAddHeadersToMailDB(nsIImapProtocol* aProtocol) { + // now, tell it we don't need any bodies. + nsTArray<nsMsgKey> noBodies; + aProtocol->NotifyBodysToDownload(noBodies); +} + +void nsImapMailFolder::TweakHeaderFlags(nsIImapProtocol* aProtocol, + nsIMsgDBHdr* tweakMe) { + if (mDatabase && aProtocol && tweakMe) { + tweakMe->SetMessageKey(m_curMsgUid); + tweakMe->SetMessageSize(m_nextMessageByteLength); + + bool foundIt = false; + + nsCOMPtr<nsIImapFlagAndUidState> flagState; + nsresult rv = aProtocol->GetFlagAndUidState(getter_AddRefs(flagState)); + NS_ENSURE_SUCCESS_VOID(rv); + rv = flagState->HasMessage(m_curMsgUid, &foundIt); + + if (NS_SUCCEEDED(rv) && foundIt) { + imapMessageFlagsType imap_flags; + nsCString customFlags; + flagState->GetMessageFlagsByUid(m_curMsgUid, &imap_flags); + if (imap_flags & kImapMsgCustomKeywordFlag) { + flagState->GetCustomFlags(m_curMsgUid, getter_Copies(customFlags)); + } + + // make a mask and clear these message flags + uint32_t mask = nsMsgMessageFlags::Read | nsMsgMessageFlags::Replied | + nsMsgMessageFlags::Marked | + nsMsgMessageFlags::IMAPDeleted | + nsMsgMessageFlags::Labels; + uint32_t dbHdrFlags; + + tweakMe->GetFlags(&dbHdrFlags); + tweakMe->AndFlags(~mask, &dbHdrFlags); + + // set the new value for these flags + uint32_t newFlags = 0; + if (imap_flags & kImapMsgSeenFlag) + newFlags |= nsMsgMessageFlags::Read; + else // if (imap_flags & kImapMsgRecentFlag) + newFlags |= nsMsgMessageFlags::New; + + // Okay here is the MDN needed logic (if DNT header seen): + /* if server support user defined flag: + XXX TODO: Fix badly formatted comment which doesn't reflect the code. + MDNSent flag set => clear kMDNNeeded flag + MDNSent flag not set => do nothing, leave kMDNNeeded on + else if + not nsMsgMessageFlags::New => clear kMDNNeeded flag + nsMsgMessageFlags::New => do nothing, leave kMDNNeeded on + */ + uint16_t userFlags; + rv = aProtocol->GetSupportedUserFlags(&userFlags); + if (NS_SUCCEEDED(rv) && (userFlags & (kImapMsgSupportUserFlag | + kImapMsgSupportMDNSentFlag))) { + if (imap_flags & kImapMsgMDNSentFlag) { + newFlags |= nsMsgMessageFlags::MDNReportSent; + if (dbHdrFlags & nsMsgMessageFlags::MDNReportNeeded) + tweakMe->AndFlags(~nsMsgMessageFlags::MDNReportNeeded, &dbHdrFlags); + } + } + + if (imap_flags & kImapMsgAnsweredFlag) + newFlags |= nsMsgMessageFlags::Replied; + if (imap_flags & kImapMsgFlaggedFlag) + newFlags |= nsMsgMessageFlags::Marked; + if (imap_flags & kImapMsgDeletedFlag) + newFlags |= nsMsgMessageFlags::IMAPDeleted; + if (imap_flags & kImapMsgForwardedFlag) + newFlags |= nsMsgMessageFlags::Forwarded; + if (newFlags) tweakMe->OrFlags(newFlags, &dbHdrFlags); + if (!customFlags.IsEmpty()) + (void)HandleCustomFlags(m_curMsgUid, tweakMe, userFlags, customFlags); + } + } +} + +NS_IMETHODIMP +nsImapMailFolder::SetupMsgWriteStream(nsIFile* aFile, bool addDummyEnvelope) { + nsresult rv; + aFile->Remove(false); + m_tempMessageStreamBytesWritten = 0; + rv = MsgNewBufferedFileOutputStream( + getter_AddRefs(m_tempMessageStream), aFile, + PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE, 00700); + if (m_tempMessageStream && addDummyEnvelope) { + nsAutoCString result; + char* ct; + uint32_t writeCount; + time_t now = time((time_t*)0); + ct = ctime(&now); + ct[24] = 0; + result = "From - "; + result += ct; + result += MSG_LINEBREAK; + + rv = m_tempMessageStream->Write(result.get(), result.Length(), &writeCount); + NS_ENSURE_SUCCESS(rv, rv); + m_tempMessageStreamBytesWritten += writeCount; + + result = "X-Mozilla-Status: 0001"; + result += MSG_LINEBREAK; + rv = m_tempMessageStream->Write(result.get(), result.Length(), &writeCount); + NS_ENSURE_SUCCESS(rv, rv); + m_tempMessageStreamBytesWritten += writeCount; + + result = "X-Mozilla-Status2: 00000000"; + result += MSG_LINEBREAK; + rv = m_tempMessageStream->Write(result.get(), result.Length(), &writeCount); + NS_ENSURE_SUCCESS(rv, rv); + m_tempMessageStreamBytesWritten += writeCount; + } + return rv; +} + +NS_IMETHODIMP nsImapMailFolder::DownloadMessagesForOffline( + nsTArray<RefPtr<nsIMsgDBHdr>> const& messages, nsIMsgWindow* window) { + nsAutoCString messageIds; + nsTArray<nsMsgKey> srcKeyArray; + nsresult rv = BuildIdsAndKeyArray(messages, messageIds, srcKeyArray); + if (NS_FAILED(rv) || messageIds.IsEmpty()) return rv; + + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = AcquireSemaphore(static_cast<nsIMsgFolder*>(this)); + if (NS_FAILED(rv)) { + ThrowAlertMsg("operationFailedFolderBusy", window); + return rv; + } + return imapService->DownloadMessagesForOffline(messageIds, this, this, + window); +} + +NS_IMETHODIMP nsImapMailFolder::DownloadAllForOffline(nsIUrlListener* listener, + nsIMsgWindow* msgWindow) { + nsresult rv; + nsCOMPtr<nsIURI> runningURI; + bool noSelect; + GetFlag(nsMsgFolderFlags::ImapNoselect, &noSelect); + + if (!noSelect) { + nsAutoCString messageIdsToDownload; + nsTArray<nsMsgKey> msgsToDownload; + + GetDatabase(); + m_downloadingFolderForOfflineUse = true; + + rv = AcquireSemaphore(static_cast<nsIMsgFolder*>(this)); + if (NS_FAILED(rv)) { + m_downloadingFolderForOfflineUse = false; + ThrowAlertMsg("operationFailedFolderBusy", msgWindow); + return rv; + } + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // Selecting the folder with nsIImapUrl::shouldStoreMsgOffline true will + // cause us to fetch any message bodies we don't have. + m_urlListener = listener; + rv = imapService->SelectFolder(this, this, msgWindow, + getter_AddRefs(runningURI)); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIImapUrl> imapUrl(do_QueryInterface(runningURI)); + if (imapUrl) imapUrl->SetStoreResultsOffline(true); + m_urlRunning = true; + } + } else + rv = NS_MSG_FOLDER_UNREADABLE; + return rv; +} + +NS_IMETHODIMP +nsImapMailFolder::ParseAdoptedMsgLine(const char* adoptedMessageLine, + nsMsgKey uidOfMessage, + nsIImapUrl* aImapUrl) { + NS_ENSURE_ARG_POINTER(aImapUrl); + uint32_t count = 0; + nsresult rv; + // remember the uid of the message we're downloading. + m_curMsgUid = uidOfMessage; + if (!m_offlineHeader) { + rv = GetMessageHeader(uidOfMessage, getter_AddRefs(m_offlineHeader)); + if (NS_SUCCEEDED(rv) && !m_offlineHeader) rv = NS_ERROR_UNEXPECTED; + NS_ENSURE_SUCCESS(rv, rv); + rv = StartNewOfflineMessage(); + NS_ENSURE_SUCCESS(rv, rv); + } + // 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. + 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) { + rv = m_tempMessageStream->Write(adoptedMessageLine, + PL_strlen(adoptedMessageLine), &count); + NS_ENSURE_SUCCESS(rv, rv); + m_tempMessageStreamBytesWritten += count; + } + return NS_OK; +} + +void nsImapMailFolder::EndOfflineDownload() { + if (m_tempMessageStream) { + m_tempMessageStream->Close(); + m_tempMessageStream = nullptr; + ReleaseSemaphore(static_cast<nsIMsgFolder*>(this)); + if (mDatabase) mDatabase->Commit(nsMsgDBCommitType::kLargeCommit); + } + m_offlineHeader = nullptr; +} + +NS_IMETHODIMP +nsImapMailFolder::NormalEndMsgWriteStream(nsMsgKey uidOfMessage, bool markRead, + nsIImapUrl* imapUrl, + int32_t updatedMessageSize) { + if (updatedMessageSize != -1) { + // retrieve the message header to update size, if we don't already have it + nsCOMPtr<nsIMsgDBHdr> msgHeader = m_offlineHeader; + if (!msgHeader) GetMessageHeader(uidOfMessage, getter_AddRefs(msgHeader)); + if (msgHeader) { + uint32_t msgSize; + msgHeader->GetMessageSize(&msgSize); + MOZ_LOG(IMAP, mozilla::LogLevel::Debug, + ("Updating stored message size from %u, new size %d", msgSize, + updatedMessageSize)); + msgHeader->SetMessageSize(updatedMessageSize); + // only commit here if this isn't an offline message + // offline header gets committed in EndNewOfflineMessage() called below + if (mDatabase && !m_offlineHeader) + mDatabase->Commit(nsMsgDBCommitType::kLargeCommit); + } else + NS_WARNING( + "Failed to get message header when trying to update message size"); + } + + if (m_offlineHeader) EndNewOfflineMessage(NS_OK); + + m_curMsgUid = uidOfMessage; + + // Apply filter now if it needed a body + if (m_filterListRequiresBody) { + if (m_filterList) { + nsCOMPtr<nsIMsgDBHdr> newMsgHdr; + GetMessageHeader(uidOfMessage, getter_AddRefs(newMsgHdr)); + GetMoveCoalescer(); + nsCOMPtr<nsIMsgWindow> msgWindow; + if (imapUrl) { + nsresult rv; + nsCOMPtr<nsIMsgMailNewsUrl> msgUrl; + msgUrl = do_QueryInterface(imapUrl, &rv); + if (msgUrl && NS_SUCCEEDED(rv)) + msgUrl->GetMsgWindow(getter_AddRefs(msgWindow)); + } + m_filterList->ApplyFiltersToHdr(nsMsgFilterType::InboxRule, newMsgHdr, + this, mDatabase, EmptyCString(), this, + msgWindow); + NotifyFolderEvent(kFiltersApplied); + } + // Process filter plugins and other items normally done at the end of + // HeaderFetchCompleted. + bool pendingMoves = m_moveCoalescer && m_moveCoalescer->HasPendingMoves(); + PlaybackCoalescedOperations(); + + bool filtersRun; + CallFilterPlugins(nullptr, &filtersRun); + int32_t numNewBiffMsgs = 0; + if (m_performingBiff) GetNumNewMessages(false, &numNewBiffMsgs); + + if (!filtersRun && m_performingBiff && mDatabase && numNewBiffMsgs > 0 && + (!pendingMoves || !ShowPreviewText())) { + // If we are performing biff for this folder, tell the + // stand-alone biff about the new high water mark + // We must ensure that the server knows that we are performing biff. + // Otherwise the stand-alone biff won't fire. + nsCOMPtr<nsIMsgIncomingServer> server; + if (NS_SUCCEEDED(GetServer(getter_AddRefs(server))) && server) + server->SetPerformingBiff(true); + + SetBiffState(nsIMsgFolder::nsMsgBiffState_NewMail); + if (server) server->SetPerformingBiff(false); + m_performingBiff = false; + } + + if (m_filterList) (void)m_filterList->FlushLogIfNecessary(); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsImapMailFolder::AbortMsgWriteStream() { + m_offlineHeader = nullptr; + return NS_ERROR_FAILURE; +} + +// message move/copy related methods +NS_IMETHODIMP +nsImapMailFolder::OnlineCopyCompleted(nsIImapProtocol* aProtocol, + ImapOnlineCopyState aCopyState) { + NS_ENSURE_ARG_POINTER(aProtocol); + + nsresult rv; + if (aCopyState == ImapOnlineCopyStateType::kSuccessfulCopy) { + nsCOMPtr<nsIImapUrl> imapUrl; + rv = aProtocol->GetRunningImapURL(getter_AddRefs(imapUrl)); + if (NS_FAILED(rv) || !imapUrl) return NS_ERROR_FAILURE; + nsImapAction action; + rv = imapUrl->GetImapAction(&action); + if (NS_FAILED(rv)) return rv; + if (action != nsIImapUrl::nsImapOnlineToOfflineMove) + return NS_ERROR_FAILURE; // don't assert here... + nsCString messageIds; + rv = imapUrl->GetListOfMessageIds(messageIds); + if (NS_FAILED(rv)) return rv; + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + return imapService->AddMessageFlags(this, nullptr, messageIds, + kImapMsgDeletedFlag, true); + } + /* unhandled copystate */ + if (m_copyState) // whoops, this is the wrong folder - should use the source + // folder + { + nsCOMPtr<nsIMsgFolder> srcFolder; + srcFolder = do_QueryInterface(m_copyState->m_srcSupport, &rv); + if (srcFolder) srcFolder->NotifyFolderEvent(kDeleteOrMoveMsgCompleted); + } else + rv = NS_ERROR_FAILURE; + + return rv; +} + +NS_IMETHODIMP +nsImapMailFolder::CloseMockChannel(nsIImapMockChannel* aChannel) { + aChannel->Close(); + return NS_OK; +} + +NS_IMETHODIMP +nsImapMailFolder::ReleaseUrlCacheEntry(nsIMsgMailNewsUrl* aUrl) { + NS_ENSURE_ARG_POINTER(aUrl); + return aUrl->SetMemCacheEntry(nullptr); +} + +NS_IMETHODIMP +nsImapMailFolder::BeginMessageUpload() { return NS_ERROR_FAILURE; } + +nsresult nsImapMailFolder::HandleCustomFlags(nsMsgKey uidOfMessage, + nsIMsgDBHdr* dbHdr, + uint16_t userFlags, + nsCString& keywords) { + nsresult rv = GetDatabase(); + NS_ENSURE_SUCCESS(rv, rv); + + ToLowerCase(keywords); + bool messageClassified = true; + // ### TODO: we really should parse the keywords into space delimited keywords + // before checking + // Mac Mail, Yahoo uses "NotJunk" + if (FindInReadable("NonJunk"_ns, keywords, + nsCaseInsensitiveCStringComparator) || + FindInReadable("NotJunk"_ns, keywords, + nsCaseInsensitiveCStringComparator)) { + nsAutoCString msgJunkScore; + msgJunkScore.AppendInt(nsIJunkMailPlugin::IS_HAM_SCORE); + mDatabase->SetStringProperty(uidOfMessage, "junkscore", msgJunkScore); + } else if (FindInReadable("Junk"_ns, keywords, + nsCaseInsensitiveCStringComparator)) { + uint32_t newFlags; + dbHdr->AndFlags(~nsMsgMessageFlags::New, &newFlags); + nsAutoCString msgJunkScore; + msgJunkScore.AppendInt(nsIJunkMailPlugin::IS_SPAM_SCORE); + mDatabase->SetStringProperty(uidOfMessage, "junkscore", msgJunkScore); + } else + messageClassified = false; + if (messageClassified) { + // only set the junkscore origin if it wasn't set before. + nsCString existingProperty; + dbHdr->GetStringProperty("junkscoreorigin", existingProperty); + if (existingProperty.IsEmpty()) + dbHdr->SetStringProperty("junkscoreorigin", "imapflag"_ns); + } + + if (!(userFlags & kImapMsgSupportUserFlag)) { + nsCString localKeywords; + nsCString prevKeywords; + dbHdr->GetStringProperty("keywords", localKeywords); + dbHdr->GetStringProperty("prevkeywords", prevKeywords); + // localKeywords: tags currently stored in database for the message. + // keywords: tags stored in server and obtained when flags for the message + // were last fetched. (Parameter of this function.) + // prevKeywords: saved keywords from previous call of this function. + // clang-format off + MOZ_LOG(IMAP_KW, mozilla::LogLevel::Debug, + ("UID=%" PRIu32 ", localKeywords=|%s| keywords=|%s|, prevKeywords=|%s|", + uidOfMessage, localKeywords.get(), keywords.get(), prevKeywords.get())); + // clang-format on + + // Store keywords to detect changes on next call of this function. + dbHdr->SetStringProperty("prevkeywords", keywords); + + // Parse the space separated strings into arrays. + nsTArray<nsCString> localKeywordArray; + nsTArray<nsCString> keywordArray; + nsTArray<nsCString> prevKeywordArray; + ParseString(localKeywords, ' ', localKeywordArray); + ParseString(keywords, ' ', keywordArray); + ParseString(prevKeywords, ' ', prevKeywordArray); + + // If keyword not received now but was the last time, remove it from + // the localKeywords. This means the keyword was removed by another user + // sharing the folder. + for (uint32_t i = 0; i < prevKeywordArray.Length(); i++) { + bool inRcvd = keywordArray.Contains(prevKeywordArray[i]); + bool inLocal = localKeywordArray.Contains(prevKeywordArray[i]); + if (!inRcvd && inLocal) + localKeywordArray.RemoveElement(prevKeywordArray[i]); + } + + // Combine local and rcvd keyword arrays into a single string + // so it can be passed to SetStringProperty(). If element of + // local already in rcvd, avoid duplicates in combined string. + nsAutoCString combinedKeywords; + for (uint32_t i = 0; i < localKeywordArray.Length(); i++) { + if (!keywordArray.Contains(localKeywordArray[i])) { + combinedKeywords.Append(localKeywordArray[i]); + combinedKeywords.Append(' '); + } + } + for (uint32_t i = 0; i < keywordArray.Length(); i++) { + combinedKeywords.Append(keywordArray[i]); + combinedKeywords.Append(' '); + } + MOZ_LOG(IMAP_KW, mozilla::LogLevel::Debug, + ("combinedKeywords stored = |%s|", combinedKeywords.get())); + // combinedKeywords are tags being stored in database for the message. + return dbHdr->SetStringProperty("keywords", combinedKeywords); + } + return (userFlags & kImapMsgSupportUserFlag) + ? dbHdr->SetStringProperty("keywords", keywords) + : NS_OK; +} + +// synchronize the message flags in the database with the server flags +nsresult nsImapMailFolder::SyncFlags(nsIImapFlagAndUidState* flagState) { + nsresult rv = GetDatabase(); // we need a database for this + NS_ENSURE_SUCCESS(rv, rv); + bool partialUIDFetch; + flagState->GetPartialUIDFetch(&partialUIDFetch); + + // update all of the database flags + int32_t messageIndex; + uint32_t messageSize; + + // Take this opportunity to recalculate the folder size, if we're not a + // partial (condstore) fetch. + int64_t newFolderSize = 0; + + flagState->GetNumberOfMessages(&messageIndex); + + uint16_t supportedUserFlags; + flagState->GetSupportedUserFlags(&supportedUserFlags); + + for (int32_t flagIndex = 0; flagIndex < messageIndex; flagIndex++) { + uint32_t uidOfMessage; + flagState->GetUidOfMessage(flagIndex, &uidOfMessage); + imapMessageFlagsType flags; + flagState->GetMessageFlags(flagIndex, &flags); + bool containsKey; + rv = mDatabase->ContainsKey(uidOfMessage, &containsKey); + // if we don't have the header, don't diddle the flags. + // GetMsgHdrForKey will create the header if it doesn't exist. + if (NS_FAILED(rv) || !containsKey) continue; + + nsCOMPtr<nsIMsgDBHdr> dbHdr; + rv = mDatabase->GetMsgHdrForKey(uidOfMessage, getter_AddRefs(dbHdr)); + if (NS_FAILED(rv)) continue; + if (NS_SUCCEEDED(dbHdr->GetMessageSize(&messageSize))) + newFolderSize += messageSize; + + nsCString keywords; + if (NS_SUCCEEDED( + flagState->GetCustomFlags(uidOfMessage, getter_Copies(keywords)))) + HandleCustomFlags(uidOfMessage, dbHdr, supportedUserFlags, keywords); + + NotifyMessageFlagsFromHdr(dbHdr, uidOfMessage, flags); + } + if (!partialUIDFetch && newFolderSize != mFolderSize) { + int64_t oldFolderSize = mFolderSize; + mFolderSize = newFolderSize; + NotifyIntPropertyChanged(kFolderSize, oldFolderSize, mFolderSize); + } + + return NS_OK; +} + +// helper routine to sync the flags on a given header +nsresult nsImapMailFolder::NotifyMessageFlagsFromHdr(nsIMsgDBHdr* dbHdr, + nsMsgKey msgKey, + uint32_t flags) { + nsresult rv = GetDatabase(); + NS_ENSURE_SUCCESS(rv, rv); + + // Although it may seem strange to keep a local reference of mDatabase here, + // the current lifetime management of databases requires that methods + // sometimes null the database when they think they opened it. Unfortunately + // experience shows this happens when we don't expect, so for crash protection + // best practice with the current flawed database management is to keep a + // local reference when there will be complex calls in a method. See bug + // 1312254. + nsCOMPtr<nsIMsgDatabase> database(mDatabase); + NS_ENSURE_STATE(database); + + database->MarkHdrRead(dbHdr, (flags & kImapMsgSeenFlag) != 0, nullptr); + database->MarkHdrReplied(dbHdr, (flags & kImapMsgAnsweredFlag) != 0, nullptr); + database->MarkHdrMarked(dbHdr, (flags & kImapMsgFlaggedFlag) != 0, nullptr); + database->MarkImapDeleted(msgKey, (flags & kImapMsgDeletedFlag) != 0, + nullptr); + + uint32_t supportedFlags; + GetSupportedUserFlags(&supportedFlags); + if (supportedFlags & kImapMsgSupportForwardedFlag) + database->MarkForwarded(msgKey, (flags & kImapMsgForwardedFlag) != 0, + nullptr); + if (supportedFlags & kImapMsgSupportMDNSentFlag) + database->MarkMDNSent(msgKey, (flags & kImapMsgMDNSentFlag) != 0, nullptr); + + return NS_OK; +} + +// message flags operation - this is called from the imap protocol, +// proxied over from the imap thread to the ui thread, when a flag changes +NS_IMETHODIMP +nsImapMailFolder::NotifyMessageFlags(uint32_t aFlags, + const nsACString& aKeywords, + nsMsgKey aMsgKey, + uint64_t aHighestModSeq) { + if (NS_SUCCEEDED(GetDatabase()) && mDatabase) { + bool msgDeleted = aFlags & kImapMsgDeletedFlag; + if (aHighestModSeq || msgDeleted) { + nsCOMPtr<nsIDBFolderInfo> dbFolderInfo; + mDatabase->GetDBFolderInfo(getter_AddRefs(dbFolderInfo)); + if (dbFolderInfo) { + if (aHighestModSeq) { + char intStrBuf[40]; + PR_snprintf(intStrBuf, sizeof(intStrBuf), "%llu", aHighestModSeq); + MOZ_LOG(IMAP_CS, mozilla::LogLevel::Debug, + ("NotifyMessageFlags(): Store highest MODSEQ=%" PRIu64 + " for folder=%s", + aHighestModSeq, m_onlineFolderName.get())); + dbFolderInfo->SetCharProperty(kModSeqPropertyName, + nsDependentCString(intStrBuf)); + } + if (msgDeleted) { + uint32_t oldDeletedCount; + dbFolderInfo->GetUint32Property(kDeletedHdrCountPropertyName, 0, + &oldDeletedCount); + dbFolderInfo->SetUint32Property(kDeletedHdrCountPropertyName, + oldDeletedCount + 1); + } + } + } + nsCOMPtr<nsIMsgDBHdr> dbHdr; + bool containsKey; + nsresult rv = mDatabase->ContainsKey(aMsgKey, &containsKey); + // if we don't have the header, don't diddle the flags. + // GetMsgHdrForKey will create the header if it doesn't exist. + if (NS_FAILED(rv) || !containsKey) return rv; + rv = mDatabase->GetMsgHdrForKey(aMsgKey, getter_AddRefs(dbHdr)); + if (NS_SUCCEEDED(rv) && dbHdr) { + uint32_t supportedUserFlags; + GetSupportedUserFlags(&supportedUserFlags); + NotifyMessageFlagsFromHdr(dbHdr, aMsgKey, aFlags); + nsCString keywords(aKeywords); + HandleCustomFlags(aMsgKey, dbHdr, supportedUserFlags, keywords); + } + } + return NS_OK; +} + +NS_IMETHODIMP +nsImapMailFolder::NotifyMessageDeleted(const char* onlineFolderName, + bool deleteAllMsgs, + const char* msgIdString) { + if (deleteAllMsgs) return NS_OK; + + if (!msgIdString) return NS_OK; + + nsTArray<nsMsgKey> affectedMessages; + ParseUidString(msgIdString, affectedMessages); + + if (!ShowDeletedMessages()) { + GetDatabase(); + NS_ENSURE_TRUE(mDatabase, NS_OK); + if (!ShowDeletedMessages()) { + if (!affectedMessages.IsEmpty()) // perhaps Search deleted these messages + { + DeleteStoreMessages(affectedMessages); + mDatabase->DeleteMessages(affectedMessages, nullptr); + } + } else // && !imapDeleteIsMoveToTrash // TODO: can this ever be executed? + SetIMAPDeletedFlag(mDatabase, affectedMessages, false); + } + return NS_OK; +} + +bool nsImapMailFolder::ShowDeletedMessages() { + nsresult rv; + nsCOMPtr<nsIImapHostSessionList> hostSession = + do_GetService(kCImapHostSessionList, &rv); + NS_ENSURE_SUCCESS(rv, false); + + bool showDeleted = false; + nsCString serverKey; + GetServerKey(serverKey); + hostSession->GetShowDeletedMessagesForHost(serverKey.get(), showDeleted); + + return showDeleted; +} + +bool nsImapMailFolder::DeleteIsMoveToTrash() { + nsresult err; + nsCOMPtr<nsIImapHostSessionList> hostSession = + do_GetService(kCImapHostSessionList, &err); + NS_ENSURE_SUCCESS(err, true); + bool rv = true; + + nsCString serverKey; + GetServerKey(serverKey); + hostSession->GetDeleteIsMoveToTrashForHost(serverKey.get(), rv); + return rv; +} + +nsresult nsImapMailFolder::GetTrashFolder(nsIMsgFolder** pTrashFolder) { + NS_ENSURE_ARG_POINTER(pTrashFolder); + nsCOMPtr<nsIMsgFolder> rootFolder; + nsresult rv = GetRootFolder(getter_AddRefs(rootFolder)); + if (NS_SUCCEEDED(rv) && rootFolder) { + rv = rootFolder->GetFolderWithFlags(nsMsgFolderFlags::Trash, pTrashFolder); + if (!*pTrashFolder) rv = NS_ERROR_FAILURE; + } + return rv; +} + +// store nsMsgMessageFlags::IMAPDeleted in the specified mailhdr records +void nsImapMailFolder::SetIMAPDeletedFlag(nsIMsgDatabase* mailDB, + const nsTArray<nsMsgKey>& msgids, + bool markDeleted) { + nsresult markStatus = NS_OK; + uint32_t total = msgids.Length(); + + for (uint32_t msgIndex = 0; NS_SUCCEEDED(markStatus) && (msgIndex < total); + msgIndex++) + markStatus = + mailDB->MarkImapDeleted(msgids[msgIndex], markDeleted, nullptr); +} + +NS_IMETHODIMP +nsImapMailFolder::GetMessageSizeFromDB(const char* id, uint32_t* size) { + NS_ENSURE_ARG_POINTER(size); + + *size = 0; + nsresult rv = GetDatabase(); + NS_ENSURE_SUCCESS(rv, rv); + if (id) { + nsMsgKey key = msgKeyFromInt(ParseUint64Str(id)); + nsCOMPtr<nsIMsgDBHdr> mailHdr; + rv = mDatabase->GetMsgHdrForKey(key, getter_AddRefs(mailHdr)); + if (NS_SUCCEEDED(rv) && mailHdr) rv = mailHdr->GetMessageSize(size); + } + return rv; +} + +NS_IMETHODIMP +nsImapMailFolder::GetCurMoveCopyMessageInfo(nsIImapUrl* runningUrl, + PRTime* aDate, + nsACString& aKeywords, + uint32_t* aResult) { + nsCOMPtr<nsISupports> copyState; + runningUrl->GetCopyState(getter_AddRefs(copyState)); + if (copyState) { + nsCOMPtr<nsImapMailCopyState> mailCopyState = do_QueryInterface(copyState); + uint32_t supportedFlags = 0; + GetSupportedUserFlags(&supportedFlags); + if (mailCopyState && + mailCopyState->m_curIndex < mailCopyState->m_messages.Length()) { + nsIMsgDBHdr* message = + mailCopyState->m_messages[mailCopyState->m_curIndex]; + message->GetFlags(aResult); + if (aDate) message->GetDate(aDate); + if (supportedFlags & kImapMsgSupportUserFlag) { + // setup the custom imap keywords, which includes the message keywords + // plus any junk status + nsCString junkscore; + message->GetStringProperty("junkscore", junkscore); + bool isJunk = false, isNotJunk = false; + if (!junkscore.IsEmpty()) { + if (junkscore.EqualsLiteral("0")) + isNotJunk = true; + else + isJunk = true; + } + + nsCString keywords; // MsgFindKeyword can't use nsACString + message->GetStringProperty("keywords", keywords); + int32_t start; + int32_t length; + bool hasJunk = MsgFindKeyword("junk"_ns, keywords, &start, &length); + if (hasJunk && !isJunk) + keywords.Cut(start, length); + else if (!hasJunk && isJunk) + keywords.AppendLiteral(" Junk"); + bool hasNonJunk = + MsgFindKeyword("nonjunk"_ns, keywords, &start, &length); + if (!hasNonJunk) + hasNonJunk = MsgFindKeyword("notjunk"_ns, keywords, &start, &length); + if (hasNonJunk && !isNotJunk) + keywords.Cut(start, length); + else if (!hasNonJunk && isNotJunk) + keywords.AppendLiteral(" NonJunk"); + + // Cleanup extra spaces + while (!keywords.IsEmpty() && keywords.First() == ' ') + keywords.Cut(0, 1); + while (!keywords.IsEmpty() && keywords.Last() == ' ') + keywords.Cut(keywords.Length() - 1, 1); + while (!keywords.IsEmpty() && (start = keywords.Find(" "_ns)) >= 0) + keywords.Cut(start, 1); + aKeywords.Assign(keywords); + } + } + // if we don't have a source header, and it's not the drafts folder, + // then mark the message read, since it must be an append to the + // fcc or templates folder. + else if (mailCopyState) { + *aResult = mailCopyState->m_newMsgFlags; + if (supportedFlags & kImapMsgSupportUserFlag) + aKeywords.Assign(mailCopyState->m_newMsgKeywords); + } + } + return NS_OK; +} + +// nsIUrlListener implementation. +NS_IMETHODIMP +nsImapMailFolder::OnStartRunningUrl(nsIURI* aUrl) { + NS_ASSERTION(aUrl, "sanity check - need to be be running non-null url"); + nsCOMPtr<nsIMsgMailNewsUrl> mailUrl = do_QueryInterface(aUrl); + if (mailUrl) { + bool updatingFolder; + mailUrl->GetUpdatingFolder(&updatingFolder); + m_updatingFolder = updatingFolder; + } + m_urlRunning = true; + return NS_OK; +} + +// nsIUrlListener implementation. +// nsImapMailFolder passes itself as a listener when it kicks off operations +// on the nsIImapService. So, when the operation completes, this gets called +// to handle all the different operations, using a big switch statement. +NS_IMETHODIMP +nsImapMailFolder::OnStopRunningUrl(nsIURI* aUrl, nsresult aExitCode) { + nsresult rv; + bool endedOfflineDownload = false; + nsImapAction imapAction = nsIImapUrl::nsImapTest; + m_urlRunning = false; + m_updatingFolder = false; + nsCOMPtr<nsIMsgMailSession> session = + do_GetService("@mozilla.org/messenger/services/session;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + if (aUrl) { + nsCOMPtr<nsIImapUrl> imapUrl = do_QueryInterface(aUrl, &rv); + NS_ENSURE_SUCCESS(rv, rv); + bool downloadingForOfflineUse; + imapUrl->GetStoreResultsOffline(&downloadingForOfflineUse); + bool hasSemaphore = false; + // if we have the folder locked, clear it. + TestSemaphore(static_cast<nsIMsgFolder*>(this), &hasSemaphore); + if (hasSemaphore) ReleaseSemaphore(static_cast<nsIMsgFolder*>(this)); + if (downloadingForOfflineUse) { + endedOfflineDownload = true; + EndOfflineDownload(); + } + nsCOMPtr<nsIMsgWindow> msgWindow; + nsCOMPtr<nsIMsgMailNewsUrl> mailUrl = do_QueryInterface(aUrl); + bool folderOpen = false; + if (mailUrl) mailUrl->GetMsgWindow(getter_AddRefs(msgWindow)); + if (session) session->IsFolderOpenInWindow(this, &folderOpen); +#ifdef DEBUG_bienvenu + printf("stop running url %s\n", aUrl->GetSpecOrDefault().get()); +#endif + + if (imapUrl) { + DisplayStatusMsg(imapUrl, EmptyString()); + imapUrl->GetImapAction(&imapAction); + if (imapAction == nsIImapUrl::nsImapMsgFetch || + imapAction == nsIImapUrl::nsImapMsgDownloadForOffline) { + ReleaseSemaphore(static_cast<nsIMsgFolder*>(this)); + 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("@mozilla.org/messenger/msgnotificationservice;1")); + if (notifier && m_copyState) { + if (imapAction == nsIImapUrl::nsImapOnlineMove) { + notifier->NotifyMsgsMoveCopyCompleted(true, m_copyState->m_messages, + this, {}); + } else if (imapAction == nsIImapUrl::nsImapOnlineCopy) { + notifier->NotifyMsgsMoveCopyCompleted(false, m_copyState->m_messages, + this, {}); + } else if (imapAction == nsIImapUrl::nsImapDeleteMsg) { + notifier->NotifyMsgsDeleted(m_copyState->m_messages); + } + } + + switch (imapAction) { + case nsIImapUrl::nsImapDeleteMsg: + case nsIImapUrl::nsImapOnlineMove: + case nsIImapUrl::nsImapOnlineCopy: + if (NS_SUCCEEDED(aExitCode)) { + if (folderOpen) + UpdateFolder(msgWindow); + else + UpdatePendingCounts(); + } + + if (m_copyState) { + nsCOMPtr<nsIMsgFolder> srcFolder = + do_QueryInterface(m_copyState->m_srcSupport, &rv); + if (m_copyState->m_isMove && !m_copyState->m_isCrossServerOp) { + if (NS_SUCCEEDED(aExitCode)) { + nsCOMPtr<nsIMsgDatabase> srcDB; + if (srcFolder) + rv = srcFolder->GetMsgDatabase(getter_AddRefs(srcDB)); + if (NS_SUCCEEDED(rv) && srcDB) { + RefPtr<nsImapMoveCopyMsgTxn> msgTxn; + nsTArray<nsMsgKey> srcKeyArray; + if (m_copyState->m_allowUndo) { + msgTxn = m_copyState->m_undoMsgTxn; + if (msgTxn) msgTxn->GetSrcKeyArray(srcKeyArray); + } else { + nsAutoCString messageIds; + rv = BuildIdsAndKeyArray(m_copyState->m_messages, + messageIds, srcKeyArray); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (!ShowDeletedMessages()) { + // We only reach here for same-server operations + // (!m_copyState->m_isCrossServerOp in if above), so we can + // assume that the src is also imap that uses offline + // storage. + DeleteStoreMessages(srcKeyArray, srcFolder); + srcDB->DeleteMessages(srcKeyArray, nullptr); + } else + MarkMessagesImapDeleted(&srcKeyArray, true, srcDB); + } + srcFolder->EnableNotifications(allMessageCountNotifications, + true); + // even if we're showing deleted messages, + // we still need to notify FE so it will show the imap deleted + // flag + srcFolder->NotifyFolderEvent(kDeleteOrMoveMsgCompleted); + // is there a way to see that we think we have new msgs? + nsCOMPtr<nsIPrefBranch> prefBranch( + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + if (NS_SUCCEEDED(rv)) { + bool showPreviewText; + prefBranch->GetBoolPref("mail.biff.alert.show_preview", + &showPreviewText); + // if we're showing preview text, update ourselves if we got a + // new unread message copied so that we can download the new + // headers and have a chance to preview the msg bodies. + if (!folderOpen && showPreviewText && + m_copyState->m_unreadCount > 0 && + !(mFlags & + (nsMsgFolderFlags::Trash | nsMsgFolderFlags::Junk))) + UpdateFolder(msgWindow); + } + } else { + srcFolder->EnableNotifications(allMessageCountNotifications, + true); + srcFolder->NotifyFolderEvent(kDeleteOrMoveMsgFailed); + } + } + if (m_copyState->m_msgWindow && + m_copyState->m_undoMsgTxn && // may be null from filters + NS_SUCCEEDED( + aExitCode)) // we should do this only if move/copy succeeds + { + nsCOMPtr<nsITransactionManager> txnMgr; + m_copyState->m_msgWindow->GetTransactionManager( + getter_AddRefs(txnMgr)); + if (txnMgr) { + RefPtr<nsImapMoveCopyMsgTxn> txn = m_copyState->m_undoMsgTxn; + mozilla::DebugOnly<nsresult> rv2 = txnMgr->DoTransaction(txn); + NS_ASSERTION(NS_SUCCEEDED(rv2), "doing transaction failed"); + } + } + // nsImapUrl can hold a pointer to our m_copyState, so force a + // release here (see Bug 1586494). + imapUrl->SetCopyState(nullptr); + (void)OnCopyCompleted(m_copyState->m_srcSupport, aExitCode); + } + + // we're the dest folder of a move/copy - if we're not open in the ui, + // then we should clear our nsMsgDatabase pointer. Otherwise, the db + // would be open until the user selected it and then selected another + // folder. but don't do this for the trash or inbox - we'll leave them + // open + if (!folderOpen && + !(mFlags & (nsMsgFolderFlags::Trash | nsMsgFolderFlags::Inbox))) + SetMsgDatabase(nullptr); + break; + case nsIImapUrl::nsImapSubtractMsgFlags: { + // this isn't really right - we'd like to know we were + // deleting a message to start with, but it probably + // won't do any harm. + imapMessageFlagsType flags = 0; + imapUrl->GetMsgFlags(&flags); + // we need to subtract the delete flag in db only in case when we show + // deleted msgs + if (flags & kImapMsgDeletedFlag && ShowDeletedMessages()) { + nsCOMPtr<nsIMsgDatabase> db; + rv = GetMsgDatabase(getter_AddRefs(db)); + if (NS_SUCCEEDED(rv) && db) { + nsTArray<nsMsgKey> keyArray; + nsCString keyString; + imapUrl->GetListOfMessageIds(keyString); + ParseUidString(keyString.get(), keyArray); + MarkMessagesImapDeleted(&keyArray, false, db); + db->Commit(nsMsgDBCommitType::kLargeCommit); + } + } + } break; + case nsIImapUrl::nsImapAddMsgFlags: { + imapMessageFlagsType flags = 0; + imapUrl->GetMsgFlags(&flags); + if (flags & kImapMsgDeletedFlag) { + // we need to delete headers from db only when we don't show deleted + // msgs + if (!ShowDeletedMessages()) { + nsCOMPtr<nsIMsgDatabase> db; + rv = GetMsgDatabase(getter_AddRefs(db)); + if (NS_SUCCEEDED(rv) && db) { + nsTArray<nsMsgKey> keyArray; + nsCString keyString; + imapUrl->GetListOfMessageIds(keyString); + ParseUidString(keyString.get(), keyArray); + + // For pluggable stores that do not support compaction, we need + // to delete the messages now. + bool supportsCompaction = false; + nsCOMPtr<nsIMsgPluggableStore> offlineStore; + (void)GetMsgStore(getter_AddRefs(offlineStore)); + if (offlineStore) + offlineStore->GetSupportsCompaction(&supportsCompaction); + + nsTArray<RefPtr<nsIMsgDBHdr>> msgHdrs; + if (notifier || !supportsCompaction) { + MsgGetHeadersFromKeys(db, keyArray, msgHdrs); + } + + // Notify listeners of delete. + if (notifier && !msgHdrs.IsEmpty()) { + // XXX Currently, the DeleteMessages below gets executed twice + // on deletes. Once in DeleteMessages, once here. The second + // time, it silently fails to delete. This is why we're also + // checking whether the array is empty. + notifier->NotifyMsgsDeleted(msgHdrs); + } + + if (!supportsCompaction && !msgHdrs.IsEmpty()) + DeleteStoreMessages(msgHdrs); + + db->DeleteMessages(keyArray, nullptr); + db->SetSummaryValid(true); + db->Commit(nsMsgDBCommitType::kLargeCommit); + } + } + } + } break; + case nsIImapUrl::nsImapAppendMsgFromFile: + case nsIImapUrl::nsImapAppendDraftFromFile: + if (m_copyState) { + if (NS_SUCCEEDED(aExitCode)) { + UpdatePendingCounts(); + + m_copyState->m_curIndex++; + if (m_copyState->m_curIndex >= m_copyState->m_messages.Length()) { + nsCOMPtr<nsIUrlListener> saveUrlListener = m_urlListener; + if (folderOpen) { + // This gives a way for the caller to get notified + // when the UpdateFolder url is done. + // (if the nsIMsgCopyServiceListener also implements + // nsIUrlListener) + if (m_copyState->m_listener) + m_urlListener = do_QueryInterface(m_copyState->m_listener); + } + if (m_copyState->m_msgWindow && m_copyState->m_undoMsgTxn) { + nsCOMPtr<nsITransactionManager> txnMgr; + m_copyState->m_msgWindow->GetTransactionManager( + getter_AddRefs(txnMgr)); + if (txnMgr) { + RefPtr<nsImapMoveCopyMsgTxn> txn = + m_copyState->m_undoMsgTxn; + txnMgr->DoTransaction(txn); + } + } + (void)OnCopyCompleted(m_copyState->m_srcSupport, aExitCode); + if (folderOpen || + imapAction == nsIImapUrl::nsImapAppendDraftFromFile) { + UpdateFolderWithListener(msgWindow, m_urlListener); + m_urlListener = saveUrlListener; + } + } + } else { + // clear the copyState if copy has failed + (void)OnCopyCompleted(m_copyState->m_srcSupport, aExitCode); + } + } + break; + case nsIImapUrl::nsImapMoveFolderHierarchy: + if (m_copyState) // delete folder gets here, but w/o an m_copyState + { + nsCOMPtr<nsIMsgCopyService> copyService = do_GetService( + "@mozilla.org/messenger/messagecopyservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgFolder> srcFolder = + do_QueryInterface(m_copyState->m_srcSupport); + if (srcFolder) { + copyService->NotifyCompletion(m_copyState->m_srcSupport, this, + aExitCode); + } + m_copyState = nullptr; + } + break; + case nsIImapUrl::nsImapRenameFolder: + if (NS_FAILED(aExitCode)) { + NotifyFolderEvent(kRenameCompleted); + } + break; + case nsIImapUrl::nsImapDeleteAllMsgs: + if (NS_SUCCEEDED(aExitCode)) { + if (folderOpen) + UpdateFolder(msgWindow); + else { + ChangeNumPendingTotalMessages(-mNumPendingTotalMessages); + ChangeNumPendingUnread(-mNumPendingUnreadMessages); + m_numServerUnseenMessages = 0; + } + } + break; + case nsIImapUrl::nsImapListFolder: + if (NS_SUCCEEDED(aExitCode)) { + // listing folder will open db; don't leave the db open. + SetMsgDatabase(nullptr); + if (!m_verifiedAsOnlineFolder) { + // If folder is not verified, we remove it. + nsCOMPtr<nsIMsgFolder> parent; + rv = GetParent(getter_AddRefs(parent)); + if (NS_SUCCEEDED(rv) && parent) { + nsCOMPtr<nsIMsgImapMailFolder> imapParent = + do_QueryInterface(parent); + if (imapParent) this->RemoveLocalSelf(); + } + } + } + break; + case nsIImapUrl::nsImapRefreshFolderUrls: + // we finished getting an admin url for the folder. + if (!m_adminUrl.IsEmpty()) FolderPrivileges(msgWindow); + break; + case nsIImapUrl::nsImapCreateFolder: + if (NS_FAILED(aExitCode)) // if success notification already done + { + NotifyFolderEvent(kFolderCreateFailed); + } + break; + case nsIImapUrl::nsImapSubscribe: + if (NS_SUCCEEDED(aExitCode) && msgWindow) { + nsCString canonicalFolderName; + imapUrl->CreateCanonicalSourceFolderPathString( + getter_Copies(canonicalFolderName)); + nsCOMPtr<nsIMsgFolder> rootFolder; + nsresult rv = GetRootFolder(getter_AddRefs(rootFolder)); + if (NS_SUCCEEDED(rv) && rootFolder) { + nsCOMPtr<nsIMsgImapMailFolder> imapRoot = + do_QueryInterface(rootFolder); + if (imapRoot) { + nsCOMPtr<nsIMsgImapMailFolder> foundFolder; + rv = imapRoot->FindOnlineSubFolder(canonicalFolderName, + getter_AddRefs(foundFolder)); + if (NS_SUCCEEDED(rv) && foundFolder) { + nsCString uri; + nsCOMPtr<nsIMsgFolder> msgFolder = + do_QueryInterface(foundFolder); + if (msgFolder) { + nsCOMPtr<nsIObserverService> obsServ = + mozilla::services::GetObserverService(); + obsServ->NotifyObservers(msgFolder, "folder-subscribed", + nullptr); + } + } + } + } + } + break; + case nsIImapUrl::nsImapExpungeFolder: + break; + default: + break; + } + } + // give base class a chance to send folder loaded notification... + rv = nsMsgDBFolder::OnStopRunningUrl(aUrl, aExitCode); + } + // if we're not running a url, we must not be getting new mail. + SetGettingNewMessages(false); + + // If we're planning to inform another listener then do that now. + // Some folder methods can take a listener to inform when the operation + // is complete e.g. UpdateFolderWithListener(), DownloadAllForOffline(), + // GetNewMessages(). That listener is stashed in m_urlListener. + if (m_urlListener) { + nsCOMPtr<nsIUrlListener> saveListener = m_urlListener; + m_urlListener = nullptr; + saveListener->OnStopRunningUrl(aUrl, aExitCode); + } + return rv; +} + +void nsImapMailFolder::UpdatePendingCounts() { + if (m_copyState) { + int32_t delta = + m_copyState->m_isCrossServerOp ? 1 : m_copyState->m_messages.Length(); + if (!m_copyState->m_selectedState && m_copyState->m_messages.IsEmpty()) { + // special case from CopyFileMessage(): + // - copied a single message in from a file + // - no previously-existing messages are involved + delta = 1; + } + ChangePendingTotal(delta); + + // count the moves that were unread + int numUnread = m_copyState->m_unreadCount; + if (numUnread) { + m_numServerUnseenMessages += + numUnread; // adjust last status count by this delta. + ChangeNumPendingUnread(numUnread); + } + SummaryChanged(); + } +} + +NS_IMETHODIMP +nsImapMailFolder::ClearFolderRights() { + SetFolderNeedsACLListed(false); + delete m_folderACL; + m_folderACL = new nsMsgIMAPFolderACL(this); + return NS_OK; +} + +NS_IMETHODIMP +nsImapMailFolder::AddFolderRights(const nsACString& userName, + const nsACString& rights) { + SetFolderNeedsACLListed(false); + GetFolderACL()->SetFolderRightsForUser(userName, rights); + return NS_OK; +} + +NS_IMETHODIMP +nsImapMailFolder::RefreshFolderRights() { + if (GetFolderACL()->GetIsFolderShared()) + SetFlag(nsMsgFolderFlags::PersonalShared); + else + ClearFlag(nsMsgFolderFlags::PersonalShared); + return NS_OK; +} + +NS_IMETHODIMP +nsImapMailFolder::SetCopyResponseUid(const char* msgIdString, + nsIImapUrl* aUrl) { // CopyMessages() only + nsresult rv = NS_OK; + RefPtr<nsImapMoveCopyMsgTxn> msgTxn; + nsCOMPtr<nsISupports> copyState; + + if (aUrl) aUrl->GetCopyState(getter_AddRefs(copyState)); + + if (copyState) { + nsCOMPtr<nsImapMailCopyState> mailCopyState = + do_QueryInterface(copyState, &rv); + if (NS_FAILED(rv)) return rv; + if (mailCopyState->m_undoMsgTxn) msgTxn = mailCopyState->m_undoMsgTxn; + } else if (aUrl && m_pendingOfflineMoves.Length()) { + nsCString urlSourceMsgIds, undoTxnSourceMsgIds; + aUrl->GetListOfMessageIds(urlSourceMsgIds); + RefPtr<nsImapMoveCopyMsgTxn> imapUndo = m_pendingOfflineMoves[0]; + if (imapUndo) { + imapUndo->GetSrcMsgIds(undoTxnSourceMsgIds); + if (undoTxnSourceMsgIds.Equals(urlSourceMsgIds)) msgTxn = imapUndo; + // ### we should handle batched moves, but lets keep it simple for a2. + m_pendingOfflineMoves.Clear(); + } + } + if (msgTxn) msgTxn->SetCopyResponseUid(msgIdString); + return NS_OK; +} + +NS_IMETHODIMP +nsImapMailFolder::StartMessage(nsIMsgMailNewsUrl* aUrl) { + nsCOMPtr<nsIImapUrl> imapUrl(do_QueryInterface(aUrl)); + nsCOMPtr<nsISupports> copyState; + NS_ENSURE_TRUE(imapUrl, NS_ERROR_FAILURE); + + imapUrl->GetCopyState(getter_AddRefs(copyState)); + if (copyState) { + nsCOMPtr<nsICopyMessageStreamListener> listener = + do_QueryInterface(copyState); + if (listener) listener->StartMessage(); + } + return NS_OK; +} + +NS_IMETHODIMP +nsImapMailFolder::EndMessage(nsIMsgMailNewsUrl* aUrl, nsMsgKey uidOfMessage) { + nsCOMPtr<nsIImapUrl> imapUrl(do_QueryInterface(aUrl)); + nsCOMPtr<nsISupports> copyState; + NS_ENSURE_TRUE(imapUrl, NS_ERROR_FAILURE); + imapUrl->GetCopyState(getter_AddRefs(copyState)); + if (copyState) { + nsCOMPtr<nsICopyMessageStreamListener> listener = + do_QueryInterface(copyState); + if (listener) listener->EndMessage(uidOfMessage); + } + return NS_OK; +} + +#define WHITESPACE " \015\012" // token delimiter + +NS_IMETHODIMP +nsImapMailFolder::NotifySearchHit(nsIMsgMailNewsUrl* aUrl, + const char* searchHitLine) { + NS_ENSURE_ARG_POINTER(aUrl); + nsresult rv = GetDatabase(); + NS_ENSURE_SUCCESS(rv, rv); + + // expect search results in the form of "* SEARCH <hit> <hit> ..." + // expect search results in the form of "* SEARCH <hit> <hit> ..." + nsCString tokenString(searchHitLine); + char* currentPosition = PL_strcasestr(tokenString.get(), "SEARCH"); + if (currentPosition) { + currentPosition += strlen("SEARCH"); + bool shownUpdateAlert = false; + char* hitUidToken = NS_strtok(WHITESPACE, ¤tPosition); + while (hitUidToken) { + long naturalLong; // %l is 64 bits on OSF1 + sscanf(hitUidToken, "%ld", &naturalLong); + nsMsgKey hitUid = (nsMsgKey)naturalLong; + + nsCOMPtr<nsIMsgDBHdr> hitHeader; + rv = mDatabase->GetMsgHdrForKey(hitUid, getter_AddRefs(hitHeader)); + if (NS_SUCCEEDED(rv) && hitHeader) { + nsCOMPtr<nsIMsgSearchSession> searchSession; + nsCOMPtr<nsIMsgSearchAdapter> searchAdapter; + aUrl->GetSearchSession(getter_AddRefs(searchSession)); + if (searchSession) { + searchSession->GetRunningAdapter(getter_AddRefs(searchAdapter)); + if (searchAdapter) searchAdapter->AddResultElement(hitHeader); + } + } else if (!shownUpdateAlert) { + } + + hitUidToken = NS_strtok(WHITESPACE, ¤tPosition); + } + } + return NS_OK; +} + +NS_IMETHODIMP +nsImapMailFolder::SetAppendMsgUid(nsMsgKey aKey, nsIImapUrl* aUrl) { + nsresult rv; + nsCOMPtr<nsISupports> copyState; + if (aUrl) aUrl->GetCopyState(getter_AddRefs(copyState)); + if (copyState) { + nsCOMPtr<nsImapMailCopyState> mailCopyState = + do_QueryInterface(copyState, &rv); + if (NS_FAILED(rv)) return rv; + + if (mailCopyState->m_undoMsgTxn) // CopyMessages() + { + RefPtr<nsImapMoveCopyMsgTxn> msgTxn; + msgTxn = mailCopyState->m_undoMsgTxn; + msgTxn->AddDstKey(aKey); + } else if (mailCopyState->m_listener) // CopyFileMessage(); + // Draft/Template goes here + { + mailCopyState->m_appendUID = aKey; + mailCopyState->m_listener->SetMessageKey(aKey); + } + } + return NS_OK; +} + +NS_IMETHODIMP +nsImapMailFolder::GetMessageId(nsIImapUrl* aUrl, nsACString& messageId) { + nsresult rv = NS_OK; + nsCOMPtr<nsISupports> copyState; + + if (aUrl) aUrl->GetCopyState(getter_AddRefs(copyState)); + if (copyState) { + nsCOMPtr<nsImapMailCopyState> mailCopyState = + do_QueryInterface(copyState, &rv); + if (NS_FAILED(rv)) return rv; + if (mailCopyState->m_listener) + rv = mailCopyState->m_listener->GetMessageId(messageId); + } + if (NS_SUCCEEDED(rv) && messageId.Length() > 0) { + if (messageId.First() == '<') messageId.Cut(0, 1); + if (messageId.Last() == '>') messageId.SetLength(messageId.Length() - 1); + } + return rv; +} + +NS_IMETHODIMP +nsImapMailFolder::HeaderFetchCompleted(nsIImapProtocol* aProtocol) { + nsCOMPtr<nsIMsgWindow> + msgWindow; // we might need this for the filter plugins. + if (mBackupDatabase) RemoveBackupMsgDatabase(); + + SetSizeOnDisk(mFolderSize); + int32_t numNewBiffMsgs = 0; + if (m_performingBiff) GetNumNewMessages(false, &numNewBiffMsgs); + + bool pendingMoves = m_moveCoalescer && m_moveCoalescer->HasPendingMoves(); + PlaybackCoalescedOperations(); + if (aProtocol) { + // check if we should download message bodies because it's the inbox and + // the server is specified as one where where we download msg bodies + // automatically. Or if we autosyncing all offline folders. + nsCOMPtr<nsIImapIncomingServer> imapServer; + GetImapIncomingServer(getter_AddRefs(imapServer)); + + bool autoDownloadNewHeaders = false; + bool autoSyncOfflineStores = false; + + if (imapServer) { + imapServer->GetAutoSyncOfflineStores(&autoSyncOfflineStores); + imapServer->GetDownloadBodiesOnGetNewMail(&autoDownloadNewHeaders); + if (m_filterListRequiresBody) autoDownloadNewHeaders = true; + } + bool notifiedBodies = false; + if (m_downloadingFolderForOfflineUse || autoSyncOfflineStores || + autoDownloadNewHeaders) { + nsTArray<nsMsgKey> keysToDownload; + GetBodysToDownload(&keysToDownload); + if (!keysToDownload.IsEmpty() && + (m_downloadingFolderForOfflineUse || autoDownloadNewHeaders)) { + // this is the case when DownloadAllForOffline is called. + notifiedBodies = true; + aProtocol->NotifyBodysToDownload(keysToDownload); + } else { + // create auto-sync state object lazily + InitAutoSyncState(); + if (MOZ_LOG_TEST(gAutoSyncLog, mozilla::LogLevel::Debug)) { + int32_t flags = 0; + GetFlags((uint32_t*)&flags); + nsString folderName; + GetName(folderName); + nsCString utfLeafName; + CopyUTF16toUTF8(folderName, utfLeafName); + MOZ_LOG(gAutoSyncLog, mozilla::LogLevel::Debug, + ("%s: foldername=%s, flags=0x%X, " + "isOffline=%s, nsMsgFolderFlags::Offline=0x%X", + __func__, utfLeafName.get(), flags, + (flags & nsMsgFolderFlags::Offline) ? "true" : "false", + nsMsgFolderFlags::Offline)); + MOZ_LOG(gAutoSyncLog, mozilla::LogLevel::Debug, + ("%s: created autosync obj, have keys to download=%s", + __func__, keysToDownload.IsEmpty() ? "false" : "true")); + } + // make enough room for new downloads + m_autoSyncStateObj->ManageStorageSpace(); // currently a no-op + + m_autoSyncStateObj->SetServerCounts( + m_numServerTotalMessages, m_numServerRecentMessages, + m_numServerUnseenMessages, m_nextUID); + m_autoSyncStateObj->OnNewHeaderFetchCompleted(keysToDownload); + } + } + if (!notifiedBodies) { + nsTArray<nsMsgKey> noBodies; + aProtocol->NotifyBodysToDownload(noBodies); + } + + nsCOMPtr<nsIURI> runningUri; + aProtocol->GetRunningUrl(getter_AddRefs(runningUri)); + if (runningUri) { + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(runningUri); + if (mailnewsUrl) mailnewsUrl->GetMsgWindow(getter_AddRefs(msgWindow)); + } + } + + // delay calling plugins if filter application is also delayed + if (!m_filterListRequiresBody) { + bool filtersRun; + CallFilterPlugins(msgWindow, &filtersRun); + if (!filtersRun && m_performingBiff && mDatabase && numNewBiffMsgs > 0 && + (!pendingMoves || !ShowPreviewText())) { + // If we are performing biff for this folder, tell the + // stand-alone biff about the new high water mark + // We must ensure that the server knows that we are performing biff. + // Otherwise the stand-alone biff won't fire. + nsCOMPtr<nsIMsgIncomingServer> server; + if (NS_SUCCEEDED(GetServer(getter_AddRefs(server))) && server) + server->SetPerformingBiff(true); + + SetBiffState(nsIMsgFolder::nsMsgBiffState_NewMail); + if (server) server->SetPerformingBiff(false); + m_performingBiff = false; + } + + if (m_filterList) (void)m_filterList->FlushLogIfNecessary(); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsImapMailFolder::SetBiffStateAndUpdate(nsMsgBiffState biffState) { + SetBiffState(biffState); + return NS_OK; +} + +NS_IMETHODIMP +nsImapMailFolder::GetUidValidity(int32_t* uidValidity) { + NS_ENSURE_ARG(uidValidity); + if ((int32_t)m_uidValidity == kUidUnknown) { + nsCOMPtr<nsIMsgDatabase> db; + nsCOMPtr<nsIDBFolderInfo> dbFolderInfo; + (void)GetDBFolderInfoAndDB(getter_AddRefs(dbFolderInfo), + getter_AddRefs(db)); + if (db) db->GetDBFolderInfo(getter_AddRefs(dbFolderInfo)); + + if (dbFolderInfo) + dbFolderInfo->GetImapUidValidity((int32_t*)&m_uidValidity); + } + *uidValidity = m_uidValidity; + return NS_OK; +} + +NS_IMETHODIMP +nsImapMailFolder::SetUidValidity(int32_t uidValidity) { + m_uidValidity = uidValidity; + return NS_OK; +} + +NS_IMETHODIMP +nsImapMailFolder::FillInFolderProps(nsIMsgImapFolderProps* aFolderProps) { + NS_ENSURE_ARG(aFolderProps); + const char* folderTypeStringID; + const char* folderTypeDescStringID = nullptr; + const char* folderQuotaStatusStringID; + nsString folderType; + nsString folderTypeDesc; + nsString folderQuotaStatusDesc; + nsCOMPtr<nsIStringBundle> bundle; + nsresult rv = IMAPGetStringBundle(getter_AddRefs(bundle)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIImapIncomingServer> imapServer; + rv = GetImapIncomingServer(getter_AddRefs(imapServer)); + // if for some bizarre reason this fails, we'll still fall through to the + // normal sharing code + if (NS_SUCCEEDED(rv)) { + // get the latest committed imap capabilities bit mask. + eIMAPCapabilityFlags capability = kCapabilityUndefined; + imapServer->GetCapability(&capability); + bool haveACL = capability & kACLCapability; + bool haveQuota = capability & kQuotaCapability; + + // Figure out what to display in the Quota tab of the folder properties. + // Does the server support quotas? This depends on the latest imap + // CAPABILITY response. + if (haveQuota) { + // Have quota capability. Have we asked the server for quota information? + if (m_folderQuotaCommandIssued) { + // Has the server replied with all the quota info? + if (m_folderQuotaDataIsValid) { + if (!m_folderQuota.IsEmpty()) { + // If so, set quota data to show in the quota tab + folderQuotaStatusStringID = nullptr; + aFolderProps->SetQuotaData(m_folderQuota); + } else { + // The server reported no quota limits on this folder. + folderQuotaStatusStringID = "imapQuotaStatusNoQuota2"; + } + } else { + // The getquotaroot command was sent to the server but the complete + // response was not yet received when the folder properties were + // requested. This is rare. Request the folder properties again to + // obtain the quota data. + folderQuotaStatusStringID = "imapQuotaStatusInProgress"; + } + } else { + // The folder is not open, so no quota information is available + folderQuotaStatusStringID = "imapQuotaStatusFolderNotOpen"; + } + } else { + // Either the server doesn't support quotas, or we don't know if it does + // (e.g., because we don't have a connection yet). If the latter, we fall + // back to saying that no information is available because the folder is + // not yet open. + folderQuotaStatusStringID = (capability == kCapabilityUndefined) + ? "imapQuotaStatusFolderNotOpen" + : "imapQuotaStatusNotSupported"; + } + + if (!folderQuotaStatusStringID) { + // Display quota data + aFolderProps->ShowQuotaData(true); + } else { + // Hide quota data and show reason why it is not available + aFolderProps->ShowQuotaData(false); + + rv = IMAPGetStringByName(folderQuotaStatusStringID, + getter_Copies(folderQuotaStatusDesc)); + if (NS_SUCCEEDED(rv)) aFolderProps->SetQuotaStatus(folderQuotaStatusDesc); + } + + // See if the server supports ACL. + // If not, just set the folder description to a string that says + // the server doesn't support sharing, and return. + if (!haveACL) { + rv = IMAPGetStringByName("imapServerDoesntSupportAcl", + getter_Copies(folderTypeDesc)); + if (NS_SUCCEEDED(rv)) + aFolderProps->SetFolderTypeDescription(folderTypeDesc); + aFolderProps->ServerDoesntSupportACL(); + return NS_OK; + } + } + if (mFlags & nsMsgFolderFlags::ImapPublic) { + folderTypeStringID = "imapPublicFolderTypeName"; + folderTypeDescStringID = "imapPublicFolderTypeDescription"; + } else if (mFlags & nsMsgFolderFlags::ImapOtherUser) { + folderTypeStringID = "imapOtherUsersFolderTypeName"; + nsCString owner; + nsString uniOwner; + GetFolderOwnerUserName(owner); + if (owner.IsEmpty()) { + IMAPGetStringByName(folderTypeStringID, getter_Copies(uniOwner)); + // Another user's folder, for which we couldn't find an owner name + NS_ASSERTION(false, "couldn't get owner name for other user's folder"); + } else { + CopyUTF8toUTF16(owner, uniOwner); + } + AutoTArray<nsString, 1> params = {uniOwner}; + bundle->FormatStringFromName("imapOtherUsersFolderTypeDescription", params, + folderTypeDesc); + } else if (GetFolderACL()->GetIsFolderShared()) { + folderTypeStringID = "imapPersonalSharedFolderTypeName"; + folderTypeDescStringID = "imapPersonalSharedFolderTypeDescription"; + } else { + folderTypeStringID = "imapPersonalSharedFolderTypeName"; + folderTypeDescStringID = "imapPersonalFolderTypeDescription"; + } + + rv = IMAPGetStringByName(folderTypeStringID, getter_Copies(folderType)); + if (NS_SUCCEEDED(rv)) aFolderProps->SetFolderType(folderType); + + if (folderTypeDesc.IsEmpty() && folderTypeDescStringID) + IMAPGetStringByName(folderTypeDescStringID, getter_Copies(folderTypeDesc)); + if (!folderTypeDesc.IsEmpty()) + aFolderProps->SetFolderTypeDescription(folderTypeDesc); + + nsString rightsString; + rv = CreateACLRightsStringForFolder(rightsString); + if (NS_SUCCEEDED(rv)) aFolderProps->SetFolderPermissions(rightsString); + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::SetAclFlags(uint32_t aclFlags) { + nsresult rv = NS_OK; + if (m_aclFlags != aclFlags) { + nsCOMPtr<nsIDBFolderInfo> dbFolderInfo; + bool dbWasOpen = (mDatabase != nullptr); + rv = GetDatabase(); + + m_aclFlags = aclFlags; + if (mDatabase) { + rv = mDatabase->GetDBFolderInfo(getter_AddRefs(dbFolderInfo)); + if (NS_SUCCEEDED(rv) && dbFolderInfo) + dbFolderInfo->SetUint32Property("aclFlags", aclFlags); + // if setting the acl flags caused us to open the db, release the ref + // because on startup, we might get acl on all folders,which will + // leave a lot of db's open. + if (!dbWasOpen) { + mDatabase->Close(true /* commit changes */); + mDatabase = nullptr; + } + } + } + return rv; +} + +NS_IMETHODIMP nsImapMailFolder::GetAclFlags(uint32_t* aclFlags) { + NS_ENSURE_ARG_POINTER(aclFlags); + nsresult rv; + ReadDBFolderInfo(false); // update cache first. + if (m_aclFlags == kAclInvalid) // -1 means invalid value, so get it from db. + { + nsCOMPtr<nsIDBFolderInfo> dbFolderInfo; + bool dbWasOpen = (mDatabase != nullptr); + rv = GetDatabase(); + + if (mDatabase) { + rv = mDatabase->GetDBFolderInfo(getter_AddRefs(dbFolderInfo)); + if (NS_SUCCEEDED(rv) && dbFolderInfo) { + rv = dbFolderInfo->GetUint32Property("aclFlags", 0, aclFlags); + m_aclFlags = *aclFlags; + } + // if getting the acl flags caused us to open the db, release the ref + // because on startup, we might get acl on all folders,which will + // leave a lot of db's open. + if (!dbWasOpen) { + mDatabase->Close(true /* commit changes */); + mDatabase = nullptr; + } + } + } else + *aclFlags = m_aclFlags; + return NS_OK; +} + +nsresult nsImapMailFolder::SetSupportedUserFlags(uint32_t userFlags) { + nsCOMPtr<nsIDBFolderInfo> dbFolderInfo; + nsresult rv = GetDatabase(); + + m_supportedUserFlags = userFlags; + if (mDatabase) { + rv = mDatabase->GetDBFolderInfo(getter_AddRefs(dbFolderInfo)); + if (NS_SUCCEEDED(rv) && dbFolderInfo) + dbFolderInfo->SetUint32Property("imapFlags", userFlags); + } + return rv; +} + +nsresult nsImapMailFolder::GetSupportedUserFlags(uint32_t* userFlags) { + NS_ENSURE_ARG_POINTER(userFlags); + + nsresult rv = NS_OK; + + ReadDBFolderInfo(false); // update cache first. + if (m_supportedUserFlags == 0) // 0 means invalid value, so get it from db. + { + nsCOMPtr<nsIDBFolderInfo> dbFolderInfo; + rv = GetDatabase(); + + if (mDatabase) { + rv = mDatabase->GetDBFolderInfo(getter_AddRefs(dbFolderInfo)); + if (NS_SUCCEEDED(rv) && dbFolderInfo) { + rv = dbFolderInfo->GetUint32Property("imapFlags", 0, userFlags); + m_supportedUserFlags = *userFlags; + } + } + } else + *userFlags = m_supportedUserFlags; + return rv; +} + +NS_IMETHODIMP nsImapMailFolder::GetCanOpenFolder(bool* aBool) { + NS_ENSURE_ARG_POINTER(aBool); + bool noSelect; + GetFlag(nsMsgFolderFlags::ImapNoselect, &noSelect); + *aBool = (noSelect) ? false : GetFolderACL()->GetCanIReadFolder(); + return NS_OK; +} + +///////// nsMsgIMAPFolderACL class /////////////////////////////// + +// This string is defined in the ACL RFC to be "anyone" +#define IMAP_ACL_ANYONE_STRING "anyone" + +nsMsgIMAPFolderACL::nsMsgIMAPFolderACL(nsImapMailFolder* folder) + : m_rightsHash(24) { + NS_ASSERTION(folder, "need folder"); + m_folder = folder; + m_aclCount = 0; + BuildInitialACLFromCache(); +} + +nsMsgIMAPFolderACL::~nsMsgIMAPFolderACL() {} + +// We cache most of our own rights in the MSG_FOLDER_PREF_* flags +void nsMsgIMAPFolderACL::BuildInitialACLFromCache() { + nsAutoCString myrights; + + uint32_t startingFlags; + m_folder->GetAclFlags(&startingFlags); + + if (startingFlags & IMAP_ACL_READ_FLAG) myrights += "r"; + if (startingFlags & IMAP_ACL_STORE_SEEN_FLAG) myrights += "s"; + if (startingFlags & IMAP_ACL_WRITE_FLAG) myrights += "w"; + if (startingFlags & IMAP_ACL_INSERT_FLAG) myrights += "i"; + if (startingFlags & IMAP_ACL_POST_FLAG) myrights += "p"; + if (startingFlags & IMAP_ACL_CREATE_SUBFOLDER_FLAG) myrights += "c"; + if (startingFlags & IMAP_ACL_DELETE_FLAG) myrights += "dt"; + if (startingFlags & IMAP_ACL_ADMINISTER_FLAG) myrights += "a"; + if (startingFlags & IMAP_ACL_EXPUNGE_FLAG) myrights += "e"; + + if (!myrights.IsEmpty()) SetFolderRightsForUser(EmptyCString(), myrights); +} + +void nsMsgIMAPFolderACL::UpdateACLCache() { + uint32_t startingFlags = 0; + m_folder->GetAclFlags(&startingFlags); + + if (GetCanIReadFolder()) + startingFlags |= IMAP_ACL_READ_FLAG; + else + startingFlags &= ~IMAP_ACL_READ_FLAG; + + if (GetCanIStoreSeenInFolder()) + startingFlags |= IMAP_ACL_STORE_SEEN_FLAG; + else + startingFlags &= ~IMAP_ACL_STORE_SEEN_FLAG; + + if (GetCanIWriteFolder()) + startingFlags |= IMAP_ACL_WRITE_FLAG; + else + startingFlags &= ~IMAP_ACL_WRITE_FLAG; + + if (GetCanIInsertInFolder()) + startingFlags |= IMAP_ACL_INSERT_FLAG; + else + startingFlags &= ~IMAP_ACL_INSERT_FLAG; + + if (GetCanIPostToFolder()) + startingFlags |= IMAP_ACL_POST_FLAG; + else + startingFlags &= ~IMAP_ACL_POST_FLAG; + + if (GetCanICreateSubfolder()) + startingFlags |= IMAP_ACL_CREATE_SUBFOLDER_FLAG; + else + startingFlags &= ~IMAP_ACL_CREATE_SUBFOLDER_FLAG; + + if (GetCanIDeleteInFolder()) + startingFlags |= IMAP_ACL_DELETE_FLAG; + else + startingFlags &= ~IMAP_ACL_DELETE_FLAG; + + if (GetCanIAdministerFolder()) + startingFlags |= IMAP_ACL_ADMINISTER_FLAG; + else + startingFlags &= ~IMAP_ACL_ADMINISTER_FLAG; + + if (GetCanIExpungeFolder()) + startingFlags |= IMAP_ACL_EXPUNGE_FLAG; + else + startingFlags &= ~IMAP_ACL_EXPUNGE_FLAG; + + m_folder->SetAclFlags(startingFlags); +} + +bool nsMsgIMAPFolderACL::SetFolderRightsForUser(const nsACString& userName, + const nsACString& rights) { + nsCString myUserName; + nsCOMPtr<nsIMsgIncomingServer> server; + nsresult rv = m_folder->GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, false); + + server->GetUsername(myUserName); + + nsAutoCString ourUserName; + if (userName.IsEmpty()) + ourUserName.Assign(myUserName); + else + ourUserName.Assign(userName); + + if (ourUserName.IsEmpty()) return false; + + ToLowerCase(ourUserName); + nsCString oldValue = m_rightsHash.Get(ourUserName); + if (!oldValue.IsEmpty()) { + m_rightsHash.Remove(ourUserName); + m_aclCount--; + NS_ASSERTION(m_aclCount >= 0, "acl count can't go negative"); + } + m_aclCount++; + m_rightsHash.InsertOrUpdate(ourUserName, PromiseFlatCString(rights)); + + if (myUserName.Equals(ourUserName) || + ourUserName.EqualsLiteral(IMAP_ACL_ANYONE_STRING)) + // if this is setting an ACL for me, cache it in the folder pref flags + UpdateACLCache(); + + return true; +} + +NS_IMETHODIMP nsImapMailFolder::GetOtherUsersWithAccess( + nsIUTF8StringEnumerator** aResult) { + return GetFolderACL()->GetOtherUsers(aResult); +} + +nsresult nsMsgIMAPFolderACL::GetOtherUsers(nsIUTF8StringEnumerator** aResult) { + nsCString myUserName; + nsCOMPtr<nsIMsgIncomingServer> server; + nsresult rv = m_folder->GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + server->GetUsername(myUserName); + + // We need to filter out myUserName from m_rightsHash. + nsTArray<nsCString>* resultArray = new nsTArray<nsCString>; + for (auto iter = m_rightsHash.Iter(); !iter.Done(); iter.Next()) { + if (!iter.Key().Equals(myUserName)) resultArray->AppendElement(iter.Key()); + } + + // enumerator will free resultArray + return NS_NewAdoptingUTF8StringEnumerator(aResult, resultArray); +} + +nsresult nsImapMailFolder::GetPermissionsForUser(const nsACString& otherUser, + nsACString& aResult) { + nsCString str; + nsresult rv = GetFolderACL()->GetRightsStringForUser(otherUser, str); + NS_ENSURE_SUCCESS(rv, rv); + aResult = str; + return NS_OK; +} + +nsresult nsMsgIMAPFolderACL::GetRightsStringForUser( + const nsACString& inUserName, nsCString& rights) { + nsCString userName; + userName.Assign(inUserName); + if (userName.IsEmpty()) { + nsCOMPtr<nsIMsgIncomingServer> server; + + nsresult rv = m_folder->GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + // we need the real user name to match with what the imap server returns + // in the acl response. + server->GetUsername(userName); + } + ToLowerCase(userName); + rights = m_rightsHash.Get(userName); + return NS_OK; +} + +// First looks for individual user; then looks for 'anyone' if the user isn't +// found. Returns defaultIfNotFound, if neither are found. +bool nsMsgIMAPFolderACL::GetFlagSetInRightsForUser(const nsACString& userName, + char flag, + bool defaultIfNotFound) { + nsCString flags; + nsresult rv = GetRightsStringForUser(userName, flags); + NS_ENSURE_SUCCESS(rv, defaultIfNotFound); + if (flags.IsEmpty()) { + nsCString anyoneFlags; + GetRightsStringForUser(nsLiteralCString(IMAP_ACL_ANYONE_STRING), + anyoneFlags); + if (anyoneFlags.IsEmpty()) return defaultIfNotFound; + return (anyoneFlags.FindChar(flag) != kNotFound); + } + return (flags.FindChar(flag) != kNotFound); +} + +bool nsMsgIMAPFolderACL::GetCanUserLookupFolder(const nsACString& userName) { + return GetFlagSetInRightsForUser(userName, 'l', false); +} + +bool nsMsgIMAPFolderACL::GetCanUserReadFolder(const nsACString& userName) { + return GetFlagSetInRightsForUser(userName, 'r', false); +} + +bool nsMsgIMAPFolderACL::GetCanUserStoreSeenInFolder( + const nsACString& userName) { + return GetFlagSetInRightsForUser(userName, 's', false); +} + +bool nsMsgIMAPFolderACL::GetCanUserWriteFolder(const nsACString& userName) { + return GetFlagSetInRightsForUser(userName, 'w', false); +} + +bool nsMsgIMAPFolderACL::GetCanUserInsertInFolder(const nsACString& userName) { + return GetFlagSetInRightsForUser(userName, 'i', false); +} + +bool nsMsgIMAPFolderACL::GetCanUserPostToFolder(const nsACString& userName) { + return GetFlagSetInRightsForUser(userName, 'p', false); +} + +bool nsMsgIMAPFolderACL::GetCanUserCreateSubfolder(const nsACString& userName) { + return GetFlagSetInRightsForUser(userName, 'c', false); +} + +bool nsMsgIMAPFolderACL::GetCanUserDeleteInFolder(const nsACString& userName) { + return GetFlagSetInRightsForUser(userName, 'd', false) || + GetFlagSetInRightsForUser(userName, 't', false); +} + +bool nsMsgIMAPFolderACL::GetCanUserAdministerFolder( + const nsACString& userName) { + return GetFlagSetInRightsForUser(userName, 'a', false); +} + +bool nsMsgIMAPFolderACL::GetCanILookupFolder() { + return GetFlagSetInRightsForUser(EmptyCString(), 'l', true); +} + +bool nsMsgIMAPFolderACL::GetCanIReadFolder() { + return GetFlagSetInRightsForUser(EmptyCString(), 'r', true); +} + +bool nsMsgIMAPFolderACL::GetCanIStoreSeenInFolder() { + return GetFlagSetInRightsForUser(EmptyCString(), 's', true); +} + +bool nsMsgIMAPFolderACL::GetCanIWriteFolder() { + return GetFlagSetInRightsForUser(EmptyCString(), 'w', true); +} + +bool nsMsgIMAPFolderACL::GetCanIInsertInFolder() { + return GetFlagSetInRightsForUser(EmptyCString(), 'i', true); +} + +bool nsMsgIMAPFolderACL::GetCanIPostToFolder() { + return GetFlagSetInRightsForUser(EmptyCString(), 'p', true); +} + +bool nsMsgIMAPFolderACL::GetCanICreateSubfolder() { + return GetFlagSetInRightsForUser(EmptyCString(), 'c', true); +} + +bool nsMsgIMAPFolderACL::GetCanIDeleteInFolder() { + return GetFlagSetInRightsForUser(EmptyCString(), 'd', true) || + GetFlagSetInRightsForUser(EmptyCString(), 't', true); +} + +bool nsMsgIMAPFolderACL::GetCanIAdministerFolder() { + return GetFlagSetInRightsForUser(EmptyCString(), 'a', true); +} + +bool nsMsgIMAPFolderACL::GetCanIExpungeFolder() { + return GetFlagSetInRightsForUser(EmptyCString(), 'e', true) || + GetFlagSetInRightsForUser(EmptyCString(), 'd', true); +} + +// We use this to see if the ACLs think a folder is shared or not. +// We will define "Shared" in 5.0 to mean: +// At least one user other than the currently authenticated user has at least +// one explicitly-listed ACL right on that folder. +bool nsMsgIMAPFolderACL::GetIsFolderShared() { + // If we have more than one ACL count for this folder, which means that + // someone other than ourself has rights on it, then it is "shared." + if (m_aclCount > 1) return true; + + // Or, if "anyone" has rights to it, it is shared. + nsCString anyonesRights = + m_rightsHash.Get(nsLiteralCString(IMAP_ACL_ANYONE_STRING)); + return (!anyonesRights.IsEmpty()); +} + +bool nsMsgIMAPFolderACL::GetDoIHaveFullRightsForFolder() { + return (GetCanIReadFolder() && GetCanIWriteFolder() && + GetCanIInsertInFolder() && GetCanIAdministerFolder() && + GetCanICreateSubfolder() && GetCanIDeleteInFolder() && + GetCanILookupFolder() && GetCanIStoreSeenInFolder() && + GetCanIExpungeFolder() && GetCanIPostToFolder()); +} + +// Returns a newly allocated string describing these rights +nsresult nsMsgIMAPFolderACL::CreateACLRightsString(nsAString& aRightsString) { + nsString curRight; + nsCOMPtr<nsIStringBundle> bundle; + nsresult rv = IMAPGetStringBundle(getter_AddRefs(bundle)); + NS_ENSURE_SUCCESS(rv, rv); + + if (GetDoIHaveFullRightsForFolder()) { + nsAutoString result; + rv = bundle->GetStringFromName("imapAclFullRights", result); + aRightsString.Assign(result); + return rv; + } + + if (GetCanIReadFolder()) { + bundle->GetStringFromName("imapAclReadRight", curRight); + aRightsString.Append(curRight); + } + if (GetCanIWriteFolder()) { + if (!aRightsString.IsEmpty()) aRightsString.AppendLiteral(", "); + bundle->GetStringFromName("imapAclWriteRight", curRight); + aRightsString.Append(curRight); + } + if (GetCanIInsertInFolder()) { + if (!aRightsString.IsEmpty()) aRightsString.AppendLiteral(", "); + bundle->GetStringFromName("imapAclInsertRight", curRight); + aRightsString.Append(curRight); + } + if (GetCanILookupFolder()) { + if (!aRightsString.IsEmpty()) aRightsString.AppendLiteral(", "); + bundle->GetStringFromName("imapAclLookupRight", curRight); + aRightsString.Append(curRight); + } + if (GetCanIStoreSeenInFolder()) { + if (!aRightsString.IsEmpty()) aRightsString.AppendLiteral(", "); + bundle->GetStringFromName("imapAclSeenRight", curRight); + aRightsString.Append(curRight); + } + if (GetCanIDeleteInFolder()) { + if (!aRightsString.IsEmpty()) aRightsString.AppendLiteral(", "); + bundle->GetStringFromName("imapAclDeleteRight", curRight); + aRightsString.Append(curRight); + } + if (GetCanIExpungeFolder()) { + if (!aRightsString.IsEmpty()) aRightsString.AppendLiteral(", "); + bundle->GetStringFromName("imapAclExpungeRight", curRight); + aRightsString.Append(curRight); + } + if (GetCanICreateSubfolder()) { + if (!aRightsString.IsEmpty()) aRightsString.AppendLiteral(", "); + bundle->GetStringFromName("imapAclCreateRight", curRight); + aRightsString.Append(curRight); + } + if (GetCanIPostToFolder()) { + if (!aRightsString.IsEmpty()) aRightsString.AppendLiteral(", "); + bundle->GetStringFromName("imapAclPostRight", curRight); + aRightsString.Append(curRight); + } + if (GetCanIAdministerFolder()) { + if (!aRightsString.IsEmpty()) aRightsString.AppendLiteral(", "); + bundle->GetStringFromName("imapAclAdministerRight", curRight); + aRightsString.Append(curRight); + } + return rv; +} + +NS_IMETHODIMP nsImapMailFolder::GetFilePath(nsIFile** aPathName) { + // this will return a copy of mPath, which is what we want. + // this will also initialize mPath using parseURI if it isn't already done + return nsMsgDBFolder::GetFilePath(aPathName); +} + +NS_IMETHODIMP nsImapMailFolder::SetFilePath(nsIFile* aPathName) { + return nsMsgDBFolder::SetFilePath( + aPathName); // call base class so mPath will get set +} + +nsresult nsImapMailFolder::DisplayStatusMsg(nsIImapUrl* aImapUrl, + const nsAString& msg) { + nsCOMPtr<nsIImapMockChannel> mockChannel; + aImapUrl->GetMockChannel(getter_AddRefs(mockChannel)); + if (mockChannel) { + nsCOMPtr<nsIProgressEventSink> progressSink; + mockChannel->GetProgressEventSink(getter_AddRefs(progressSink)); + if (progressSink) { + progressSink->OnStatus(mockChannel, NS_OK, + PromiseFlatString(msg).get()); // XXX i18n message + } + } + return NS_OK; +} + +NS_IMETHODIMP +nsImapMailFolder::ProgressStatusString(nsIImapProtocol* aProtocol, + const char* aMsgName, + const char16_t* extraInfo) { + nsString progressMsg; + + nsCOMPtr<nsIMsgIncomingServer> server; + nsresult rv = GetServer(getter_AddRefs(server)); + if (NS_SUCCEEDED(rv) && server) { + nsCOMPtr<nsIImapServerSink> serverSink = do_QueryInterface(server); + if (serverSink) serverSink->GetImapStringByName(aMsgName, progressMsg); + } + if (progressMsg.IsEmpty()) + IMAPGetStringByName(aMsgName, getter_Copies(progressMsg)); + + if (aProtocol && !progressMsg.IsEmpty()) { + nsCOMPtr<nsIImapUrl> imapUrl; + aProtocol->GetRunningImapURL(getter_AddRefs(imapUrl)); + if (imapUrl) { + if (extraInfo) { + nsString printfString; + nsTextFormatter::ssprintf(printfString, progressMsg.get(), extraInfo); + progressMsg = printfString; + } + + DisplayStatusMsg(imapUrl, progressMsg); + } + } + return NS_OK; +} + +NS_IMETHODIMP +nsImapMailFolder::PercentProgress(nsIImapProtocol* aProtocol, + nsACString const& aFmtStringName, + nsAString const& aMailboxName, + int64_t aCurrentProgress, + int64_t aMaxProgress) { + if (aProtocol) { + nsCOMPtr<nsIImapUrl> imapUrl; + aProtocol->GetRunningImapURL(getter_AddRefs(imapUrl)); + if (imapUrl) { + nsCOMPtr<nsIImapMockChannel> mockChannel; + imapUrl->GetMockChannel(getter_AddRefs(mockChannel)); + if (mockChannel) { + nsCOMPtr<nsIProgressEventSink> progressSink; + mockChannel->GetProgressEventSink(getter_AddRefs(progressSink)); + if (progressSink) { + progressSink->OnProgress(mockChannel, aCurrentProgress, aMaxProgress); + + if (!aFmtStringName.IsEmpty()) { + // There's a progress message to format (the progress messages are + // all localized and expect three params). + nsAutoString current; + current.AppendInt(aCurrentProgress); + nsAutoString expected; + expected.AppendInt(aMaxProgress); + nsAutoString mailbox(aMailboxName); + AutoTArray<nsString, 3> params = {current, expected, mailbox}; + + nsCOMPtr<nsIStringBundle> bundle; + nsresult rv = IMAPGetStringBundle(getter_AddRefs(bundle)); + NS_ENSURE_SUCCESS(rv, rv); + + nsString progressText; + rv = bundle->FormatStringFromName( + PromiseFlatCString(aFmtStringName).get(), params, progressText); + NS_ENSURE_SUCCESS(rv, rv); + if (!progressText.IsEmpty()) { + progressSink->OnStatus(mockChannel, NS_OK, progressText.get()); + } + } + } + } + } + } + return NS_OK; +} + +NS_IMETHODIMP +nsImapMailFolder::CopyNextStreamMessage(bool copySucceeded, + nsISupports* copyState) { + // if copy has failed it could be either user interrupted it or for some other + // reason don't do any subsequent copies or delete src messages if it is move + if (!copySucceeded) return NS_OK; + nsresult rv; + nsCOMPtr<nsImapMailCopyState> mailCopyState = + do_QueryInterface(copyState, &rv); + if (NS_FAILED(rv)) { + MOZ_LOG(IMAP, mozilla::LogLevel::Info, + ("QI copyState failed: %" PRIx32, static_cast<uint32_t>(rv))); + return rv; // this can fail... + } + + if (!mailCopyState->m_streamCopy) return NS_OK; + + uint32_t idx = mailCopyState->m_curIndex; + if (mailCopyState->m_isMove && idx) { + nsCOMPtr<nsIMsgFolder> srcFolder( + do_QueryInterface(mailCopyState->m_srcSupport, &rv)); + if (NS_SUCCEEDED(rv) && srcFolder) { + // Create "array" of one message header to delete + idx--; + if (idx < mailCopyState->m_messages.Length()) { + RefPtr<nsIMsgDBHdr> msg = mailCopyState->m_messages[idx]; + srcFolder->DeleteMessages({msg}, nullptr, true, true, nullptr, false); + } + } + } + + if (mailCopyState->m_curIndex < mailCopyState->m_messages.Length()) { + MOZ_LOG(IMAP, mozilla::LogLevel::Info, + ("CopyNextStreamMessage: %s %u of %u", + mailCopyState->m_isMove ? "Moving" : "Copying", + mailCopyState->m_curIndex, + (uint32_t)mailCopyState->m_messages.Length())); + nsIMsgDBHdr* message = mailCopyState->m_messages[mailCopyState->m_curIndex]; + bool isRead; + message->GetIsRead(&isRead); + mailCopyState->m_unreadCount = (isRead) ? 0 : 1; + rv = CopyStreamMessage(message, this, mailCopyState->m_msgWindow, + mailCopyState->m_isMove); + } else { + // Notify of move/copy completion in case we have some source headers + nsCOMPtr<nsIMsgFolderNotificationService> notifier( + do_GetService("@mozilla.org/messenger/msgnotificationservice;1")); + if (notifier && !mailCopyState->m_messages.IsEmpty()) { + notifier->NotifyMsgsMoveCopyCompleted( + mailCopyState->m_isMove, mailCopyState->m_messages, this, {}); + } + if (mailCopyState->m_isMove) { + nsCOMPtr<nsIMsgFolder> srcFolder( + do_QueryInterface(mailCopyState->m_srcSupport, &rv)); + if (NS_SUCCEEDED(rv) && srcFolder) { + // we want to send this notification now that the source messages have + // been deleted. + nsCOMPtr<nsIMsgLocalMailFolder> popFolder(do_QueryInterface(srcFolder)); + if (popFolder) // needed if move pop->imap to notify FE + srcFolder->NotifyFolderEvent(kDeleteOrMoveMsgCompleted); + } + } + } + if (NS_FAILED(rv)) (void)OnCopyCompleted(mailCopyState->m_srcSupport, rv); + + return rv; +} + +NS_IMETHODIMP +nsImapMailFolder::SetUrlState(nsIImapProtocol* aProtocol, + nsIMsgMailNewsUrl* aUrl, bool isRunning, + bool aSuspend, nsresult statusCode) { + // If we have no path, then the folder has been shutdown, and there's + // no point in doing anything... + if (!mPath) return NS_OK; + if (!isRunning) { + ProgressStatusString(aProtocol, "imapDone", nullptr); + m_urlRunning = false; + // if no protocol, then we're reading from the mem or disk cache + // and we don't want to end the offline download just yet. + if (aProtocol) { + EndOfflineDownload(); + m_downloadingFolderForOfflineUse = false; + } + nsCOMPtr<nsIImapUrl> imapUrl(do_QueryInterface(aUrl)); + if (imapUrl) { + nsImapAction imapAction; + imapUrl->GetImapAction(&imapAction); + // if the server doesn't support copyUID, then SetCopyResponseUid won't + // get called, so we need to clear m_pendingOfflineMoves when the online + // move operation has finished. + if (imapAction == nsIImapUrl::nsImapOnlineMove) + m_pendingOfflineMoves.Clear(); + } + } + if (aUrl && !aSuspend) return aUrl->SetUrlState(isRunning, statusCode); + return statusCode; +} + +// used when copying from local mail folder, or other imap server) +nsresult nsImapMailFolder::CopyMessagesWithStream( + nsIMsgFolder* srcFolder, nsTArray<RefPtr<nsIMsgDBHdr>> const& messages, + bool isMove, bool isCrossServerOp, nsIMsgWindow* msgWindow, + nsIMsgCopyServiceListener* listener, bool allowUndo) { + NS_ENSURE_ARG_POINTER(srcFolder); + nsresult rv; + rv = InitCopyState(srcFolder, messages, isMove, false, isCrossServerOp, 0, + EmptyCString(), listener, msgWindow, allowUndo); + if (NS_FAILED(rv)) return rv; + + m_copyState->m_streamCopy = true; + + // ** jt - needs to create server to server move/copy undo msg txn + if (m_copyState->m_allowUndo) { + nsAutoCString messageIds; + nsTArray<nsMsgKey> srcKeyArray; + rv = BuildIdsAndKeyArray(messages, messageIds, srcKeyArray); + + RefPtr<nsImapMoveCopyMsgTxn> undoMsgTxn = new nsImapMoveCopyMsgTxn; + + if (!undoMsgTxn || + NS_FAILED(undoMsgTxn->Init(srcFolder, &srcKeyArray, messageIds.get(), + this, true, isMove))) + return NS_ERROR_OUT_OF_MEMORY; + + if (isMove) { + if (mFlags & nsMsgFolderFlags::Trash) + undoMsgTxn->SetTransactionType(nsIMessenger::eDeleteMsg); + else + undoMsgTxn->SetTransactionType(nsIMessenger::eMoveMsg); + } else + undoMsgTxn->SetTransactionType(nsIMessenger::eCopyMsg); + m_copyState->m_undoMsgTxn = undoMsgTxn; + } + if (NS_SUCCEEDED(rv)) CopyStreamMessage(messages[0], this, msgWindow, isMove); + return rv; // we are clearing copy state in CopyMessages on failure +} + +nsresult nsImapMailFolder::GetClearedOriginalOp( + nsIMsgOfflineImapOperation* op, nsIMsgOfflineImapOperation** originalOp, + nsIMsgDatabase** originalDB) { + nsCOMPtr<nsIMsgOfflineImapOperation> returnOp; + nsOfflineImapOperationType opType; + op->GetOperation(&opType); + NS_ASSERTION(opType & nsIMsgOfflineImapOperation::kMoveResult, + "not an offline move op"); + + nsCString sourceFolderURI; + op->GetSourceFolderURI(sourceFolderURI); + + nsresult rv; + nsCOMPtr<nsIMsgFolder> sourceFolder; + rv = GetOrCreateFolder(sourceFolderURI, getter_AddRefs(sourceFolder)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIDBFolderInfo> folderInfo; + sourceFolder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), originalDB); + if (*originalDB) { + nsCOMPtr<nsIMsgOfflineOpsDatabase> opsDb = + do_QueryInterface(*originalDB, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsMsgKey originalKey; + op->GetMessageKey(&originalKey); + rv = + opsDb->GetOfflineOpForKey(originalKey, false, getter_AddRefs(returnOp)); + if (NS_SUCCEEDED(rv) && returnOp) { + nsCString moveDestination; + nsCString thisFolderURI; + GetURI(thisFolderURI); + returnOp->GetDestinationFolderURI(moveDestination); + if (moveDestination.Equals(thisFolderURI)) + returnOp->ClearOperation(nsIMsgOfflineImapOperation::kMoveResult); + } + } + returnOp.forget(originalOp); + return rv; +} + +nsresult nsImapMailFolder::GetOriginalOp( + nsIMsgOfflineImapOperation* op, nsIMsgOfflineImapOperation** originalOp, + nsIMsgDatabase** originalDB) { + nsCOMPtr<nsIMsgOfflineImapOperation> returnOp; + nsCString sourceFolderURI; + op->GetSourceFolderURI(sourceFolderURI); + + nsresult rv; + nsCOMPtr<nsIMsgFolder> sourceFolder; + rv = GetOrCreateFolder(sourceFolderURI, getter_AddRefs(sourceFolder)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIDBFolderInfo> folderInfo; + sourceFolder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), originalDB); + if (*originalDB) { + nsCOMPtr<nsIMsgOfflineOpsDatabase> opsDb = + do_QueryInterface(*originalDB, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsMsgKey originalKey; + op->GetMessageKey(&originalKey); + rv = + opsDb->GetOfflineOpForKey(originalKey, false, getter_AddRefs(returnOp)); + } + returnOp.forget(originalOp); + return rv; +} + +nsresult nsImapMailFolder::FindOpenRange(nsMsgKey& fakeBase, + uint32_t srcCount) { + nsresult rv = GetDatabase(); + NS_ENSURE_SUCCESS(rv, rv); + + nsMsgKey newBase = fakeBase - 1; + uint32_t freeCount = 0; + while (freeCount != srcCount && newBase > 0) { + bool containsKey; + if (NS_SUCCEEDED(mDatabase->ContainsKey(newBase, &containsKey)) && + !containsKey) + freeCount++; + else + freeCount = 0; + newBase--; + } + if (!newBase) return NS_ERROR_FAILURE; + fakeBase = newBase; + return NS_OK; +} + +// Helper to synchronously copy a message from one msgStore to another. +static nsresult CopyStoreMessage(nsIMsgDBHdr* srcHdr, nsIMsgDBHdr* destHdr, + uint64_t& bytesCopied) { + nsresult rv; + + // Boilerplate setup. + nsCOMPtr<nsIMsgFolder> srcFolder; + rv = srcHdr->GetFolder(getter_AddRefs(srcFolder)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgFolder> destFolder; + rv = destHdr->GetFolder(getter_AddRefs(destFolder)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgPluggableStore> destStore; + rv = destFolder->GetMsgStore(getter_AddRefs(destStore)); + NS_ENSURE_SUCCESS(rv, rv); + + // Copy message into the msgStore. + nsCOMPtr<nsIInputStream> srcStream; + rv = srcFolder->GetLocalMsgStream(srcHdr, getter_AddRefs(srcStream)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIOutputStream> destStream; + rv = destFolder->GetOfflineStoreOutputStream(destHdr, + getter_AddRefs(destStream)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = SyncCopyStream(srcStream, destStream, bytesCopied); + if (NS_SUCCEEDED(rv)) { + rv = destStore->FinishNewMessage(destStream, destHdr); + } else { + destStore->DiscardNewMessage(destStream, destHdr); + } + return rv; +} + +// This imap folder is the destination of an offline move/copy. +// We are either offline, or doing a pseudo-offline delete (where we do an +// offline delete, load the next message, then playback the offline delete). +nsresult nsImapMailFolder::CopyMessagesOffline( + nsIMsgFolder* srcFolder, nsTArray<RefPtr<nsIMsgDBHdr>> const& messages, + bool isMove, nsIMsgWindow* msgWindow, nsIMsgCopyServiceListener* listener) { + nsresult rv; + nsresult stopit = NS_OK; + nsCOMPtr<nsIMsgDatabase> sourceMailDB; + nsCOMPtr<nsIDBFolderInfo> srcDbFolderInfo; + srcFolder->GetDBFolderInfoAndDB(getter_AddRefs(srcDbFolderInfo), + getter_AddRefs(sourceMailDB)); + bool deleteToTrash = false; + bool deleteImmediately = false; + uint32_t srcCount = messages.Length(); + nsCOMPtr<nsIImapIncomingServer> imapServer; + rv = GetImapIncomingServer(getter_AddRefs(imapServer)); + + nsTArray<RefPtr<nsIMsgDBHdr>> msgHdrsCopied; + nsTArray<RefPtr<nsIMsgDBHdr>> destMsgHdrs; + + if (NS_SUCCEEDED(rv) && imapServer) { + nsMsgImapDeleteModel deleteModel; + imapServer->GetDeleteModel(&deleteModel); + deleteToTrash = (deleteModel == nsMsgImapDeleteModels::MoveToTrash); + deleteImmediately = (deleteModel == nsMsgImapDeleteModels::DeleteNoTrash); + } + + // This array is used only when we are actually removing the messages from the + // source database. + nsTArray<nsMsgKey> keysToDelete( + (isMove && (deleteToTrash || deleteImmediately)) ? srcCount : 0); + + if (sourceMailDB) { + // save the future ops in the source DB, if this is not a imap->local + // copy/move + nsCOMPtr<nsITransactionManager> txnMgr; + if (msgWindow) msgWindow->GetTransactionManager(getter_AddRefs(txnMgr)); + if (txnMgr) txnMgr->BeginBatch(nullptr); + nsCOMPtr<nsIMsgDatabase> database; + GetMsgDatabase(getter_AddRefs(database)); + if (database) { + // get the highest key in the dest db, so we can make up our fake keys + nsMsgKey fakeBase = 1; + nsCOMPtr<nsIDBFolderInfo> folderInfo; + rv = database->GetDBFolderInfo(getter_AddRefs(folderInfo)); + NS_ENSURE_SUCCESS(rv, rv); + nsMsgKey highWaterMark = nsMsgKey_None; + folderInfo->GetHighWater(&highWaterMark); + fakeBase += highWaterMark; + nsMsgKey fakeTop = fakeBase + srcCount; + // Check that we have enough room for the fake headers. If fakeTop + // is <= highWaterMark, we've overflowed. + if (fakeTop <= highWaterMark || fakeTop == nsMsgKey_None) { + rv = FindOpenRange(fakeBase, srcCount); + NS_ENSURE_SUCCESS(rv, rv); + } + // N.B. We must not return out of the for loop - we need the matching + // end notifications to be sent. + // We don't need to acquire the semaphor since this is synchronous + // on the UI thread but we should check if the offline store is locked. + bool isLocked; + GetLocked(&isLocked); + nsTArray<nsMsgKey> addedKeys; + nsTArray<nsMsgKey> srcKeyArray; + nsCOMArray<nsIMsgDBHdr> addedHdrs; + nsCOMArray<nsIMsgDBHdr> srcMsgs; + nsOfflineImapOperationType moveCopyOpType; + nsOfflineImapOperationType deleteOpType = + nsIMsgOfflineImapOperation::kDeletedMsg; + if (!deleteToTrash) + deleteOpType = nsIMsgOfflineImapOperation::kMsgMarkedDeleted; + nsCString messageIds; + rv = BuildIdsAndKeyArray(messages, messageIds, srcKeyArray); + // put fake message in destination db, delete source if move + EnableNotifications(nsIMsgFolder::allMessageCountNotifications, false); + nsCString originalSrcFolderURI; + srcFolder->GetURI(originalSrcFolderURI); + nsCOMPtr<nsIMsgOfflineOpsDatabase> opsDb = + do_QueryInterface(sourceMailDB, &rv); + NS_ENSURE_SUCCESS(rv, rv); + for (uint32_t sourceKeyIndex = 0; + NS_SUCCEEDED(stopit) && (sourceKeyIndex < srcCount); + sourceKeyIndex++) { + bool messageReturningHome = false; + RefPtr<nsIMsgDBHdr> message = messages[sourceKeyIndex]; + nsMsgKey originalKey; + if (message) { + rv = message->GetMessageKey(&originalKey); + } else { + NS_ERROR("bad msg in src array"); + continue; + } + nsCOMPtr<nsIMsgOfflineImapOperation> sourceOp; + rv = opsDb->GetOfflineOpForKey(originalKey, true, + getter_AddRefs(sourceOp)); + if (NS_SUCCEEDED(rv) && sourceOp) { + srcFolder->SetFlag(nsMsgFolderFlags::OfflineEvents); + nsCOMPtr<nsIMsgDatabase> originalDB; + nsOfflineImapOperationType opType; + sourceOp->GetOperation(&opType); + // if we already have an offline op for this key, then we need to see + // if it was moved into the source folder while offline + if (opType == + nsIMsgOfflineImapOperation::kMoveResult) // offline move + { + // gracious me, we are moving something we already moved while + // offline! find the original operation and clear it! + nsCOMPtr<nsIMsgOfflineImapOperation> originalOp; + GetClearedOriginalOp(sourceOp, getter_AddRefs(originalOp), + getter_AddRefs(originalDB)); + nsCOMPtr<nsIMsgOfflineOpsDatabase> opsDbOriginal = + do_QueryInterface(originalDB, &rv); + if (NS_SUCCEEDED(rv) && originalOp) { + nsCString srcFolderURI; + srcFolder->GetURI(srcFolderURI); + sourceOp->GetSourceFolderURI(originalSrcFolderURI); + sourceOp->GetMessageKey(&originalKey); + if (isMove) opsDb->RemoveOfflineOp(sourceOp); + sourceOp = originalOp; + if (originalSrcFolderURI.Equals(srcFolderURI)) { + messageReturningHome = true; + opsDbOriginal->RemoveOfflineOp(originalOp); + } + } + } + if (!messageReturningHome) { + nsCString folderURI; + GetURI(folderURI); + if (isMove) { + uint32_t msgSize; + uint32_t msgFlags; + imapMessageFlagsType newImapFlags = 0; + message->GetMessageSize(&msgSize); + message->GetFlags(&msgFlags); + sourceOp->SetDestinationFolderURI(folderURI); // offline move + sourceOp->SetOperation(nsIMsgOfflineImapOperation::kMsgMoved); + sourceOp->SetMsgSize(msgSize); + newImapFlags = msgFlags & 0x7; + if (msgFlags & nsMsgMessageFlags::Forwarded) + newImapFlags |= kImapMsgForwardedFlag; + sourceOp->SetNewFlags(newImapFlags); + } else { + sourceOp->AddMessageCopyOperation(folderURI); // offline copy + } + + sourceOp->GetOperation(&moveCopyOpType); + srcMsgs.AppendObject(message); + } + } else { + stopit = NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIMsgDBHdr> mailHdr; + rv = + sourceMailDB->GetMsgHdrForKey(originalKey, getter_AddRefs(mailHdr)); + + if (NS_SUCCEEDED(rv) && mailHdr) { + // Copy the DB hdr into the destination folder. + bool successfulCopy = false; + nsMsgKey srcDBhighWaterMark; + srcDbFolderInfo->GetHighWater(&srcDBhighWaterMark); + + nsCOMPtr<nsIMsgDBHdr> newMailHdr; + rv = database->CopyHdrFromExistingHdr(fakeBase + sourceKeyIndex, + mailHdr, true, + getter_AddRefs(newMailHdr)); + if (!newMailHdr || NS_FAILED(rv)) { + NS_ASSERTION(false, "failed to copy hdr"); + stopit = rv; + } + + if (NS_SUCCEEDED(stopit)) { + bool hasMsgOffline = false; + + destMsgHdrs.AppendElement(newMailHdr); + srcFolder->HasMsgOffline(originalKey, &hasMsgOffline); + newMailHdr->SetUint32Property("pseudoHdr", 1); + + if (hasMsgOffline && !isLocked) { + uint64_t bytesCopied; + stopit = CopyStoreMessage(mailHdr, newMailHdr, bytesCopied); + if (NS_SUCCEEDED(stopit)) { + uint32_t unused; + newMailHdr->OrFlags(nsMsgMessageFlags::Offline, &unused); + newMailHdr->SetOfflineMessageSize(bytesCopied); + } + } else { + database->MarkOffline(fakeBase + sourceKeyIndex, false, nullptr); + } + + nsCOMPtr<nsIMsgOfflineOpsDatabase> opsDb = + do_QueryInterface(database, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgOfflineImapOperation> destOp; + opsDb->GetOfflineOpForKey(fakeBase + sourceKeyIndex, true, + getter_AddRefs(destOp)); + if (destOp) { + // check if this is a move back to the original mailbox, in which + // case we just delete the offline operation. + if (messageReturningHome) { + opsDb->RemoveOfflineOp(destOp); + } else { + SetFlag(nsMsgFolderFlags::OfflineEvents); + destOp->SetSourceFolderURI(originalSrcFolderURI); + destOp->SetSrcMessageKey(originalKey); + addedKeys.AppendElement(fakeBase + sourceKeyIndex); + addedHdrs.AppendObject(newMailHdr); + } + } else { + stopit = NS_ERROR_FAILURE; + } + } + successfulCopy = NS_SUCCEEDED(stopit); + nsMsgKey msgKey; + mailHdr->GetMessageKey(&msgKey); + if (isMove && successfulCopy) { + if (deleteToTrash || deleteImmediately) + keysToDelete.AppendElement(msgKey); + else + sourceMailDB->MarkImapDeleted(msgKey, true, + nullptr); // offline delete + } + if (successfulCopy) { + // This is for both moves and copies + msgHdrsCopied.AppendElement(mailHdr); + } + } + } // End message loop. + EnableNotifications(nsIMsgFolder::allMessageCountNotifications, true); + RefPtr<nsImapOfflineTxn> addHdrMsgTxn = new nsImapOfflineTxn( + this, &addedKeys, nullptr, this, isMove, + nsIMsgOfflineImapOperation::kAddedHeader, addedHdrs); + if (addHdrMsgTxn && txnMgr) txnMgr->DoTransaction(addHdrMsgTxn); + RefPtr<nsImapOfflineTxn> undoMsgTxn = + new nsImapOfflineTxn(srcFolder, &srcKeyArray, messageIds.get(), this, + isMove, moveCopyOpType, srcMsgs); + if (undoMsgTxn) { + if (isMove) { + undoMsgTxn->SetTransactionType(nsIMessenger::eMoveMsg); + nsCOMPtr<nsIMsgImapMailFolder> srcIsImap( + do_QueryInterface(srcFolder)); + // remember this undo transaction so we can hook up the result + // msg ids in the undo transaction. + if (srcIsImap) { + nsImapMailFolder* srcImapFolder = + static_cast<nsImapMailFolder*>(srcFolder); + srcImapFolder->m_pendingOfflineMoves.AppendElement(undoMsgTxn); + } + } else { + undoMsgTxn->SetTransactionType(nsIMessenger::eCopyMsg); + } + // we're adding this undo action before the delete is successful. This + // is evil, but 4.5 did it as well. + if (txnMgr) txnMgr->DoTransaction(undoMsgTxn); + } + undoMsgTxn = + new nsImapOfflineTxn(srcFolder, &srcKeyArray, messageIds.get(), this, + isMove, deleteOpType, srcMsgs); + if (undoMsgTxn) { + if (isMove) { + if (mFlags & nsMsgFolderFlags::Trash) { + undoMsgTxn->SetTransactionType(nsIMessenger::eDeleteMsg); + } else { + undoMsgTxn->SetTransactionType(nsIMessenger::eMoveMsg); + } + } else { + undoMsgTxn->SetTransactionType(nsIMessenger::eCopyMsg); + } + if (txnMgr) txnMgr->DoTransaction(undoMsgTxn); + } + + if (isMove) sourceMailDB->Commit(nsMsgDBCommitType::kLargeCommit); + database->Commit(nsMsgDBCommitType::kLargeCommit); + SummaryChanged(); + srcFolder->SummaryChanged(); + } + if (txnMgr) txnMgr->EndBatch(false); + } + + if (!msgHdrsCopied.IsEmpty()) { + nsCOMPtr<nsIMsgFolderNotificationService> notifier( + do_GetService("@mozilla.org/messenger/msgnotificationservice;1")); + if (notifier) { + notifier->NotifyMsgsMoveCopyCompleted(isMove, msgHdrsCopied, this, + destMsgHdrs); + } + } + + // NOTE (Bug 1787963): + // If we're performing a move, by rights we should be deleting the source + // message(s) here. But that would mean they won't be available when we try + // to run the offline move operation once we're back online. So we'll just + // leave things as they are: + // - the message(s) copied into the destination folder + // - the original message(s) left in the source folder + // - the offline move operation all queued up for when we go back online + // When we do go back online, the offline move op will be performed and + // the source message(s) will be deleted. For real. + // Would be nice to have some marker to hide or grey out messages which are + // in this state of impending doom... but it's a pretty obscure corner case + // and we've already got quite enough of those. + // + // BUT... CopyMessagesOffline() is also used when online (ha!), *if* we're + // copying between folders on the same nsIMsgIncomingServer, in order to + // support undo. In that case we _do_ want to go ahead with the delete now. + + bool sameServer; + rv = IsOnSameServer(srcFolder, this, &sameServer); + + if (NS_SUCCEEDED(rv) && sameServer && isMove && + (deleteToTrash || deleteImmediately)) { + DeleteStoreMessages(keysToDelete, srcFolder); + srcFolder->EnableNotifications(nsIMsgFolder::allMessageCountNotifications, + false); + sourceMailDB->DeleteMessages(keysToDelete, nullptr); + srcFolder->EnableNotifications(nsIMsgFolder::allMessageCountNotifications, + true); + } + nsCOMPtr<nsISupports> srcSupport = do_QueryInterface(srcFolder); + OnCopyCompleted(srcSupport, rv); + + if (isMove) { + srcFolder->NotifyFolderEvent(NS_SUCCEEDED(rv) ? kDeleteOrMoveMsgCompleted + : kDeleteOrMoveMsgFailed); + } + return rv; +} + +void nsImapMailFolder::SetPendingAttributes( + const nsTArray<RefPtr<nsIMsgDBHdr>>& messages, bool aIsMove, + bool aSetOffline) { + GetDatabase(); + if (!mDatabase) return; + + uint32_t supportedUserFlags; + GetSupportedUserFlags(&supportedUserFlags); + + nsresult rv; + nsCOMPtr<nsIPrefBranch> prefBranch( + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS_VOID(rv); + + nsCString dontPreserve; + + // These preferences exist so that extensions can control which properties + // are preserved in the database when a message is moved or copied. All + // properties are preserved except those listed in these preferences + if (aIsMove) + prefBranch->GetCharPref("mailnews.database.summary.dontPreserveOnMove", + dontPreserve); + else + prefBranch->GetCharPref("mailnews.database.summary.dontPreserveOnCopy", + dontPreserve); + + // We'll add spaces at beginning and end so we can search for space-name-space + nsCString dontPreserveEx(" "_ns); + dontPreserveEx.Append(dontPreserve); + dontPreserveEx.Append(' '); + + // these properties are set as integers below, so don't set them again + // in the iteration through the properties + dontPreserveEx.AppendLiteral( + "offlineMsgSize msgOffset flags priority pseudoHdr "); + + // these fields are either copied separately when the server does not support + // custom IMAP flags, or managed directly through the flags + dontPreserveEx.AppendLiteral("keywords label "); + + // check if any msg hdr has special flags or properties set + // that we need to set on the dest hdr + for (auto msgDBHdr : messages) { + if (!(supportedUserFlags & kImapMsgSupportUserFlag)) { + nsCString keywords; + msgDBHdr->GetStringProperty("keywords", keywords); + if (!keywords.IsEmpty()) + mDatabase->SetAttributeOnPendingHdr(msgDBHdr, "keywords", + keywords.get()); + } + + nsTArray<nsCString> properties; + nsresult rv = msgDBHdr->GetProperties(properties); + NS_ENSURE_SUCCESS_VOID(rv); + + nsCString sourceString; + for (auto property : properties) { + nsAutoCString propertyEx(" "_ns); + propertyEx.Append(property); + propertyEx.Append(' '); + if (dontPreserveEx.Find(propertyEx) != kNotFound) continue; + + nsCString sourceString; + msgDBHdr->GetStringProperty(property.get(), sourceString); + mDatabase->SetAttributeOnPendingHdr(msgDBHdr, property.get(), + sourceString.get()); + } + + // Carry over HasRe flag. + uint32_t flags; + uint32_t storeFlags = 0; + msgDBHdr->GetFlags(&flags); + if (flags & nsMsgMessageFlags::HasRe) { + storeFlags = nsMsgMessageFlags::HasRe; + mDatabase->SetUint32AttributeOnPendingHdr(msgDBHdr, "flags", storeFlags); + } + + uint32_t messageSize; + uint64_t messageOffset; + nsCString storeToken; + msgDBHdr->GetMessageOffset(&messageOffset); + msgDBHdr->GetOfflineMessageSize(&messageSize); + msgDBHdr->GetStringProperty("storeToken", storeToken); + if (messageSize) { + mDatabase->SetUint32AttributeOnPendingHdr(msgDBHdr, "offlineMsgSize", + messageSize); + mDatabase->SetUint64AttributeOnPendingHdr(msgDBHdr, "msgOffset", + messageOffset); + // Not always setting "flags" attribute to nsMsgMessageFlags::Offline + // here because it can cause missing parts (inline or attachments) + // when messages are moved or copied manually or by filter action. + if (aSetOffline) + mDatabase->SetUint32AttributeOnPendingHdr( + msgDBHdr, "flags", storeFlags | nsMsgMessageFlags::Offline); + mDatabase->SetAttributeOnPendingHdr(msgDBHdr, "storeToken", + storeToken.get()); + } + nsMsgPriorityValue priority; + msgDBHdr->GetPriority(&priority); + if (priority != 0) { + nsAutoCString priorityStr; + priorityStr.AppendInt(priority); + mDatabase->SetAttributeOnPendingHdr(msgDBHdr, "priority", + priorityStr.get()); + } + } +} + +NS_IMETHODIMP +nsImapMailFolder::CopyMessages( + nsIMsgFolder* srcFolder, nsTArray<RefPtr<nsIMsgDBHdr>> const& messages, + bool isMove, nsIMsgWindow* msgWindow, nsIMsgCopyServiceListener* listener, + bool isFolder, // isFolder for future use when we do cross-server folder + // move/copy + bool allowUndo) { + UpdateTimestamps(allowUndo); + + nsresult rv; + nsCOMPtr<nsISupports> srcSupport = do_QueryInterface(srcFolder); + + bool sameServer; + rv = IsOnSameServer(srcFolder, this, &sameServer); + if (NS_FAILED(rv)) goto done; + + // in theory, if allowUndo is true, then this is a user initiated + // action, and we should do it pseudo-offline. If it's not + // user initiated (e.g., mail filters firing), then allowUndo is + // false, and we should just do the action. + if (!WeAreOffline() && sameServer && allowUndo) { + // complete the copy operation as in offline mode + rv = CopyMessagesOffline(srcFolder, messages, isMove, msgWindow, listener); + + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "error offline copy"); + // We'll warn if this fails, but we should still try to play back + // offline ops, because it's possible the copy got far enough to + // create the offline ops. + + // We make sure that the source folder is an imap folder by limiting + // pseudo-offline operations to the same imap server. If we extend the code + // to cover non imap folders in the future (i.e. imap folder->local folder), + // then the following downcast will cause either a crash or compiler error. + // Do not forget to change it accordingly. + nsImapMailFolder* srcImapFolder = static_cast<nsImapMailFolder*>(srcFolder); + + // if there is no pending request, create a new one, and set the timer. + // Otherwise use the existing one to reset the timer. it is callback + // function's responsibility to delete the new request object + if (!srcImapFolder->m_pendingPlaybackReq) { + srcImapFolder->m_pendingPlaybackReq = + new nsPlaybackRequest(srcImapFolder, msgWindow); + } + + // Create and start a new playback one-shot timer. If there is already a + // timer created that has not timed out, cancel it. + if (srcImapFolder->m_playbackTimer) + srcImapFolder->m_playbackTimer->Cancel(); + rv = NS_NewTimerWithFuncCallback( + getter_AddRefs(srcImapFolder->m_playbackTimer), PlaybackTimerCallback, + (void*)srcImapFolder->m_pendingPlaybackReq, + PLAYBACK_TIMER_INTERVAL_IN_MS, nsITimer::TYPE_ONE_SHOT, + "nsImapMailFolder::PlaybackTimerCallback", nullptr); + if (NS_FAILED(rv)) { + NS_WARNING("Could not start m_playbackTimer timer"); + } + return rv; + } else { + // sort the message array by key + + nsTArray<nsMsgKey> keyArray(messages.Length()); + for (nsIMsgDBHdr* aMessage : messages) { + if (!aMessage) { + continue; + } + nsMsgKey key; + aMessage->GetMessageKey(&key); + keyArray.AppendElement(key); + } + keyArray.Sort(); + + nsTArray<RefPtr<nsIMsgDBHdr>> sortedMsgs; + rv = MessagesInKeyOrder(keyArray, srcFolder, sortedMsgs); + NS_ENSURE_SUCCESS(rv, rv); + + if (WeAreOffline()) + return CopyMessagesOffline(srcFolder, sortedMsgs, isMove, msgWindow, + listener); + + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // 3rd parameter: Do not set offline flag. + SetPendingAttributes(sortedMsgs, isMove, false); + + // if the folders aren't on the same server, do a stream base copy + if (!sameServer) { + rv = CopyMessagesWithStream(srcFolder, sortedMsgs, isMove, true, + msgWindow, listener, allowUndo); + goto done; + } + + nsAutoCString messageIds; + rv = AllocateUidStringFromKeys(keyArray, messageIds); + if (NS_FAILED(rv)) goto done; + + nsCOMPtr<nsIUrlListener> urlListener; + rv = + QueryInterface(NS_GET_IID(nsIUrlListener), getter_AddRefs(urlListener)); + rv = InitCopyState(srcSupport, sortedMsgs, isMove, true, false, 0, + EmptyCString(), listener, msgWindow, allowUndo); + if (NS_FAILED(rv)) goto done; + + m_copyState->m_curIndex = m_copyState->m_messages.Length(); + + if (isMove) + srcFolder->EnableNotifications( + allMessageCountNotifications, + false); // disable message count notification + + nsCOMPtr<nsIURI> resultUrl; + nsCOMPtr<nsISupports> copySupport = do_QueryInterface(m_copyState); + rv = imapService->OnlineMessageCopy( + srcFolder, messageIds, this, true, isMove, urlListener, + getter_AddRefs(resultUrl), copySupport, msgWindow); + if (NS_SUCCEEDED(rv) && m_copyState->m_allowUndo) { + RefPtr<nsImapMoveCopyMsgTxn> undoMsgTxn = new nsImapMoveCopyMsgTxn; + if (!undoMsgTxn || + NS_FAILED(undoMsgTxn->Init(srcFolder, &keyArray, messageIds.get(), + this, true, isMove))) + return NS_ERROR_OUT_OF_MEMORY; + + if (isMove) { + if (mFlags & nsMsgFolderFlags::Trash) + undoMsgTxn->SetTransactionType(nsIMessenger::eDeleteMsg); + else + undoMsgTxn->SetTransactionType(nsIMessenger::eMoveMsg); + } else + undoMsgTxn->SetTransactionType(nsIMessenger::eCopyMsg); + m_copyState->m_undoMsgTxn = undoMsgTxn; + } + + } // endif + +done: + if (NS_FAILED(rv)) { + (void)OnCopyCompleted(srcSupport, rv); + if (isMove) { + srcFolder->EnableNotifications( + allMessageCountNotifications, + true); // enable message count notification + NotifyFolderEvent(kDeleteOrMoveMsgFailed); + } + } + return rv; +} + +// This is used when copying an imap or local/pop3 folder to an imap server. +// It does not allow completely moving an imap or local/pop3 folder to an imap +// server since only the messages can be moved between servers. +class nsImapFolderCopyState final : public nsIUrlListener, + public nsIMsgCopyServiceListener { + public: + nsImapFolderCopyState(nsIMsgFolder* destParent, nsIMsgFolder* srcFolder, + bool isMoveMessages, nsIMsgWindow* msgWindow, + nsIMsgCopyServiceListener* listener); + + NS_DECL_ISUPPORTS + NS_DECL_NSIURLLISTENER + NS_DECL_NSIMSGCOPYSERVICELISTENER + + nsresult StartNextCopy(); + nsresult AdvanceToNextFolder(nsresult aStatus); + + protected: + ~nsImapFolderCopyState(); + RefPtr<nsImapMailFolder> m_newDestFolder; + nsCOMPtr<nsISupports> m_origSrcFolder; + nsCOMPtr<nsIMsgFolder> m_curDestParent; + nsCOMPtr<nsIMsgFolder> m_curSrcFolder; + bool m_isMoveMessages; + nsCOMPtr<nsIMsgCopyServiceListener> m_copySrvcListener; + nsCOMPtr<nsIMsgWindow> m_msgWindow; + int32_t m_childIndex; + nsCOMArray<nsIMsgFolder> m_srcChildFolders; + nsCOMArray<nsIMsgFolder> m_destParents; +}; + +NS_IMPL_ISUPPORTS(nsImapFolderCopyState, nsIUrlListener, + nsIMsgCopyServiceListener) + +nsImapFolderCopyState::nsImapFolderCopyState( + nsIMsgFolder* destParent, nsIMsgFolder* srcFolder, bool isMoveMessages, + nsIMsgWindow* msgWindow, nsIMsgCopyServiceListener* listener) { + m_origSrcFolder = do_QueryInterface(srcFolder); + m_curDestParent = destParent; + m_curSrcFolder = srcFolder; + m_isMoveMessages = isMoveMessages; + m_msgWindow = msgWindow; + m_copySrvcListener = listener; + m_childIndex = -1; + // NOTE: The nsImapMailFolder doesn't keep a reference to us, so we're + // relying on our use as a listener by nsImapService and nsMsgCopyService + // to keep our refcount from zeroing! + // Might be safer to add a kungfudeathgrip on ourselves for the duration + // of the operation? Would need to make sure we catch all error conditions. +} + +nsImapFolderCopyState::~nsImapFolderCopyState() {} + +nsresult nsImapFolderCopyState::StartNextCopy() { + nsresult rv; + // Create the destination folder (our OnStopRunningUrl() will be called + // when done). + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsString folderName; + m_curSrcFolder->GetName(folderName); + return imapService->EnsureFolderExists(m_curDestParent, folderName, + m_msgWindow, this); +} + +nsresult nsImapFolderCopyState::AdvanceToNextFolder(nsresult aStatus) { + nsresult rv = NS_OK; + m_childIndex++; + if (m_childIndex >= m_srcChildFolders.Count()) { + if (m_newDestFolder) + m_newDestFolder->OnCopyCompleted(m_origSrcFolder, aStatus); + } else { + m_curDestParent = m_destParents[m_childIndex]; + m_curSrcFolder = m_srcChildFolders[m_childIndex]; + rv = StartNextCopy(); + } + return rv; +} + +NS_IMETHODIMP +nsImapFolderCopyState::OnStartRunningUrl(nsIURI* aUrl) { + NS_ASSERTION(aUrl, "sanity check - need to be be running non-null url"); + return NS_OK; +} + +NS_IMETHODIMP +nsImapFolderCopyState::OnStopRunningUrl(nsIURI* aUrl, nsresult aExitCode) { + if (NS_FAILED(aExitCode)) { + if (m_copySrvcListener) m_copySrvcListener->OnStopCopy(aExitCode); + return aExitCode; // or NS_OK??? + } + nsresult rv = NS_OK; + if (aUrl) { + nsCOMPtr<nsIImapUrl> imapUrl = do_QueryInterface(aUrl); + if (imapUrl) { + nsImapAction imapAction = nsIImapUrl::nsImapTest; + imapUrl->GetImapAction(&imapAction); + + switch (imapAction) { + case nsIImapUrl::nsImapEnsureExistsFolder: { + // Our EnsureFolderExists() call has completed successfully, + // so our dest folder is ready. + nsCOMPtr<nsIMsgFolder> newMsgFolder; + nsString folderName; + nsCString utfLeafName; + m_curSrcFolder->GetName(folderName); + bool utf8AcceptEnabled; + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = + do_QueryInterface(m_curDestParent); + rv = imapFolder->GetShouldUseUtf8FolderName(&utf8AcceptEnabled); + NS_ENSURE_SUCCESS(rv, rv); + if (utf8AcceptEnabled) { + CopyUTF16toUTF8(folderName, utfLeafName); + } else { + CopyUTF16toMUTF7(folderName, utfLeafName); + } + // Create the nsIMsgFolder object which represents the folder on + // the IMAP server. + rv = m_curDestParent->FindSubFolder(utfLeafName, + getter_AddRefs(newMsgFolder)); + NS_ENSURE_SUCCESS(rv, rv); + // Save the first new folder so we can send a notification to the + // copy service when this whole process is done. + if (!m_newDestFolder) + m_newDestFolder = + static_cast<nsImapMailFolder*>(newMsgFolder.get()); + + // Check if the source folder has children. If it does, list them + // into m_srcChildFolders, and set m_destParents for the + // corresponding indexes to the newly created folder. + nsTArray<RefPtr<nsIMsgFolder>> subFolders; + rv = m_curSrcFolder->GetSubFolders(subFolders); + NS_ENSURE_SUCCESS(rv, rv); + uint32_t childIndex = 0; + for (nsIMsgFolder* folder : subFolders) { + m_srcChildFolders.InsertElementAt(m_childIndex + childIndex + 1, + folder); + m_destParents.InsertElementAt(m_childIndex + childIndex + 1, + newMsgFolder); + ++childIndex; + } + + // Now kick off a copy (or move) of messages to the new folder. + nsCOMPtr<nsIMsgEnumerator> enumerator; + rv = m_curSrcFolder->GetMessages(getter_AddRefs(enumerator)); + nsTArray<RefPtr<nsIMsgDBHdr>> msgArray; + bool hasMore = false; + + if (enumerator) rv = enumerator->HasMoreElements(&hasMore); + + // Early-out for empty folder. + if (!hasMore) return AdvanceToNextFolder(NS_OK); + + while (NS_SUCCEEDED(rv) && hasMore) { + nsCOMPtr<nsIMsgDBHdr> hdr; + rv = enumerator->GetNext(getter_AddRefs(hdr)); + NS_ENSURE_SUCCESS(rv, rv); + msgArray.AppendElement(hdr); + rv = enumerator->HasMoreElements(&hasMore); + } + + nsCOMPtr<nsIMsgCopyService> copyService = + do_GetService("@mozilla.org/messenger/messagecopyservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = copyService->CopyMessages(m_curSrcFolder, msgArray, newMsgFolder, + m_isMoveMessages, this, m_msgWindow, + false /* allowUndo */); + } break; + } + } + } + return rv; +} + +NS_IMETHODIMP nsImapFolderCopyState::OnStartCopy() { return NS_OK; } + +/* void OnProgress (in uint32_t aProgress, in uint32_t aProgressMax); */ +NS_IMETHODIMP nsImapFolderCopyState::OnProgress(uint32_t aProgress, + uint32_t aProgressMax) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* void SetMessageKey (in nsMsgKey aKey); */ +NS_IMETHODIMP nsImapFolderCopyState::SetMessageKey(nsMsgKey aKey) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* [noscript] void GetMessageId (in nsCString aMessageId); */ +NS_IMETHODIMP nsImapFolderCopyState::GetMessageId(nsACString& messageId) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* void OnStopCopy (in nsresult aStatus); */ +NS_IMETHODIMP nsImapFolderCopyState::OnStopCopy(nsresult aStatus) { + if (NS_SUCCEEDED(aStatus)) return AdvanceToNextFolder(aStatus); + if (m_copySrvcListener) { + (void)m_copySrvcListener->OnStopCopy(aStatus); + m_copySrvcListener = nullptr; + } + + return NS_OK; +} + +// "this" is the destination (parent) imap folder that srcFolder is copied to. +// srcFolder may be another imap or a local/pop3 folder. +NS_IMETHODIMP +nsImapMailFolder::CopyFolder(nsIMsgFolder* srcFolder, bool isMoveFolder, + nsIMsgWindow* msgWindow, + nsIMsgCopyServiceListener* listener) { + NS_ENSURE_ARG_POINTER(srcFolder); + nsresult rv; + bool sameServer; + rv = IsOnSameServer(this, srcFolder, &sameServer); + NS_ENSURE_SUCCESS(rv, rv); + if (sameServer && isMoveFolder) { + // Do a pure folder move within the same IMAP account/server, where + // "pure" means the folder AND messages are copied to the destination and + // then both are removed from source account. + uint32_t folderFlags = 0; + if (srcFolder) srcFolder->GetFlags(&folderFlags); + + // if our source folder is a virtual folder + if (folderFlags & nsMsgFolderFlags::Virtual) { + nsCOMPtr<nsIMsgFolder> newMsgFolder; + nsString folderName; + srcFolder->GetName(folderName); + + nsAutoString safeFolderName(folderName); + NS_MsgHashIfNecessary(safeFolderName); + + srcFolder->ForceDBClosed(); + + nsCOMPtr<nsIFile> oldPathFile; + rv = srcFolder->GetFilePath(getter_AddRefs(oldPathFile)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> summaryFile; + GetSummaryFileLocation(oldPathFile, getter_AddRefs(summaryFile)); + + nsCOMPtr<nsIFile> newPathFile; + rv = GetFilePath(getter_AddRefs(newPathFile)); + NS_ENSURE_SUCCESS(rv, rv); + + bool isDirectory = false; + newPathFile->IsDirectory(&isDirectory); + if (!isDirectory) { + AddDirectorySeparator(newPathFile); + rv = newPathFile->Create(nsIFile::DIRECTORY_TYPE, 0700); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = CheckIfFolderExists(folderName, this, msgWindow); + if (NS_FAILED(rv)) return rv; + + rv = summaryFile->CopyTo(newPathFile, EmptyString()); + NS_ENSURE_SUCCESS(rv, rv); + + rv = AddSubfolder(safeFolderName, getter_AddRefs(newMsgFolder)); + NS_ENSURE_SUCCESS(rv, rv); + + newMsgFolder->SetPrettyName(folderName); + + uint32_t flags; + srcFolder->GetFlags(&flags); + newMsgFolder->SetFlags(flags); + + NotifyFolderAdded(newMsgFolder); + + // now remove the old folder + nsCOMPtr<nsIMsgFolder> msgParent; + srcFolder->GetParent(getter_AddRefs(msgParent)); + srcFolder->SetParent(nullptr); + if (msgParent) { + // The files have already been moved, so delete storage false. + msgParent->PropagateDelete(srcFolder, false); + oldPathFile->Remove(false); // berkeley mailbox + srcFolder->DeleteStorage(); + + nsCOMPtr<nsIFile> parentPathFile; + rv = msgParent->GetFilePath(getter_AddRefs(parentPathFile)); + NS_ENSURE_SUCCESS(rv, rv); + + AddDirectorySeparator(parentPathFile); + nsCOMPtr<nsIDirectoryEnumerator> children; + parentPathFile->GetDirectoryEntries(getter_AddRefs(children)); + bool more; + // checks if the directory is empty or not + if (children && NS_SUCCEEDED(children->HasMoreElements(&more)) && !more) + parentPathFile->Remove(true); + } + } else // non-virtual folder + { + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsISupports> srcSupport = do_QueryInterface(srcFolder); + bool match = false; + bool confirmed = false; + if (mFlags & nsMsgFolderFlags::Trash) { + rv = srcFolder->MatchOrChangeFilterDestination(nullptr, false, &match); + if (match) { + srcFolder->ConfirmFolderDeletionForFilter(msgWindow, &confirmed); + // should we return an error to copy service? + // or send a notification? + if (!confirmed) return NS_OK; + } + } + rv = InitCopyState(srcSupport, {}, false, false, false, 0, EmptyCString(), + listener, msgWindow, false); + if (NS_FAILED(rv)) return OnCopyCompleted(srcSupport, rv); + + rv = imapService->MoveFolder(srcFolder, this, this, msgWindow); + } + } else { + // !sameServer OR it's a copy. Unit tests expect a successful folder + // copy within the same IMAP server even though the UI forbids copy and + // only allows moves inside the same server. folderCopier, set below, + // handles the folder copy within an IMAP server (needed by unit tests) and + // the folder move or copy from another account or server into an IMAP + // account/server. The folder move from another account is "impure" since + // just the messages are moved and the source folder remains in place. + RefPtr<nsImapFolderCopyState> folderCopier = new nsImapFolderCopyState( + this, srcFolder, + isMoveFolder, // Always copy folders; if true only move the messages + msgWindow, listener); + // NOTE: the copystate object must hold itself in existence until complete, + // as we're not keeping hold of it here. + rv = folderCopier->StartNextCopy(); + } + return rv; +} + +NS_IMETHODIMP +nsImapMailFolder::CopyFileMessage(nsIFile* file, nsIMsgDBHdr* msgToReplace, + bool isDraftOrTemplate, uint32_t aNewMsgFlags, + const nsACString& aNewMsgKeywords, + nsIMsgWindow* msgWindow, + nsIMsgCopyServiceListener* listener) { + nsresult rv = NS_ERROR_NULL_POINTER; + nsMsgKey key = nsMsgKey_None; + nsAutoCString messageId; + nsTArray<RefPtr<nsIMsgDBHdr>> messages; + + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + if (NS_FAILED(rv)) return OnCopyCompleted(file, rv); + + if (msgToReplace) { + rv = msgToReplace->GetMessageKey(&key); + if (NS_SUCCEEDED(rv)) { + messageId.AppendInt((int32_t)key); + // We have an existing message to replace because the user has deleted or + // detached one or more attachments. So tell SetPendingAttributes() to + // not set several pending offline items (offset, message size, etc.) for + // the message to be replaced by setting message size temporarily to zero. + // The original message is not actually "replaced" but is imap deleted + // and a new message with the same body but with some deleted or detached + // attachments is imap appended from the file to the folder. + uint32_t saveMsgSize; + msgToReplace->GetOfflineMessageSize(&saveMsgSize); + msgToReplace->SetOfflineMessageSize(0); + SetPendingAttributes({msgToReplace}, false, false); + msgToReplace->SetOfflineMessageSize(saveMsgSize); + messages.AppendElement(msgToReplace); + } + } + + bool isMove = (msgToReplace ? true : false); + rv = InitCopyState(file, messages, isMove, isDraftOrTemplate, false, + aNewMsgFlags, aNewMsgKeywords, listener, msgWindow, false); + if (NS_FAILED(rv)) return OnCopyCompleted(file, rv); + + m_copyState->m_streamCopy = true; + rv = imapService->AppendMessageFromFile(file, this, messageId, true, + isDraftOrTemplate, this, m_copyState, + msgWindow); + if (NS_FAILED(rv)) return OnCopyCompleted(file, rv); + + return rv; +} + +nsresult nsImapMailFolder::CopyStreamMessage( + nsIMsgDBHdr* message, + nsIMsgFolder* dstFolder, // should be this + nsIMsgWindow* aMsgWindow, bool isMove) { + NS_ENSURE_ARG_POINTER(message); + if (!m_copyState) + MOZ_LOG(IMAP, mozilla::LogLevel::Info, + ("CopyStreamMessage failed with null m_copyState")); + NS_ENSURE_TRUE(m_copyState, NS_ERROR_NULL_POINTER); + nsresult rv; + nsCOMPtr<nsICopyMessageStreamListener> copyStreamListener = do_CreateInstance( + "@mozilla.org/messenger/copymessagestreamlistener;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsICopyMessageListener> copyListener( + do_QueryInterface(dstFolder, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgFolder> srcFolder( + do_QueryInterface(m_copyState->m_srcSupport, &rv)); + if (NS_FAILED(rv)) + MOZ_LOG(IMAP, mozilla::LogLevel::Info, + ("CopyStreaMessage failed with null m_copyState->m_srcSupport")); + if (NS_FAILED(rv)) return rv; + rv = copyStreamListener->Init(copyListener); + if (NS_FAILED(rv)) + MOZ_LOG(IMAP, mozilla::LogLevel::Info, + ("CopyStreaMessage failed in copyStreamListener->Init")); + if (NS_FAILED(rv)) return rv; + + nsCString uri; + srcFolder->GetUriForMsg(message, uri); + + if (!m_copyState->m_msgService) + rv = GetMessageServiceFromURI(uri, + getter_AddRefs(m_copyState->m_msgService)); + + if (NS_SUCCEEDED(rv) && m_copyState->m_msgService) { + nsCOMPtr<nsIStreamListener> streamListener( + do_QueryInterface(copyStreamListener, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + // put up status message here, if copying more than one message. + if (m_copyState->m_messages.Length() > 1) { + nsString dstFolderName, progressText; + GetName(dstFolderName); + nsAutoString curMsgString; + nsAutoString totalMsgString; + totalMsgString.AppendInt((int32_t)m_copyState->m_messages.Length()); + curMsgString.AppendInt(m_copyState->m_curIndex + 1); + + AutoTArray<nsString, 3> formatStrings = {curMsgString, totalMsgString, + dstFolderName}; + + nsCOMPtr<nsIStringBundle> bundle; + rv = IMAPGetStringBundle(getter_AddRefs(bundle)); + NS_ENSURE_SUCCESS(rv, rv); + rv = bundle->FormatStringFromName("imapCopyingMessageOf2", formatStrings, + progressText); + nsCOMPtr<nsIMsgStatusFeedback> statusFeedback; + if (m_copyState->m_msgWindow) + m_copyState->m_msgWindow->GetStatusFeedback( + getter_AddRefs(statusFeedback)); + if (statusFeedback) { + statusFeedback->ShowStatusString(progressText); + int32_t percent; + percent = (100 * m_copyState->m_curIndex) / + (int32_t)m_copyState->m_messages.Length(); + statusFeedback->ShowProgress(percent); + } + } + rv = m_copyState->m_msgService->CopyMessage( + uri, streamListener, isMove && !m_copyState->m_isCrossServerOp, nullptr, + aMsgWindow); + if (NS_FAILED(rv)) + MOZ_LOG(IMAP, mozilla::LogLevel::Info, + ("CopyMessage failed: uri %s", uri.get())); + } + return rv; +} + +nsImapMailCopyState::nsImapMailCopyState() + : m_isMove(false), + m_selectedState(false), + m_isCrossServerOp(false), + m_curIndex(0), + m_streamCopy(false), + m_dataBuffer(nullptr), + m_dataBufferSize(0), + m_leftOver(0), + m_allowUndo(false), + m_eatLF(false), + m_newMsgFlags(0), + m_appendUID(nsMsgKey_None) {} + +nsImapMailCopyState::~nsImapMailCopyState() { + PR_Free(m_dataBuffer); + if (m_tmpFile) m_tmpFile->Remove(false); +} + +NS_IMPL_ISUPPORTS(nsImapMailCopyState, nsImapMailCopyState) + +nsresult nsImapMailFolder::InitCopyState( + nsISupports* srcSupport, nsTArray<RefPtr<nsIMsgDBHdr>> const& messages, + bool isMove, bool selectedState, bool acrossServers, uint32_t newMsgFlags, + const nsACString& newMsgKeywords, nsIMsgCopyServiceListener* listener, + nsIMsgWindow* msgWindow, bool allowUndo) { + NS_ENSURE_ARG_POINTER(srcSupport); + + NS_ENSURE_TRUE(!m_copyState, NS_ERROR_FAILURE); + + m_copyState = new nsImapMailCopyState(); + + m_copyState->m_isCrossServerOp = acrossServers; + m_copyState->m_srcSupport = srcSupport; + + m_copyState->m_messages = messages.Clone(); + if (!m_copyState->m_isCrossServerOp) { + uint32_t numUnread = 0; + for (nsIMsgDBHdr* message : m_copyState->m_messages) { + // if the message is not there, then assume what the caller tells us to. + bool isRead = false; + uint32_t flags; + if (message) { + message->GetFlags(&flags); + isRead = flags & nsMsgMessageFlags::Read; + } + if (!isRead) numUnread++; + } + m_copyState->m_unreadCount = numUnread; + } else { + nsIMsgDBHdr* message = m_copyState->m_messages[m_copyState->m_curIndex]; + // if the key is not there, then assume what the caller tells us to. + bool isRead = false; + uint32_t flags; + if (message) { + message->GetFlags(&flags); + isRead = flags & nsMsgMessageFlags::Read; + } + m_copyState->m_unreadCount = (isRead) ? 0 : 1; + } + + m_copyState->m_isMove = isMove; + m_copyState->m_newMsgFlags = newMsgFlags; + m_copyState->m_newMsgKeywords = newMsgKeywords; + m_copyState->m_allowUndo = allowUndo; + m_copyState->m_selectedState = selectedState; + m_copyState->m_msgWindow = msgWindow; + if (listener) m_copyState->m_listener = listener; + return NS_OK; +} + +nsresult nsImapMailFolder::CopyFileToOfflineStore(nsIFile* srcFile, + nsMsgKey msgKey) { + nsresult rv = GetDatabase(); + NS_ENSURE_SUCCESS(rv, rv); + + bool storeOffline = (mFlags & nsMsgFolderFlags::Offline) && !WeAreOffline(); + + if (msgKey == nsMsgKey_None) { + // To support send filters, we need to store the message in the database + // when it is copied to the FCC folder. In that case, we know the UID of the + // message and therefore have the correct msgKey. In other cases, where + // we don't need the offline message copied, don't add to db. + if (!storeOffline) return NS_OK; + + mDatabase->GetNextFakeOfflineMsgKey(&msgKey); + } + + nsCOMPtr<nsIMsgDBHdr> fakeHdr; + rv = mDatabase->CreateNewHdr(msgKey, getter_AddRefs(fakeHdr)); + NS_ENSURE_SUCCESS(rv, rv); + fakeHdr->SetUint32Property("pseudoHdr", 1); + + // Should we add this to the offline store? + nsCOMPtr<nsIOutputStream> offlineStore; + if (storeOffline) { + rv = GetOfflineStoreOutputStream(fakeHdr, getter_AddRefs(offlineStore)); + NS_ENSURE_SUCCESS(rv, rv); + } + + // We set an offline kMoveResult because in any case we want to update this + // msgHdr with one downloaded from the server, with possible additional + // headers added. + nsCOMPtr<nsIMsgOfflineImapOperation> op; + nsCOMPtr<nsIMsgOfflineOpsDatabase> opsDb = do_QueryInterface(mDatabase, &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = opsDb->GetOfflineOpForKey(msgKey, true, getter_AddRefs(op)); + if (NS_SUCCEEDED(rv) && op) { + nsCString destFolderUri; + GetURI(destFolderUri); + op->SetOperation(nsIMsgOfflineImapOperation::kMoveResult); + op->SetDestinationFolderURI(destFolderUri); + SetFlag(nsMsgFolderFlags::OfflineEvents); + } + + nsCOMPtr<nsIInputStream> inputStream; + nsCOMPtr<nsIMsgParseMailMsgState> msgParser = + do_CreateInstance("@mozilla.org/messenger/messagestateparser;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + msgParser->SetMailDB(mDatabase); + + rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream), srcFile); + if (NS_SUCCEEDED(rv) && inputStream) { + // Now, parse the temp file to (optionally) copy to + // the offline store for the cur folder. + RefPtr<nsMsgLineStreamBuffer> inputStreamBuffer = + new nsMsgLineStreamBuffer(FILE_IO_BUFFER_SIZE, true, false); + int64_t fileSize; + srcFile->GetFileSize(&fileSize); + uint32_t bytesWritten; + rv = NS_OK; + msgParser->SetState(nsIMsgParseMailMsgState::ParseHeadersState); + msgParser->SetNewMsgHdr(fakeHdr); + bool needMoreData = false; + char* newLine = nullptr; + uint32_t numBytesInLine = 0; + if (offlineStore) { + const char* envelope = "From " CRLF; + offlineStore->Write(envelope, strlen(envelope), &bytesWritten); + fileSize += bytesWritten; + } + do { + newLine = inputStreamBuffer->ReadNextLine(inputStream, numBytesInLine, + needMoreData); + if (newLine) { + msgParser->ParseAFolderLine(newLine, numBytesInLine); + if (offlineStore) + rv = offlineStore->Write(newLine, numBytesInLine, &bytesWritten); + + free(newLine); + NS_ENSURE_SUCCESS(rv, rv); + } + } while (newLine); + + msgParser->FinishHeader(); + uint32_t resultFlags; + if (offlineStore) + fakeHdr->OrFlags(nsMsgMessageFlags::Offline | nsMsgMessageFlags::Read, + &resultFlags); + else + fakeHdr->OrFlags(nsMsgMessageFlags::Read, &resultFlags); + if (offlineStore) fakeHdr->SetOfflineMessageSize(fileSize); + mDatabase->AddNewHdrToDB(fakeHdr, true /* notify */); + + // Call FinishNewMessage before setting pending attributes, as in + // maildir it copies from tmp to cur and may change the storeToken + // to get a unique filename. + if (offlineStore) { + nsCOMPtr<nsIMsgPluggableStore> msgStore; + GetMsgStore(getter_AddRefs(msgStore)); + if (msgStore) msgStore->FinishNewMessage(offlineStore, fakeHdr); + } + + // We are copying from a file to offline store so set offline flag. + SetPendingAttributes({&*fakeHdr}, false, true); + + // Gloda needs this notification to index the fake message. + nsCOMPtr<nsIMsgFolderNotificationService> notifier( + do_GetService("@mozilla.org/messenger/msgnotificationservice;1")); + if (notifier) notifier->NotifyMsgsClassified({&*fakeHdr}, false, false); + inputStream->Close(); + inputStream = nullptr; + } + if (offlineStore) offlineStore->Close(); + 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<nsIFile> srcFile(do_QueryInterface(srcSupport)); + if (srcFile) + (void)CopyFileToOfflineStore(srcFile, m_copyState->m_appendUID); + } + m_copyState = nullptr; + nsresult result; + nsCOMPtr<nsIMsgCopyService> copyService = + do_GetService("@mozilla.org/messenger/messagecopyservice;1", &result); + NS_ENSURE_SUCCESS(result, result); + return copyService->NotifyCompletion(srcSupport, this, rv); +} + +nsresult nsImapMailFolder::CreateBaseMessageURI(const nsACString& aURI) { + return nsCreateImapBaseMessageURI(aURI, mBaseMessageURI); +} + +NS_IMETHODIMP nsImapMailFolder::GetFolderURL(nsACString& aFolderURL) { + nsCOMPtr<nsIMsgFolder> rootFolder; + nsresult rv = GetRootFolder(getter_AddRefs(rootFolder)); + NS_ENSURE_SUCCESS(rv, rv); + rootFolder->GetURI(aFolderURL); + if (rootFolder == this) return NS_OK; + + NS_ASSERTION(mURI.Length() > aFolderURL.Length(), + "Should match with a folder name!"); + nsCString escapedName; + MsgEscapeString(Substring(mURI, aFolderURL.Length()), + nsINetUtil::ESCAPE_URL_PATH, escapedName); + if (escapedName.IsEmpty()) return NS_ERROR_OUT_OF_MEMORY; + aFolderURL.Append(escapedName); + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::GetFolderNeedsSubscribing(bool* bVal) { + NS_ENSURE_ARG_POINTER(bVal); + *bVal = m_folderNeedsSubscribing; + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::SetFolderNeedsSubscribing(bool bVal) { + m_folderNeedsSubscribing = bVal; + return NS_OK; +} + +nsMsgIMAPFolderACL* nsImapMailFolder::GetFolderACL() { + if (!m_folderACL) m_folderACL = new nsMsgIMAPFolderACL(this); + return m_folderACL; +} + +nsresult nsImapMailFolder::CreateACLRightsStringForFolder( + nsAString& rightsString) { + GetFolderACL(); // lazy create + NS_ENSURE_TRUE(m_folderACL, NS_ERROR_NULL_POINTER); + return m_folderACL->CreateACLRightsString(rightsString); +} + +NS_IMETHODIMP nsImapMailFolder::GetFolderNeedsACLListed(bool* bVal) { + NS_ENSURE_ARG_POINTER(bVal); + bool dontNeedACLListed = !m_folderNeedsACLListed; + // if we haven't acl listed, and it's not a no select folder or the inbox, + // then we'll list the acl if it's not a namespace. + if (m_folderNeedsACLListed && + !(mFlags & (nsMsgFolderFlags::ImapNoselect | nsMsgFolderFlags::Inbox))) + GetIsNamespace(&dontNeedACLListed); + *bVal = !dontNeedACLListed; + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::SetFolderNeedsACLListed(bool bVal) { + m_folderNeedsACLListed = bVal; + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::GetIsNamespace(bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + nsresult rv = NS_OK; + if (!m_namespace) { +#ifdef DEBUG_bienvenu + // Make sure this isn't causing us to open the database + NS_ASSERTION(m_hierarchyDelimiter != kOnlineHierarchySeparatorUnknown, + "hierarchy delimiter not set"); +#endif + + nsCString onlineName, serverKey; + GetServerKey(serverKey); + GetOnlineName(onlineName); + char hierarchyDelimiter; + GetHierarchyDelimiter(&hierarchyDelimiter); + + nsCOMPtr<nsIImapHostSessionList> hostSession = + do_GetService(kCImapHostSessionList, &rv); + NS_ENSURE_SUCCESS(rv, rv); + m_namespace = nsImapNamespaceList::GetNamespaceForFolder( + serverKey.get(), onlineName.get(), hierarchyDelimiter); + if (m_namespace == nullptr) { + if (mFlags & nsMsgFolderFlags::ImapOtherUser) + rv = hostSession->GetDefaultNamespaceOfTypeForHost( + serverKey.get(), kOtherUsersNamespace, m_namespace); + else if (mFlags & nsMsgFolderFlags::ImapPublic) + rv = hostSession->GetDefaultNamespaceOfTypeForHost( + serverKey.get(), kPublicNamespace, m_namespace); + else + rv = hostSession->GetDefaultNamespaceOfTypeForHost( + serverKey.get(), kPersonalNamespace, m_namespace); + } + NS_ASSERTION(m_namespace, "failed to get namespace"); + if (m_namespace) { + nsImapNamespaceList::SuggestHierarchySeparatorForNamespace( + m_namespace, hierarchyDelimiter); + m_folderIsNamespace = nsImapNamespaceList::GetFolderIsNamespace( + serverKey.get(), onlineName.get(), hierarchyDelimiter, m_namespace); + } + } + *aResult = m_folderIsNamespace; + return rv; +} + +NS_IMETHODIMP nsImapMailFolder::SetIsNamespace(bool isNamespace) { + m_folderIsNamespace = isNamespace; + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::ResetNamespaceReferences() { + nsCString serverKey; + nsCString onlineName; + GetServerKey(serverKey); + GetOnlineName(onlineName); + char hierarchyDelimiter; + GetHierarchyDelimiter(&hierarchyDelimiter); + m_namespace = nsImapNamespaceList::GetNamespaceForFolder( + serverKey.get(), onlineName.get(), hierarchyDelimiter); + m_folderIsNamespace = m_namespace ? nsImapNamespaceList::GetFolderIsNamespace( + serverKey.get(), onlineName.get(), + hierarchyDelimiter, m_namespace) + : false; + + nsTArray<RefPtr<nsIMsgFolder>> subFolders; + nsresult rv = GetSubFolders(subFolders); + NS_ENSURE_SUCCESS(rv, rv); + + for (nsIMsgFolder* f : subFolders) { + nsCOMPtr<nsIMsgImapMailFolder> imapFolder(do_QueryInterface(f, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + rv = imapFolder->ResetNamespaceReferences(); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::FindOnlineSubFolder( + const nsACString& targetOnlineName, nsIMsgImapMailFolder** aResultFolder) { + *aResultFolder = nullptr; + nsresult rv = NS_OK; + + nsCString onlineName; + GetOnlineName(onlineName); + + if (onlineName.Equals(targetOnlineName)) { + return QueryInterface(NS_GET_IID(nsIMsgImapMailFolder), + (void**)aResultFolder); + } + + nsTArray<RefPtr<nsIMsgFolder>> subFolders; + rv = GetSubFolders(subFolders); + NS_ENSURE_SUCCESS(rv, rv); + for (nsIMsgFolder* f : subFolders) { + nsCOMPtr<nsIMsgImapMailFolder> imapFolder(do_QueryInterface(f, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + rv = imapFolder->FindOnlineSubFolder(targetOnlineName, aResultFolder); + NS_ENSURE_SUCCESS(rv, rv); + if (*aResultFolder) { + return NS_OK; // Found it! + } + } + return NS_OK; // Not found. +} + +NS_IMETHODIMP nsImapMailFolder::GetFolderNeedsAdded(bool* bVal) { + NS_ENSURE_ARG_POINTER(bVal); + *bVal = m_folderNeedsAdded; + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::SetFolderNeedsAdded(bool bVal) { + m_folderNeedsAdded = bVal; + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::GetFolderQuotaCommandIssued(bool* aCmdIssued) { + NS_ENSURE_ARG_POINTER(aCmdIssued); + *aCmdIssued = m_folderQuotaCommandIssued; + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::SetFolderQuotaCommandIssued(bool aCmdIssued) { + m_folderQuotaCommandIssued = aCmdIssued; + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::SetFolderQuotaData( + uint32_t aAction, const nsACString& aFolderQuotaRoot, + uint64_t aFolderQuotaUsage, uint64_t aFolderQuotaLimit) { + switch (aAction) { + case kInvalidateQuota: + // Reset to initialize evaluation of a new quotaroot imap response. This + // clears any previous array data and marks the quota data for this folder + // invalid. + m_folderQuotaDataIsValid = false; + m_folderQuota.Clear(); + break; + case kStoreQuota: + // Store folder's quota data to an array. This will occur zero or more + // times for a folder. + m_folderQuota.AppendElement(new nsMsgQuota( + aFolderQuotaRoot, aFolderQuotaUsage, aFolderQuotaLimit)); + break; + case kValidateQuota: + // GETQUOTAROOT command was successful and OK response has occurred. This + // indicates that all the untagged QUOTA responses have occurred so mark + // as valid. + m_folderQuotaDataIsValid = true; + break; + default: + // Called with undefined aAction parameter. + NS_ASSERTION(false, "undefined action"); + } + return NS_OK; +} + +// Provide the quota array for status bar notification. +NS_IMETHODIMP nsImapMailFolder::GetQuota( + nsTArray<RefPtr<nsIMsgQuota>>& aArray) { + if (m_folderQuotaDataIsValid) { + aArray = m_folderQuota.Clone(); + } + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::PerformExpand(nsIMsgWindow* aMsgWindow) { + nsresult rv; + bool usingSubscription = false; + nsCOMPtr<nsIImapIncomingServer> imapServer; + rv = GetImapIncomingServer(getter_AddRefs(imapServer)); + NS_ENSURE_SUCCESS(rv, rv); + rv = imapServer->GetUsingSubscription(&usingSubscription); + if (NS_SUCCEEDED(rv) && !usingSubscription) { + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = imapService->DiscoverChildren(this, this, m_onlineFolderName); + } + return rv; +} + +NS_IMETHODIMP nsImapMailFolder::RenameClient(nsIMsgWindow* msgWindow, + nsIMsgFolder* msgFolder, + const nsACString& oldName, + const nsACString& newName) { + nsresult rv; + nsCOMPtr<nsIFile> pathFile; + rv = GetFilePath(getter_AddRefs(pathFile)); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIMsgImapMailFolder> oldImapFolder = + do_QueryInterface(msgFolder, &rv); + if (NS_FAILED(rv)) return rv; + + char hierarchyDelimiter = '/'; + oldImapFolder->GetHierarchyDelimiter(&hierarchyDelimiter); + int32_t boxflags = 0; + oldImapFolder->GetBoxFlags(&boxflags); + + nsAutoString newLeafName; + NS_ConvertUTF8toUTF16 newNameString(newName); + NS_ENSURE_SUCCESS(rv, rv); + newLeafName = newNameString; + nsAutoString folderNameStr; + int32_t folderStart = newLeafName.RFindChar( + '/'); // internal use of hierarchyDelimiter is always '/' + if (folderStart > 0) { + newLeafName = Substring(newNameString, folderStart + 1); + CreateDirectoryForFolder( + getter_AddRefs(pathFile)); // needed when we move a folder to a folder + // with no subfolders. + } + + // if we get here, it's really a leaf, and "this" is the parent. + folderNameStr = newLeafName; + + // Create an empty database for this mail folder, set its name from the user + nsCOMPtr<nsIMsgDatabase> mailDBFactory; + nsCOMPtr<nsIMsgFolder> child; + nsCOMPtr<nsIMsgImapMailFolder> imapFolder; + + nsCOMPtr<nsIMsgDBService> msgDBService = + do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgDatabase> unusedDB; + nsCOMPtr<nsIFile> dbFile; + + // warning, path will be changed + rv = CreateFileForDB(folderNameStr, pathFile, getter_AddRefs(dbFile)); + NS_ENSURE_SUCCESS(rv, rv); + + // Use openMailDBFromFile() and not OpenFolderDB() here, since we don't use + // the DB. + rv = msgDBService->OpenMailDBFromFile(dbFile, nullptr, true, true, + getter_AddRefs(unusedDB)); + if (NS_SUCCEEDED(rv) && unusedDB) { + // need to set the folder name + nsCOMPtr<nsIDBFolderInfo> folderInfo; + rv = unusedDB->GetDBFolderInfo(getter_AddRefs(folderInfo)); + + // Now let's create the actual new folder + rv = AddSubfolderWithPath(folderNameStr, dbFile, getter_AddRefs(child)); + if (!child || NS_FAILED(rv)) return rv; + nsAutoString unicodeName; + rv = CopyFolderNameToUTF16(NS_ConvertUTF16toUTF8(folderNameStr), + unicodeName); + if (NS_SUCCEEDED(rv)) child->SetPrettyName(unicodeName); + imapFolder = do_QueryInterface(child); + if (imapFolder) { + nsAutoCString onlineName(m_onlineFolderName); + + if (!onlineName.IsEmpty()) onlineName.Append(hierarchyDelimiter); + onlineName.Append(NS_ConvertUTF16toUTF8(folderNameStr)); + imapFolder->SetVerifiedAsOnlineFolder(true); + imapFolder->SetOnlineName(onlineName); + imapFolder->SetHierarchyDelimiter(hierarchyDelimiter); + imapFolder->SetBoxFlags(boxflags); + // store the online name as the mailbox name in the db folder info + // I don't think anyone uses the mailbox name, so we'll use it + // to restore the online name when blowing away an imap db. + if (folderInfo) { + nsAutoString unicodeOnlineName; + CopyUTF8toUTF16(onlineName, unicodeOnlineName); + folderInfo->SetMailboxName(unicodeOnlineName); + } + bool changed = false; + msgFolder->MatchOrChangeFilterDestination( + child, false /*caseInsensitive*/, &changed); + if (changed) msgFolder->AlertFilterChanged(msgWindow); + } + unusedDB->SetSummaryValid(true); + unusedDB->Commit(nsMsgDBCommitType::kLargeCommit); + unusedDB->Close(true); + child->RenameSubFolders(msgWindow, msgFolder); + nsCOMPtr<nsIMsgFolder> msgParent; + msgFolder->GetParent(getter_AddRefs(msgParent)); + msgFolder->SetParent(nullptr); + // Reset online status now that the folder is renamed. + nsCOMPtr<nsIMsgImapMailFolder> oldImapFolder = do_QueryInterface(msgFolder); + if (oldImapFolder) oldImapFolder->SetVerifiedAsOnlineFolder(false); + nsCOMPtr<nsIMsgFolderNotificationService> notifier( + do_GetService("@mozilla.org/messenger/msgnotificationservice;1")); + if (notifier) notifier->NotifyFolderRenamed(msgFolder, child); + + // Do not propagate the deletion until after we have (synchronously) + // notified all listeners about the rename. This allows them to access + // properties on the source folder without experiencing failures. + if (msgParent) msgParent->PropagateDelete(msgFolder, true); + NotifyFolderAdded(child); + } + return rv; +} + +NS_IMETHODIMP nsImapMailFolder::RenameSubFolders(nsIMsgWindow* msgWindow, + nsIMsgFolder* oldFolder) { + m_initialized = true; + nsTArray<RefPtr<nsIMsgFolder>> subFolders; + nsresult rv = oldFolder->GetSubFolders(subFolders); + NS_ENSURE_SUCCESS(rv, rv); + + for (nsIMsgFolder* msgFolder : subFolders) { + nsCOMPtr<nsIMsgImapMailFolder> folder(do_QueryInterface(msgFolder, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + char hierarchyDelimiter = '/'; + folder->GetHierarchyDelimiter(&hierarchyDelimiter); + + int32_t boxflags; + folder->GetBoxFlags(&boxflags); + + bool verified; + folder->GetVerifiedAsOnlineFolder(&verified); + + nsCOMPtr<nsIFile> oldPathFile; + rv = msgFolder->GetFilePath(getter_AddRefs(oldPathFile)); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIFile> newParentPathFile; + rv = GetFilePath(getter_AddRefs(newParentPathFile)); + if (NS_FAILED(rv)) return rv; + + rv = AddDirectorySeparator(newParentPathFile); + nsAutoCString oldLeafName; + oldPathFile->GetNativeLeafName(oldLeafName); + newParentPathFile->AppendNative(oldLeafName); + + nsCOMPtr<nsIFile> newPathFile = + do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + newPathFile->InitWithFile(newParentPathFile); + + nsCOMPtr<nsIFile> dbFilePath = newPathFile; + + nsCOMPtr<nsIMsgFolder> child; + + nsString folderName; + rv = msgFolder->GetName(folderName); + if (folderName.IsEmpty() || NS_FAILED(rv)) return rv; + + nsCString utfLeafName; + bool utf8AcceptEnabled; + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(msgFolder); + rv = imapFolder->GetShouldUseUtf8FolderName(&utf8AcceptEnabled); + NS_ENSURE_SUCCESS(rv, rv); + if (utf8AcceptEnabled) { + CopyUTF16toUTF8(folderName, utfLeafName); + } else { + CopyUTF16toMUTF7(folderName, utfLeafName); + } + + // XXX : Fix this non-sense by fixing AddSubfolderWithPath + nsAutoString unicodeLeafName; + CopyUTF8toUTF16(utfLeafName, unicodeLeafName); + + rv = AddSubfolderWithPath(unicodeLeafName, dbFilePath, + getter_AddRefs(child)); + if (!child || NS_FAILED(rv)) return rv; + + child->SetName(folderName); + imapFolder = do_QueryInterface(child); + nsCString onlineName; + GetOnlineName(onlineName); + nsAutoCString onlineCName(onlineName); + onlineCName.Append(hierarchyDelimiter); + onlineCName.Append(utfLeafName); + if (imapFolder) { + imapFolder->SetVerifiedAsOnlineFolder(verified); + imapFolder->SetOnlineName(onlineCName); + imapFolder->SetHierarchyDelimiter(hierarchyDelimiter); + imapFolder->SetBoxFlags(boxflags); + + bool changed = false; + msgFolder->MatchOrChangeFilterDestination( + child, false /*caseInsensitive*/, &changed); + if (changed) msgFolder->AlertFilterChanged(msgWindow); + child->RenameSubFolders(msgWindow, msgFolder); + } + } + return rv; +} + +NS_IMETHODIMP nsImapMailFolder::IsCommandEnabled(const nsACString& command, + bool* result) { + NS_ENSURE_ARG_POINTER(result); + *result = !(WeAreOffline() && (command.EqualsLiteral("cmd_renameFolder") || + command.EqualsLiteral("cmd_compactFolder") || + command.EqualsLiteral("button_compact") || + command.EqualsLiteral("cmd_delete") || + command.EqualsLiteral("button_delete"))); + return NS_OK; +} + +NS_IMETHODIMP +nsImapMailFolder::GetCanFileMessages(bool* aCanFileMessages) { + nsresult rv; + *aCanFileMessages = true; + + nsCOMPtr<nsIMsgIncomingServer> server; + rv = GetServer(getter_AddRefs(server)); + if (NS_SUCCEEDED(rv) && server) + rv = server->GetCanFileMessagesOnServer(aCanFileMessages); + + if (*aCanFileMessages) + rv = nsMsgDBFolder::GetCanFileMessages(aCanFileMessages); + + if (*aCanFileMessages) { + bool noSelect; + GetFlag(nsMsgFolderFlags::ImapNoselect, &noSelect); + *aCanFileMessages = + (noSelect) ? false : GetFolderACL()->GetCanIInsertInFolder(); + return NS_OK; + } + return rv; +} + +NS_IMETHODIMP +nsImapMailFolder::GetCanDeleteMessages(bool* aCanDeleteMessages) { + NS_ENSURE_ARG_POINTER(aCanDeleteMessages); + *aCanDeleteMessages = GetFolderACL()->GetCanIDeleteInFolder(); + return NS_OK; +} + +NS_IMETHODIMP +nsImapMailFolder::GetPerformingBiff(bool* aPerformingBiff) { + NS_ENSURE_ARG_POINTER(aPerformingBiff); + *aPerformingBiff = m_performingBiff; + return NS_OK; +} + +NS_IMETHODIMP +nsImapMailFolder::SetPerformingBiff(bool aPerformingBiff) { + m_performingBiff = aPerformingBiff; + return NS_OK; +} + +NS_IMETHODIMP +nsImapMailFolder::SetFilterList(nsIMsgFilterList* aMsgFilterList) { + m_filterList = aMsgFilterList; + return nsMsgDBFolder::SetFilterList(aMsgFilterList); +} + +nsresult nsImapMailFolder::GetMoveCoalescer() { + if (!m_moveCoalescer) + m_moveCoalescer = new nsImapMoveCoalescer(this, nullptr /* msgWindow */); + return NS_OK; +} + +NS_IMETHODIMP +nsImapMailFolder::StoreCustomKeywords(nsIMsgWindow* aMsgWindow, + const nsACString& aFlagsToAdd, + const nsACString& aFlagsToSubtract, + const nsTArray<nsMsgKey>& aKeysToStore, + nsIURI** _retval) { + if (aKeysToStore.IsEmpty()) return NS_OK; + nsresult rv = NS_OK; + if (WeAreOffline()) { + GetDatabase(); + if (!mDatabase) return NS_ERROR_UNEXPECTED; + nsCOMPtr<nsIMsgOfflineOpsDatabase> opsDb = + do_QueryInterface(mDatabase, &rv); + NS_ENSURE_SUCCESS(rv, rv); + for (auto key : aKeysToStore) { + nsCOMPtr<nsIMsgOfflineImapOperation> op; + nsresult rv2 = opsDb->GetOfflineOpForKey(key, true, getter_AddRefs(op)); + if (NS_FAILED(rv2)) rv = rv2; + SetFlag(nsMsgFolderFlags::OfflineEvents); + if (NS_SUCCEEDED(rv2) && op) { + if (!aFlagsToAdd.IsEmpty()) + op->AddKeywordToAdd(PromiseFlatCString(aFlagsToAdd).get()); + if (!aFlagsToSubtract.IsEmpty()) + op->AddKeywordToRemove(PromiseFlatCString(aFlagsToSubtract).get()); + } + } + mDatabase->Commit(nsMsgDBCommitType::kLargeCommit); // flush offline ops + return rv; + } + + nsCOMPtr<nsIImapService> imapService( + do_GetService("@mozilla.org/messenger/imapservice;1", &rv)); + NS_ENSURE_SUCCESS(rv, rv); + nsAutoCString msgIds; + AllocateUidStringFromKeys(aKeysToStore, msgIds); + nsCOMPtr<nsIURI> retUri; + rv = imapService->StoreCustomKeywords(this, aMsgWindow, aFlagsToAdd, + aFlagsToSubtract, msgIds, + getter_AddRefs(retUri)); + if (_retval) { + retUri.forget(_retval); + } + return rv; +} + +NS_IMETHODIMP nsImapMailFolder::NotifyIfNewMail() { + return PerformBiffNotifications(); +} + +bool nsImapMailFolder::ShowPreviewText() { + bool showPreviewText = false; + nsCOMPtr<nsIPrefBranch> prefBranch(do_GetService(NS_PREFSERVICE_CONTRACTID)); + if (prefBranch) + prefBranch->GetBoolPref("mail.biff.alert.show_preview", &showPreviewText); + return showPreviewText; +} + +nsresult nsImapMailFolder::PlaybackCoalescedOperations() { + if (m_moveCoalescer) { + nsTArray<nsMsgKey>* junkKeysToClassify = m_moveCoalescer->GetKeyBucket(0); + if (junkKeysToClassify && !junkKeysToClassify->IsEmpty()) + StoreCustomKeywords(m_moveCoalescer->GetMsgWindow(), "Junk"_ns, + EmptyCString(), *junkKeysToClassify, nullptr); + junkKeysToClassify->Clear(); + nsTArray<nsMsgKey>* nonJunkKeysToClassify = + m_moveCoalescer->GetKeyBucket(1); + if (nonJunkKeysToClassify && !nonJunkKeysToClassify->IsEmpty()) + StoreCustomKeywords(m_moveCoalescer->GetMsgWindow(), "NonJunk"_ns, + EmptyCString(), *nonJunkKeysToClassify, nullptr); + nonJunkKeysToClassify->Clear(); + return m_moveCoalescer->PlaybackMoves(ShowPreviewText()); + } + return NS_OK; // must not be any coalesced operations +} + +NS_IMETHODIMP +nsImapMailFolder::SetJunkScoreForMessages( + const nsTArray<RefPtr<nsIMsgDBHdr>>& aMessages, + const nsACString& aJunkScore) { + nsresult rv = nsMsgDBFolder::SetJunkScoreForMessages(aMessages, aJunkScore); + if (NS_SUCCEEDED(rv)) { + nsAutoCString messageIds; + nsTArray<nsMsgKey> keys; + nsresult rv = BuildIdsAndKeyArray(aMessages, messageIds, keys); + NS_ENSURE_SUCCESS(rv, rv); + StoreCustomKeywords( + nullptr, aJunkScore.EqualsLiteral("0") ? "NonJunk"_ns : "Junk"_ns, + aJunkScore.EqualsLiteral("0") ? "Junk"_ns : "NonJunk"_ns, keys, + nullptr); + if (mDatabase) mDatabase->Commit(nsMsgDBCommitType::kLargeCommit); + } + return rv; +} + +NS_IMETHODIMP +nsImapMailFolder::OnMessageClassified(const nsACString& aMsgURI, + nsMsgJunkStatus aClassification, + uint32_t aJunkPercent) { + nsCOMPtr<nsIMsgIncomingServer> server; + nsresult rv = GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + + if (!aMsgURI.IsEmpty()) // not end of batch + { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + rv = GetMsgDBHdrFromURI(aMsgURI, getter_AddRefs(msgHdr)); + NS_ENSURE_SUCCESS(rv, rv); + + nsMsgKey msgKey; + rv = msgHdr->GetMessageKey(&msgKey); + NS_ENSURE_SUCCESS(rv, rv); + + // check if this message needs junk classification + + uint32_t processingFlags; + GetProcessingFlags(msgKey, &processingFlags); + + if (processingFlags & nsMsgProcessingFlags::ClassifyJunk) { + nsMsgDBFolder::OnMessageClassified(aMsgURI, aClassification, + aJunkPercent); + + GetMoveCoalescer(); + if (m_moveCoalescer) { + nsTArray<nsMsgKey>* keysToClassify = m_moveCoalescer->GetKeyBucket( + (aClassification == nsIJunkMailPlugin::JUNK) ? 0 : 1); + NS_ASSERTION(keysToClassify, "error getting key bucket"); + if (keysToClassify) keysToClassify->AppendElement(msgKey); + } + if (aClassification == nsIJunkMailPlugin::JUNK) { + nsCOMPtr<nsISpamSettings> spamSettings; + rv = server->GetSpamSettings(getter_AddRefs(spamSettings)); + NS_ENSURE_SUCCESS(rv, rv); + + bool markAsReadOnSpam; + (void)spamSettings->GetMarkAsReadOnSpam(&markAsReadOnSpam); + if (markAsReadOnSpam) { + m_junkMessagesToMarkAsRead.AppendElement(msgHdr); + } + + bool willMoveMessage = false; + + // don't do the move when we are opening up + // the junk mail folder or the trash folder + // or when manually classifying messages in those folders + if (!(mFlags & nsMsgFolderFlags::Junk || + mFlags & nsMsgFolderFlags::Trash)) { + bool moveOnSpam; + (void)spamSettings->GetMoveOnSpam(&moveOnSpam); + if (moveOnSpam) { + nsCString spamFolderURI; + rv = spamSettings->GetSpamFolderURI(spamFolderURI); + NS_ENSURE_SUCCESS(rv, rv); + + if (!spamFolderURI.IsEmpty()) { + rv = FindFolder(spamFolderURI, getter_AddRefs(mSpamFolder)); + NS_ENSURE_SUCCESS(rv, rv); + if (mSpamFolder) { + rv = mSpamFolder->SetFlag(nsMsgFolderFlags::Junk); + NS_ENSURE_SUCCESS(rv, rv); + mSpamKeysToMove.AppendElement(msgKey); + willMoveMessage = true; + } else { + // XXX TODO + // JUNK MAIL RELATED + // the listener should do + // rv = folder->SetFlag(nsMsgFolderFlags::Junk); + // NS_ENSURE_SUCCESS(rv,rv); + // if (NS_SUCCEEDED(GetMoveCoalescer())) { + // m_moveCoalescer->AddMove(folder, msgKey); + // willMoveMessage = true; + // } + rv = GetOrCreateJunkFolder(spamFolderURI, + nullptr /* aListener */); + NS_ASSERTION(NS_SUCCEEDED(rv), "GetOrCreateJunkFolder failed"); + } + } + } + } + rv = spamSettings->LogJunkHit(msgHdr, willMoveMessage); + NS_ENSURE_SUCCESS(rv, rv); + } + } + } + + else // end of batch + { + // Parent will apply post bayes filters. + nsMsgDBFolder::OnMessageClassified(EmptyCString(), + nsIJunkMailPlugin::UNCLASSIFIED, 0); + + if (!m_junkMessagesToMarkAsRead.IsEmpty()) { + rv = MarkMessagesRead(m_junkMessagesToMarkAsRead, true); + NS_ENSURE_SUCCESS(rv, rv); + m_junkMessagesToMarkAsRead.Clear(); + } + if (!mSpamKeysToMove.IsEmpty()) { + GetMoveCoalescer(); + for (uint32_t keyIndex = 0; keyIndex < mSpamKeysToMove.Length(); + keyIndex++) { + // If an upstream filter moved this message, don't move it here. + nsMsgKey msgKey = mSpamKeysToMove.ElementAt(keyIndex); + nsMsgProcessingFlagType processingFlags; + GetProcessingFlags(msgKey, &processingFlags); + if (!(processingFlags & nsMsgProcessingFlags::FilterToMove)) { + if (m_moveCoalescer && mSpamFolder) + m_moveCoalescer->AddMove(mSpamFolder, msgKey); + } else { + // We don't need the FilterToMove flag anymore. + AndProcessingFlags(msgKey, ~nsMsgProcessingFlags::FilterToMove); + } + } + mSpamKeysToMove.Clear(); + } + + // Let's not hold onto the spam folder reference longer than necessary. + mSpamFolder = nullptr; + + bool pendingMoves = m_moveCoalescer && m_moveCoalescer->HasPendingMoves(); + PlaybackCoalescedOperations(); + // If we are performing biff for this folder, tell the server object + if ((!pendingMoves || !ShowPreviewText()) && m_performingBiff) { + // we don't need to adjust the num new messages in this folder because + // the playback moves code already did that. + (void)PerformBiffNotifications(); + server->SetPerformingBiff(false); + m_performingBiff = false; + } + } + return NS_OK; +} + +NS_IMETHODIMP +nsImapMailFolder::GetShouldDownloadAllHeaders(bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + *aResult = false; + // for just the inbox, we check if the filter list has arbitrary headers. + // for all folders, check if we have a spam plugin that requires all headers + if (mFlags & nsMsgFolderFlags::Inbox) { + nsCOMPtr<nsIMsgFilterList> filterList; + nsresult rv = GetFilterList(nullptr, getter_AddRefs(filterList)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = filterList->GetShouldDownloadAllHeaders(aResult); + if (*aResult) return rv; + } + nsCOMPtr<nsIMsgFilterPlugin> filterPlugin; + nsCOMPtr<nsIMsgIncomingServer> server; + + if (NS_SUCCEEDED(GetServer(getter_AddRefs(server)))) + server->GetSpamFilterPlugin(getter_AddRefs(filterPlugin)); + + return (filterPlugin) ? filterPlugin->GetShouldDownloadAllHeaders(aResult) + : NS_OK; +} + +void nsImapMailFolder::GetTrashFolderName(nsAString& aFolderName) { + nsCOMPtr<nsIMsgIncomingServer> server; + nsCOMPtr<nsIImapIncomingServer> imapServer; + nsresult rv; + rv = GetServer(getter_AddRefs(server)); + if (NS_FAILED(rv)) return; + imapServer = do_QueryInterface(server, &rv); + if (NS_FAILED(rv)) return; + imapServer->GetTrashFolderName(aFolderName); + return; +} +NS_IMETHODIMP nsImapMailFolder::FetchMsgPreviewText( + nsTArray<nsMsgKey> const& aKeysToFetch, nsIUrlListener* aUrlListener, + bool* aAsyncResults) { + NS_ENSURE_ARG_POINTER(aAsyncResults); + + nsTArray<nsMsgKey> keysToFetchFromServer; + + *aAsyncResults = false; + nsresult rv = NS_OK; + + nsCOMPtr<nsIMsgMessageService> msgService = + do_GetService("@mozilla.org/messenger/messageservice;1?type=imap", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + for (uint32_t i = 0; i < aKeysToFetch.Length(); i++) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + nsCString prevBody; + rv = GetMessageHeader(aKeysToFetch[i], getter_AddRefs(msgHdr)); + NS_ENSURE_SUCCESS(rv, rv); + // ignore messages that already have a preview body. + msgHdr->GetStringProperty("preview", prevBody); + if (!prevBody.IsEmpty()) continue; + + /* check if message is in memory cache or offline store. */ + nsCOMPtr<nsIURI> url; + nsCOMPtr<nsIInputStream> inputStream; + nsCString messageUri; + rv = GetUriForMsg(msgHdr, messageUri); + NS_ENSURE_SUCCESS(rv, rv); + rv = msgService->GetUrlForUri(messageUri, nullptr, getter_AddRefs(url)); + NS_ENSURE_SUCCESS(rv, rv); + + // Lets look in the offline store. + uint32_t msgFlags; + msgHdr->GetFlags(&msgFlags); + if (msgFlags & nsMsgMessageFlags::Offline) { + rv = GetLocalMsgStream(msgHdr, getter_AddRefs(inputStream)); + NS_ENSURE_SUCCESS(rv, rv); + rv = GetMsgPreviewTextFromStream(msgHdr, inputStream); + NS_ENSURE_SUCCESS(rv, rv); + } else { + nsMsgKey msgKey; + msgHdr->GetMessageKey(&msgKey); + keysToFetchFromServer.AppendElement(msgKey); + } + } + if (!keysToFetchFromServer.IsEmpty()) { + uint32_t msgCount = keysToFetchFromServer.Length(); + nsAutoCString messageIds; + AllocateImapUidString(keysToFetchFromServer.Elements(), msgCount, nullptr, + messageIds); + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIURI> outUri; + rv = imapService->GetBodyStart(this, aUrlListener, messageIds, 2048, + getter_AddRefs(outUri)); + *aAsyncResults = true; // the preview text will be available async... + } + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::AddKeywordsToMessages( + const nsTArray<RefPtr<nsIMsgDBHdr>>& aMessages, + const nsACString& aKeywords) { + nsresult rv = nsMsgDBFolder::AddKeywordsToMessages(aMessages, aKeywords); + if (NS_SUCCEEDED(rv)) { + nsAutoCString messageIds; + nsTArray<nsMsgKey> keys; + rv = BuildIdsAndKeyArray(aMessages, messageIds, keys); + NS_ENSURE_SUCCESS(rv, rv); + rv = StoreCustomKeywords(nullptr, aKeywords, EmptyCString(), keys, nullptr); + if (mDatabase) mDatabase->Commit(nsMsgDBCommitType::kLargeCommit); + } + return rv; +} + +NS_IMETHODIMP nsImapMailFolder::RemoveKeywordsFromMessages( + const nsTArray<RefPtr<nsIMsgDBHdr>>& aMessages, + const nsACString& aKeywords) { + nsresult rv = nsMsgDBFolder::RemoveKeywordsFromMessages(aMessages, aKeywords); + if (NS_SUCCEEDED(rv)) { + nsAutoCString messageIds; + nsTArray<nsMsgKey> keys; + nsresult rv = BuildIdsAndKeyArray(aMessages, messageIds, keys); + NS_ENSURE_SUCCESS(rv, rv); + rv = StoreCustomKeywords(nullptr, EmptyCString(), aKeywords, keys, nullptr); + if (mDatabase) mDatabase->Commit(nsMsgDBCommitType::kLargeCommit); + } + return rv; +} + +NS_IMETHODIMP nsImapMailFolder::GetCustomIdentity(nsIMsgIdentity** aIdentity) { + NS_ENSURE_ARG_POINTER(aIdentity); + if (mFlags & nsMsgFolderFlags::ImapOtherUser) { + nsresult rv; + bool delegateOtherUsersFolders = false; + nsCOMPtr<nsIPrefBranch> prefBranch( + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + prefBranch->GetBoolPref("mail.imap.delegateOtherUsersFolders", + &delegateOtherUsersFolders); + // if we're automatically delegating other user's folders, we need to + // cons up an e-mail address for the other user. We do that by + // taking the other user's name and the current user's domain name, + // assuming they'll be the same. So, <otherUsersName>@<ourDomain> + if (delegateOtherUsersFolders) { + nsCOMPtr<nsIMsgIncomingServer> server = do_QueryReferent(mServer, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgAccountManager> accountManager = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgIdentity> ourIdentity; + nsCOMPtr<nsIMsgIdentity> retIdentity; + nsCOMPtr<nsIMsgAccount> account; + nsCString foldersUserName; + nsCString ourEmailAddress; + + accountManager->FindAccountForServer(server, getter_AddRefs(account)); + NS_ENSURE_SUCCESS(rv, rv); + account->GetDefaultIdentity(getter_AddRefs(ourIdentity)); + NS_ENSURE_SUCCESS(rv, rv); + ourIdentity->GetEmail(ourEmailAddress); + int32_t atPos = ourEmailAddress.FindChar('@'); + if (atPos != kNotFound) { + nsCString otherUsersEmailAddress; + GetFolderOwnerUserName(otherUsersEmailAddress); + otherUsersEmailAddress.Append( + Substring(ourEmailAddress, atPos, ourEmailAddress.Length())); + nsTArray<RefPtr<nsIMsgIdentity>> identities; + rv = accountManager->GetIdentitiesForServer(server, identities); + NS_ENSURE_SUCCESS(rv, rv); + + for (auto identity : identities) { + if (!identity) continue; + nsCString identityEmail; + identity->GetEmail(identityEmail); + if (identityEmail.Equals(otherUsersEmailAddress)) { + retIdentity = identity; + break; + } + } + if (!retIdentity) { + // create the identity + rv = accountManager->CreateIdentity(getter_AddRefs(retIdentity)); + NS_ENSURE_SUCCESS(rv, rv); + retIdentity->SetEmail(otherUsersEmailAddress); + nsCOMPtr<nsIMsgAccount> account; + accountManager->FindAccountForServer(server, getter_AddRefs(account)); + NS_ENSURE_SUCCESS(rv, rv); + account->AddIdentity(retIdentity); + } + } + if (retIdentity) { + retIdentity.forget(aIdentity); + return NS_OK; + } + } + } + return nsMsgDBFolder::GetCustomIdentity(aIdentity); +} + +NS_IMETHODIMP nsImapMailFolder::ChangePendingTotal(int32_t aDelta) { + ChangeNumPendingTotalMessages(aDelta); + if (aDelta > 0) NotifyHasPendingMsgs(); + return NS_OK; +} + +void nsImapMailFolder::NotifyHasPendingMsgs() { + InitAutoSyncState(); + nsresult rv; + nsCOMPtr<nsIAutoSyncManager> autoSyncMgr = + do_GetService(NS_AUTOSYNCMANAGER_CONTRACTID, &rv); + if (NS_SUCCEEDED(rv)) autoSyncMgr->OnFolderHasPendingMsgs(m_autoSyncStateObj); +} + +/* void changePendingUnread (in long aDelta); */ +NS_IMETHODIMP nsImapMailFolder::ChangePendingUnread(int32_t aDelta) { + ChangeNumPendingUnread(aDelta); + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::GetServerRecent(int32_t* aServerRecent) { + NS_ENSURE_ARG_POINTER(aServerRecent); + *aServerRecent = m_numServerRecentMessages; + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::GetServerTotal(int32_t* aServerTotal) { + NS_ENSURE_ARG_POINTER(aServerTotal); + *aServerTotal = m_numServerTotalMessages; + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::GetServerUnseen(int32_t* aServerUnseen) { + NS_ENSURE_ARG_POINTER(aServerUnseen); + *aServerUnseen = m_numServerUnseenMessages; + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::GetServerNextUID(int32_t* aNextUID) { + NS_ENSURE_ARG_POINTER(aNextUID); + *aNextUID = m_nextUID; + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::GetAutoSyncStateObj( + nsIAutoSyncState** autoSyncStateObj) { + NS_ENSURE_ARG_POINTER(autoSyncStateObj); + + // create auto-sync state object lazily + InitAutoSyncState(); + + NS_IF_ADDREF(*autoSyncStateObj = m_autoSyncStateObj); + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::InitiateAutoSync(nsIUrlListener* aUrlListener) { + nsCString folderName; + GetURI(folderName); + MOZ_LOG(gAutoSyncLog, mozilla::LogLevel::Debug, + ("%s: Updating folder: %s", __func__, folderName.get())); + + // HACK: if UpdateFolder finds out that it can't open + // the folder, it doesn't set the url listener and returns + // no error. In this case, we return success from this call + // but the caller never gets a notification on its url listener. + bool canOpenThisFolder = true; + GetCanOpenFolder(&canOpenThisFolder); + + if (!canOpenThisFolder) { + MOZ_LOG(gAutoSyncLog, mozilla::LogLevel::Debug, + ("%s: Cannot update folder: %s", __func__, folderName.get())); + return NS_ERROR_FAILURE; + } + + // create auto-sync state object lazily + InitAutoSyncState(); + + // make sure we get the counts from the folder cache. + ReadDBFolderInfo(false); + + nsresult rv = m_autoSyncStateObj->ManageStorageSpace(); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t syncState; + m_autoSyncStateObj->GetState(&syncState); + if (syncState == nsAutoSyncState::stUpdateNeeded) + return m_autoSyncStateObj->UpdateFolder(); + + // We only want to init the autosyncStateObj server counts the first time + // we update, and update it when the STATUS call finishes. This deals with + // the case where biff is doing a STATUS on a non-inbox folder, which + // can make autosync think the counts aren't changing. + PRTime lastUpdateTime; + m_autoSyncStateObj->GetLastUpdateTime(&lastUpdateTime); + if (!lastUpdateTime) + m_autoSyncStateObj->SetServerCounts(m_numServerTotalMessages, + m_numServerRecentMessages, + m_numServerUnseenMessages, m_nextUID); + // Issue a STATUS command and see if any counts changed. + m_autoSyncStateObj->SetState(nsAutoSyncState::stStatusIssued); + // The OnStopRunningUrl method of the autosync state obj + // will check if the counts or next uid have changed, + // and if so, will issue an UpdateFolder(). + rv = UpdateStatus(m_autoSyncStateObj, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + // record the last update time + m_autoSyncStateObj->SetLastUpdateTime(PR_Now()); + + return NS_OK; +} + +/* static */ +void nsImapMailFolder::PlaybackTimerCallback(nsITimer* aTimer, void* aClosure) { + nsPlaybackRequest* request = static_cast<nsPlaybackRequest*>(aClosure); + + NS_ASSERTION(request->SrcFolder->m_pendingPlaybackReq == request, + "wrong playback request pointer"); + + RefPtr<nsImapOfflineSync> offlineSync = new nsImapOfflineSync(); + offlineSync->Init(request->MsgWindow, nullptr, request->SrcFolder, true); + if (offlineSync) { + mozilla::DebugOnly<nsresult> rv = offlineSync->ProcessNextOperation(); + NS_ASSERTION(NS_SUCCEEDED(rv), "pseudo-offline playback is not successful"); + } + + // release request struct and timer + request->SrcFolder->m_pendingPlaybackReq = nullptr; + request->SrcFolder->m_playbackTimer = nullptr; // Just to flag timed out + delete request; +} + +void nsImapMailFolder::InitAutoSyncState() { + if (!m_autoSyncStateObj) m_autoSyncStateObj = new nsAutoSyncState(this); +} + +NS_IMETHODIMP nsImapMailFolder::HasMsgOffline(nsMsgKey msgKey, bool* _retval) { + NS_ENSURE_ARG_POINTER(_retval); + *_retval = false; + nsCOMPtr<nsIMsgFolder> msgFolder; + nsresult rv = GetOfflineMsgFolder(msgKey, getter_AddRefs(msgFolder)); + if (NS_SUCCEEDED(rv) && msgFolder) *_retval = true; + return NS_OK; +} + +nsresult nsImapMailFolder::GetOfflineMsgFolder(nsMsgKey msgKey, + nsIMsgFolder** aMsgFolder) { + // Check if we have the message in the current folder. + NS_ENSURE_ARG_POINTER(aMsgFolder); + nsCOMPtr<nsIMsgFolder> subMsgFolder; + nsresult rv = GetDatabase(); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgDBHdr> hdr; + rv = mDatabase->GetMsgHdrForKey(msgKey, getter_AddRefs(hdr)); + if (NS_FAILED(rv)) return rv; + + if (hdr) { + uint32_t msgFlags = 0; + hdr->GetFlags(&msgFlags); + // Check if we already have this message body offline + if ((msgFlags & nsMsgMessageFlags::Offline)) { + NS_IF_ADDREF(*aMsgFolder = this); + return NS_OK; + } + } + + if (!*aMsgFolder) { + // Checking the existence of message in other folders in case of GMail + // Server + bool isGMail; + nsCOMPtr<nsIImapIncomingServer> imapServer; + rv = GetImapIncomingServer(getter_AddRefs(imapServer)); + NS_ENSURE_SUCCESS(rv, rv); + rv = imapServer->GetIsGMailServer(&isGMail); + NS_ENSURE_SUCCESS(rv, rv); + + if (isGMail) { + nsCString labels; + nsTArray<nsCString> labelNames; + hdr->GetStringProperty("X-GM-LABELS", labels); + ParseString(labels, ' ', labelNames); + nsCOMPtr<nsIMsgFolder> rootFolder; + nsCOMPtr<nsIMsgImapMailFolder> subFolder; + for (uint32_t i = 0; i < labelNames.Length(); i++) { + rv = GetRootFolder(getter_AddRefs(rootFolder)); + if (NS_SUCCEEDED(rv) && (rootFolder)) { + nsCOMPtr<nsIMsgImapMailFolder> imapRootFolder = + do_QueryInterface(rootFolder); + if (labelNames[i].EqualsLiteral("\"\\\\Draft\"")) + rv = rootFolder->GetFolderWithFlags(nsMsgFolderFlags::Drafts, + getter_AddRefs(subMsgFolder)); + if (labelNames[i].EqualsLiteral("\"\\\\Inbox\"")) + rv = rootFolder->GetFolderWithFlags(nsMsgFolderFlags::Inbox, + getter_AddRefs(subMsgFolder)); + if (labelNames[i].EqualsLiteral("\"\\\\All Mail\"")) + rv = rootFolder->GetFolderWithFlags(nsMsgFolderFlags::Archive, + getter_AddRefs(subMsgFolder)); + if (labelNames[i].EqualsLiteral("\"\\\\Trash\"")) + rv = rootFolder->GetFolderWithFlags(nsMsgFolderFlags::Trash, + getter_AddRefs(subMsgFolder)); + if (labelNames[i].EqualsLiteral("\"\\\\Spam\"")) + rv = rootFolder->GetFolderWithFlags(nsMsgFolderFlags::Junk, + getter_AddRefs(subMsgFolder)); + if (labelNames[i].EqualsLiteral("\"\\\\Sent\"")) + rv = rootFolder->GetFolderWithFlags(nsMsgFolderFlags::SentMail, + getter_AddRefs(subMsgFolder)); + if (FindInReadable("[Imap]/"_ns, labelNames[i], + nsCaseInsensitiveCStringComparator)) { + labelNames[i].ReplaceSubstring("[Imap]/", ""); + imapRootFolder->FindOnlineSubFolder(labelNames[i], + getter_AddRefs(subFolder)); + subMsgFolder = do_QueryInterface(subFolder); + } + if (!subMsgFolder) { + imapRootFolder->FindOnlineSubFolder(labelNames[i], + getter_AddRefs(subFolder)); + subMsgFolder = do_QueryInterface(subFolder); + } + if (subMsgFolder) { + nsCOMPtr<nsIMsgDatabase> db; + subMsgFolder->GetMsgDatabase(getter_AddRefs(db)); + if (db) { + nsCOMPtr<nsIMsgDBHdr> retHdr; + nsCString gmMsgID; + hdr->GetStringProperty("X-GM-MSGID", gmMsgID); + rv = db->GetMsgHdrForGMMsgID(gmMsgID.get(), + getter_AddRefs(retHdr)); + if (NS_FAILED(rv)) return rv; + if (retHdr) { + uint32_t gmFlags = 0; + retHdr->GetFlags(&gmFlags); + if ((gmFlags & nsMsgMessageFlags::Offline)) { + subMsgFolder.forget(aMsgFolder); + // Focus on first positive result. + return NS_OK; + } + } + } + } + } + } + } + } + return NS_OK; +} + +nsresult nsImapMailFolder::GetOfflineFileStream(nsMsgKey msgKey, + uint64_t* offset, + uint32_t* size, + nsIInputStream** aFileStream) { + NS_ENSURE_ARG(aFileStream); + nsCOMPtr<nsIMsgFolder> offlineFolder; + nsresult rv = GetOfflineMsgFolder(msgKey, getter_AddRefs(offlineFolder)); + NS_ENSURE_SUCCESS(rv, rv); + if (!offlineFolder) return NS_ERROR_FAILURE; + + rv = GetDatabase(); + NS_ENSURE_SUCCESS(rv, rv); + + if (offlineFolder == this) { + return nsMsgDBFolder::GetOfflineFileStream(msgKey, offset, size, + aFileStream); + } + + // The message we want is stored in a different folder (hackery for gmail). + nsCOMPtr<nsIMsgDBHdr> hdr; + rv = mDatabase->GetMsgHdrForKey(msgKey, getter_AddRefs(hdr)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString gmMsgID; + hdr->GetStringProperty("X-GM-MSGID", gmMsgID); + nsCOMPtr<nsIMsgDatabase> db; + offlineFolder->GetMsgDatabase(getter_AddRefs(db)); + rv = db->GetMsgHdrForGMMsgID(gmMsgID.get(), getter_AddRefs(hdr)); + NS_ENSURE_SUCCESS(rv, rv); + if (!hdr) { + return NS_ERROR_FAILURE; + } + + nsMsgKey newMsgKey; + hdr->GetMessageKey(&newMsgKey); + + // We _know_ it's a nsImapMailFolder. + nsImapMailFolder* other = static_cast<nsImapMailFolder*>(offlineFolder.get()); + return other->GetOfflineFileStream(newMsgKey, offset, size, aFileStream); +} + +NS_IMETHODIMP nsImapMailFolder::GetLocalMsgStream(nsIMsgDBHdr* hdr, + nsIInputStream** stream) { + nsMsgKey key; + hdr->GetMessageKey(&key); + + uint64_t offset = 0; + uint32_t size = 0; + nsCOMPtr<nsIInputStream> rawStream; + nsresult rv = + GetOfflineFileStream(key, &offset, &size, getter_AddRefs(rawStream)); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<SlicedInputStream> slicedStream = + new SlicedInputStream(rawStream.forget(), offset, uint64_t(size)); + slicedStream.forget(stream); + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::GetIncomingServerType(nsACString& serverType) { + serverType.AssignLiteral("imap"); + return NS_OK; +} + +NS_IMETHODIMP nsImapMailFolder::GetShouldUseUtf8FolderName(bool* aUseUTF8) { + *aUseUTF8 = false; + nsCOMPtr<nsIMsgIncomingServer> server; + nsresult rv = GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIImapIncomingServer> imapServer = do_QueryInterface(server, &rv); + NS_ENSURE_SUCCESS(rv, rv); + imapServer->GetUtf8AcceptEnabled(aUseUTF8); + return NS_OK; +} + +void nsImapMailFolder::DeleteStoreMessages( + const nsTArray<RefPtr<nsIMsgDBHdr>>& aMessages) { + // Delete messages for pluggable stores that do not support compaction. + nsCOMPtr<nsIMsgPluggableStore> offlineStore; + (void)GetMsgStore(getter_AddRefs(offlineStore)); + + if (offlineStore) { + bool supportsCompaction; + offlineStore->GetSupportsCompaction(&supportsCompaction); + if (!supportsCompaction) offlineStore->DeleteMessages(aMessages); + } +} + +void nsImapMailFolder::DeleteStoreMessages( + const nsTArray<nsMsgKey>& aMessages) { + DeleteStoreMessages(aMessages, this); +} + +void nsImapMailFolder::DeleteStoreMessages(const nsTArray<nsMsgKey>& aMessages, + nsIMsgFolder* aFolder) { + // Delete messages for pluggable stores that do not support compaction. + NS_ASSERTION(aFolder, "Missing Source Folder"); + nsCOMPtr<nsIMsgPluggableStore> offlineStore; + (void)aFolder->GetMsgStore(getter_AddRefs(offlineStore)); + if (offlineStore) { + bool supportsCompaction; + offlineStore->GetSupportsCompaction(&supportsCompaction); + if (!supportsCompaction) { + nsCOMPtr<nsIMsgDatabase> db; + aFolder->GetMsgDatabase(getter_AddRefs(db)); + nsresult rv = NS_ERROR_FAILURE; + nsTArray<RefPtr<nsIMsgDBHdr>> messages; + if (db) rv = MsgGetHeadersFromKeys(db, aMessages, messages); + if (NS_SUCCEEDED(rv)) + offlineStore->DeleteMessages(messages); + else + NS_WARNING("Failed to get database"); + } + } +} diff --git a/comm/mailnews/imap/src/nsImapMailFolder.h b/comm/mailnews/imap/src/nsImapMailFolder.h new file mode 100644 index 0000000000..0f7f6ce406 --- /dev/null +++ b/comm/mailnews/imap/src/nsImapMailFolder.h @@ -0,0 +1,599 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef nsImapMailFolder_h__ +#define nsImapMailFolder_h__ + +#include "mozilla/Attributes.h" +#include "nsImapCore.h" // so that consumers including ImapMailFolder.h also get the kImapMsg* constants +#include "nsMsgDBFolder.h" +#include "nsIImapMailFolderSink.h" +#include "nsIImapMessageSink.h" +#include "nsICopyMessageListener.h" +#include "nsIUrlListener.h" +#include "nsIImapIncomingServer.h" // we need this for its IID +#include "nsIMsgParseMailMsgState.h" +#include "nsImapUndoTxn.h" +#include "nsIMsgMessageService.h" +#include "nsIMsgFilterHitNotify.h" +#include "nsIMsgFilterList.h" +#include "prmon.h" +#include "nsIMsgImapMailFolder.h" +#include "nsIMsgThread.h" +#include "nsIImapMailFolderSink.h" +#include "nsIMsgFilterPlugin.h" +#include "nsISimpleEnumerator.h" +#include "nsIStringEnumerator.h" +#include "nsTHashMap.h" +#include "nsITimer.h" +#include "nsCOMArray.h" +#include "nsAutoSyncState.h" + +class nsImapMoveCoalescer; +class nsIMsgIdentity; +class nsIMsgOfflineImapOperation; + +#define COPY_BUFFER_SIZE 16384 + +#define NS_IMAPMAILCOPYSTATE_IID \ + { \ + 0xb64534f0, 0x3d53, 0x11d3, { \ + 0xac, 0x2a, 0x00, 0x80, 0x5f, 0x8a, 0xc9, 0x68 \ + } \ + } + +class nsImapMailCopyState : public nsISupports { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_IMAPMAILCOPYSTATE_IID) + + NS_DECL_THREADSAFE_ISUPPORTS + + nsImapMailCopyState(); + + nsCOMPtr<nsISupports> m_srcSupport; // source file spec or folder + nsTArray<RefPtr<nsIMsgDBHdr>> m_messages; // array of source messages + RefPtr<nsImapMoveCopyMsgTxn> + m_undoMsgTxn; // undo object with this copy operation + nsCOMPtr<nsIMsgCopyServiceListener> m_listener; // listener of this copy + // operation + nsCOMPtr<nsIFile> m_tmpFile; // temp file spec for copy operation + nsCOMPtr<nsIMsgWindow> m_msgWindow; // msg window for copy operation + + nsCOMPtr<nsIMsgMessageService> + m_msgService; // source folder message service; can + // be Nntp, Mailbox, or Imap + bool m_isMove; // is a move + bool m_selectedState; // needs to be in selected state; append msg + bool m_isCrossServerOp; // are we copying between imap servers? + uint32_t m_curIndex; // message index to the message array which we are + // copying + uint32_t m_unreadCount; // num unread messages we're moving + bool m_streamCopy; + char* m_dataBuffer; // temporary buffer for this copy operation + nsCOMPtr<nsIOutputStream> m_msgFileStream; // temporary file (processed mail) + uint32_t m_dataBufferSize; + uint32_t m_leftOver; + bool m_allowUndo; + bool m_eatLF; + uint32_t m_newMsgFlags; // only used if m_messages is empty + nsCString m_newMsgKeywords; // ditto + // If the server supports UIDPLUS, this is the UID for the append, + // if we're doing an append. + nsMsgKey m_appendUID; + + private: + virtual ~nsImapMailCopyState(); +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(nsImapMailCopyState, NS_IMAPMAILCOPYSTATE_IID) + +// ACLs for this folder. +// Generally, we will try to always query this class when performing +// an operation on the folder. +// If the server doesn't support ACLs, none of this data will be filled in. +// Therefore, we can assume that if we look up ourselves and don't find +// any info (and also look up "anyone") then we have full rights, that is, ACLs +// don't exist. +class nsImapMailFolder; + +// clang-format off +#define IMAP_ACL_READ_FLAG 0x0000001 // SELECT, CHECK, FETCH, PARTIAL, SEARCH, COPY from folder +#define IMAP_ACL_STORE_SEEN_FLAG 0x0000002 // STORE SEEN flag +#define IMAP_ACL_WRITE_FLAG 0x0000004 // STORE flags other than SEEN and DELETED +#define IMAP_ACL_INSERT_FLAG 0x0000008 // APPEND, COPY into folder +#define IMAP_ACL_POST_FLAG 0x0000010 // Can I send mail to the submission address for folder? +#define IMAP_ACL_CREATE_SUBFOLDER_FLAG 0x0000020 // Can I CREATE a subfolder of this folder? +#define IMAP_ACL_DELETE_FLAG 0x0000040 // STORE DELETED flag +#define IMAP_ACL_ADMINISTER_FLAG 0x0000080 // perform SETACL +#define IMAP_ACL_RETRIEVED_FLAG 0x0000100 // ACL info for this folder has been initialized +#define IMAP_ACL_EXPUNGE_FLAG 0x0000200 // can EXPUNGE or do implicit EXPUNGE on CLOSE +#define IMAP_ACL_DELETE_FOLDER 0x0000400 // can DELETE/RENAME folder +// clang-format on + +class nsMsgIMAPFolderACL { + public: + explicit nsMsgIMAPFolderACL(nsImapMailFolder* folder); + ~nsMsgIMAPFolderACL(); + + bool SetFolderRightsForUser(const nsACString& userName, + const nsACString& rights); + + public: + // generic for any user, although we might not use them in + // DO NOT use these for looking up information about the currently + // authenticated user. (There are some different checks and defaults we do). + // Instead, use the functions below, GetICan....() + // clang-format off + bool GetCanUserLookupFolder(const nsACString& userName); // Is folder visible to LIST/LSUB? + bool GetCanUserReadFolder(const nsACString& userName); // SELECT, CHECK, FETCH, PARTIAL, SEARCH, COPY from folder? + bool GetCanUserStoreSeenInFolder(const nsACString& userName); // STORE SEEN flag? + bool GetCanUserWriteFolder(const nsACString& userName); // STORE flags other than SEEN and DELETED? + bool GetCanUserInsertInFolder(const nsACString& userName); // APPEND, COPY into folder? + bool GetCanUserPostToFolder(const nsACString& userName); // Can I send mail to the submission address for folder? + bool GetCanUserCreateSubfolder(const nsACString& userName); // Can I CREATE a subfolder of this folder? + bool GetCanUserDeleteInFolder(const nsACString& userName); // STORE DELETED flag, perform EXPUNGE? + bool GetCanUserAdministerFolder(const nsACString& userName); // perform SETACL? + + // Functions to find out rights for the currently authenticated user. + + bool GetCanILookupFolder(); // Is folder visible to LIST/LSUB? + bool GetCanIReadFolder(); // SELECT, CHECK, FETCH, PARTIAL, SEARCH, COPY from folder? + bool GetCanIStoreSeenInFolder(); // STORE SEEN flag? + bool GetCanIWriteFolder(); // STORE flags other than SEEN and DELETED? + bool GetCanIInsertInFolder(); // APPEND, COPY into folder? + bool GetCanIPostToFolder(); // Can I send mail to the submission address for folder? + bool GetCanICreateSubfolder(); // Can I CREATE a subfolder of this folder? + bool GetCanIDeleteInFolder(); // STORE DELETED flag? + bool GetCanIAdministerFolder(); // perform SETACL? + bool GetCanIExpungeFolder(); // perform EXPUNGE? + // clang-format on + + bool GetDoIHaveFullRightsForFolder(); // Returns TRUE if I have full rights + // on this folder (all of the above + // return TRUE) + + bool GetIsFolderShared(); // We use this to see if the ACLs think a folder is + // shared or not. + // We will define "Shared" in 5.0 to mean: + // At least one user other than the currently authenticated user has at least + // one explicitly-listed ACL right on that folder. + + // Returns a newly allocated string describing these rights + nsresult CreateACLRightsString(nsAString& rightsString); + + nsresult GetRightsStringForUser(const nsACString& userName, + nsCString& rights); + + nsresult GetOtherUsers(nsIUTF8StringEnumerator** aResult); + + protected: + bool GetFlagSetInRightsForUser(const nsACString& userName, char flag, + bool defaultIfNotFound); + void BuildInitialACLFromCache(); + void UpdateACLCache(); + + protected: + nsTHashMap<nsCStringHashKey, nsCString> + m_rightsHash; // Hash table, mapping username strings to rights strings. + nsImapMailFolder* m_folder; + int32_t m_aclCount; +}; + +/** + * Encapsulates parameters required to playback offline ops + * on given folder. + */ +struct nsPlaybackRequest { + explicit nsPlaybackRequest(nsImapMailFolder* srcFolder, + nsIMsgWindow* msgWindow) + : SrcFolder(srcFolder), MsgWindow(msgWindow) {} + nsImapMailFolder* SrcFolder; + nsCOMPtr<nsIMsgWindow> MsgWindow; +}; + +class nsMsgQuota final : public nsIMsgQuota { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIMSGQUOTA + + nsMsgQuota(const nsACString& aName, const uint64_t& aUsage, + const uint64_t& aLimit); + + protected: + ~nsMsgQuota(); + + nsCString mName; + uint64_t mUsage, mLimit; +}; + +class nsImapMailFolder : public nsMsgDBFolder, + public nsIMsgImapMailFolder, + public nsIImapMailFolderSink, + public nsIImapMessageSink, + public nsICopyMessageListener, + public nsIMsgFilterHitNotify { + static const uint32_t PLAYBACK_TIMER_INTERVAL_IN_MS = 500; + + public: + nsImapMailFolder(); + + NS_DECL_ISUPPORTS_INHERITED + + // nsIMsgFolder methods: + NS_IMETHOD GetSubFolders(nsTArray<RefPtr<nsIMsgFolder>>& folders) override; + + NS_IMETHOD UpdateFolder(nsIMsgWindow* aWindow) override; + + NS_IMETHOD CreateSubfolder(const nsAString& folderName, + nsIMsgWindow* msgWindow) override; + NS_IMETHOD AddSubfolder(const nsAString& aName, + nsIMsgFolder** aChild) override; + NS_IMETHODIMP CreateStorageIfMissing(nsIUrlListener* urlListener) override; + + NS_IMETHOD Compact(nsIUrlListener* aListener, + nsIMsgWindow* aMsgWindow) override; + NS_IMETHOD CompactAll(nsIUrlListener* aListener, + nsIMsgWindow* aMsgWindow) override; + NS_IMETHOD EmptyTrash(nsIUrlListener* aListener) override; + NS_IMETHOD CopyDataToOutputStreamForAppend( + nsIInputStream* aIStream, int32_t aLength, + nsIOutputStream* outputStream) override; + NS_IMETHOD CopyDataDone() override; + NS_IMETHOD DeleteStorage() override; + NS_IMETHOD Rename(const nsAString& newName, nsIMsgWindow* msgWindow) override; + NS_IMETHOD RenameSubFolders(nsIMsgWindow* msgWindow, + nsIMsgFolder* oldFolder) override; + NS_IMETHOD GetNoSelect(bool* aResult) override; + + NS_IMETHOD GetPrettyName(nsAString& prettyName) + override; // Override of the base, for top-level mail folder + + NS_IMETHOD GetFolderURL(nsACString& url) override; + + NS_IMETHOD UpdateSummaryTotals(bool force) override; + + NS_IMETHOD GetDeletable(bool* deletable) override; + + NS_IMETHOD GetSizeOnDisk(int64_t* size) override; + + NS_IMETHOD GetCanCreateSubfolders(bool* aResult) override; + NS_IMETHOD GetCanSubscribe(bool* aResult) override; + + NS_IMETHOD ApplyRetentionSettings() override; + + NS_IMETHOD AddMessageDispositionState( + nsIMsgDBHdr* aMessage, nsMsgDispositionState aDispositionFlag) override; + NS_IMETHOD MarkMessagesRead(const nsTArray<RefPtr<nsIMsgDBHdr>>& messages, + bool markRead) override; + NS_IMETHOD MarkAllMessagesRead(nsIMsgWindow* aMsgWindow) override; + NS_IMETHOD MarkMessagesFlagged(const nsTArray<RefPtr<nsIMsgDBHdr>>& messages, + bool markFlagged) override; + NS_IMETHOD MarkThreadRead(nsIMsgThread* thread) override; + NS_IMETHOD SetJunkScoreForMessages( + const nsTArray<RefPtr<nsIMsgDBHdr>>& aMessages, + const nsACString& aJunkScore) override; + NS_IMETHOD DeleteSelf(nsIMsgWindow* msgWindow) override; + NS_IMETHOD ReadFromFolderCacheElem( + nsIMsgFolderCacheElement* element) override; + NS_IMETHOD WriteToFolderCacheElem(nsIMsgFolderCacheElement* element) override; + + NS_IMETHOD GetDBFolderInfoAndDB(nsIDBFolderInfo** folderInfo, + nsIMsgDatabase** db) override; + MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD + DeleteMessages(nsTArray<RefPtr<nsIMsgDBHdr>> const& msgHeaders, + nsIMsgWindow* msgWindow, bool deleteStorage, bool isMove, + nsIMsgCopyServiceListener* listener, bool allowUndo) override; + NS_IMETHOD CopyMessages(nsIMsgFolder* srcFolder, + nsTArray<RefPtr<nsIMsgDBHdr>> const& messages, + bool isMove, nsIMsgWindow* msgWindow, + nsIMsgCopyServiceListener* listener, bool isFolder, + bool allowUndo) override; + NS_IMETHOD CopyFolder(nsIMsgFolder* srcFolder, bool isMove, + nsIMsgWindow* msgWindow, + nsIMsgCopyServiceListener* listener) override; + NS_IMETHOD CopyFileMessage(nsIFile* file, nsIMsgDBHdr* msgToReplace, + bool isDraftOrTemplate, uint32_t aNewMsgFlags, + const nsACString& aNewMsgKeywords, + nsIMsgWindow* msgWindow, + nsIMsgCopyServiceListener* listener) override; + NS_IMETHOD GetNewMessages(nsIMsgWindow* aWindow, + nsIUrlListener* aListener) override; + + NS_IMETHOD GetFilePath(nsIFile** aPathName) override; + NS_IMETHOD SetFilePath(nsIFile* aPath) override; + + NS_IMETHOD Shutdown(bool shutdownChildren) override; + + NS_IMETHOD DownloadMessagesForOffline( + nsTArray<RefPtr<nsIMsgDBHdr>> const& messages, + nsIMsgWindow* msgWindow) override; + + NS_IMETHOD DownloadAllForOffline(nsIUrlListener* listener, + nsIMsgWindow* msgWindow) override; + NS_IMETHOD GetCanFileMessages(bool* aCanFileMessages) override; + NS_IMETHOD GetCanDeleteMessages(bool* aCanDeleteMessages) override; + NS_IMETHOD FetchMsgPreviewText(nsTArray<nsMsgKey> const& aKeysToFetch, + nsIUrlListener* aUrlListener, + bool* aAsyncResults) override; + + NS_IMETHOD AddKeywordsToMessages( + const nsTArray<RefPtr<nsIMsgDBHdr>>& aMessages, + const nsACString& aKeywords) override; + NS_IMETHOD RemoveKeywordsFromMessages( + const nsTArray<RefPtr<nsIMsgDBHdr>>& aMessages, + const nsACString& aKeywords) override; + + NS_IMETHOD NotifyCompactCompleted() override; + + // overrides nsMsgDBFolder::HasMsgOffline() + NS_IMETHOD HasMsgOffline(nsMsgKey msgKey, bool* _retval) override; + NS_IMETHOD GetLocalMsgStream(nsIMsgDBHdr* hdr, + nsIInputStream** stream) override; + + NS_DECL_NSIMSGIMAPMAILFOLDER + NS_DECL_NSIIMAPMAILFOLDERSINK + NS_DECL_NSIIMAPMESSAGESINK + NS_DECL_NSICOPYMESSAGELISTENER + + // nsIUrlListener methods + NS_IMETHOD OnStartRunningUrl(nsIURI* aUrl) override; + MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD + OnStopRunningUrl(nsIURI* aUrl, nsresult aExitCode) override; + + NS_DECL_NSIMSGFILTERHITNOTIFY + NS_DECL_NSIJUNKMAILCLASSIFICATIONLISTENER + + NS_IMETHOD IsCommandEnabled(const nsACString& command, bool* result) override; + NS_IMETHOD SetFilterList(nsIMsgFilterList* aMsgFilterList) override; + NS_IMETHOD GetCustomIdentity(nsIMsgIdentity** aIdentity) override; + + NS_IMETHOD GetIncomingServerType(nsACString& serverType) override; + + nsresult AddSubfolderWithPath(nsAString& name, nsIFile* dbPath, + nsIMsgFolder** child, bool brandNew = false); + nsresult MoveIncorporatedMessage(nsIMsgDBHdr* mailHdr, + nsIMsgDatabase* sourceDB, + const nsACString& destFolder, + nsIMsgFilter* filter, + nsIMsgWindow* msgWindow); + + // send notification to copy service listener. + nsresult OnCopyCompleted(nsISupports* srcSupport, nsresult exitCode); + + static nsresult AllocateUidStringFromKeys(const nsTArray<nsMsgKey>& keys, + nsCString& msgIds); + static nsresult BuildIdsAndKeyArray( + const nsTArray<RefPtr<nsIMsgDBHdr>>& messages, nsCString& msgIds, + nsTArray<nsMsgKey>& keyArray); + + // these might end up as an nsIImapMailFolder attribute. + nsresult SetSupportedUserFlags(uint32_t userFlags); + nsresult GetSupportedUserFlags(uint32_t* userFlags); + + // Find the start of a range of msgKeys that can hold srcCount headers. + nsresult FindOpenRange(nsMsgKey& fakeBase, uint32_t srcCount); + + protected: + virtual ~nsImapMailFolder(); + // Helper methods + + nsresult ExpungeAndCompact(nsIUrlListener* aListener, + nsIMsgWindow* aMsgWindow); + virtual nsresult CreateChildFromURI(const nsACString& uri, + nsIMsgFolder** folder) override; + void FindKeysToAdd(const nsTArray<nsMsgKey>& existingKeys, + nsTArray<nsMsgKey>& keysToFetch, uint32_t& numNewUnread, + nsIImapFlagAndUidState* flagState); + void FindKeysToDelete(const nsTArray<nsMsgKey>& existingKeys, + nsTArray<nsMsgKey>& keysToFetch, + nsIImapFlagAndUidState* flagState, uint32_t boxFlags); + void PrepareToAddHeadersToMailDB(nsIImapProtocol* aProtocol); + void TweakHeaderFlags(nsIImapProtocol* aProtocol, nsIMsgDBHdr* tweakMe); + + nsresult SyncFlags(nsIImapFlagAndUidState* flagState); + nsresult HandleCustomFlags(nsMsgKey uidOfMessage, nsIMsgDBHdr* dbHdr, + uint16_t userFlags, nsCString& keywords); + nsresult NotifyMessageFlagsFromHdr(nsIMsgDBHdr* dbHdr, nsMsgKey msgKey, + uint32_t flags); + + nsresult SetupHeaderParseStream(uint32_t size, const nsACString& content_type, + nsIMailboxSpec* boxSpec); + nsresult ParseAdoptedHeaderLine(const char* messageLine, nsMsgKey msgKey); + nsresult NormalEndHeaderParseStream(nsIImapProtocol* aProtocol, + nsIImapUrl* imapUrl); + + void EndOfflineDownload(); + + /** + * At the end of a file-to-folder copy operation, copy the file to the + * offline store and/or add to the message database, (if needed). + * + * @param srcFile file containing the message key + * @param msgKey key to use for the new messages + */ + nsresult CopyFileToOfflineStore(nsIFile* srcFile, nsMsgKey msgKey); + + nsresult MarkMessagesImapDeleted(nsTArray<nsMsgKey>* keyArray, bool 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, bool markDeleted); + virtual bool ShowDeletedMessages(); + virtual bool DeleteIsMoveToTrash(); + nsresult GetFolder(const nsACString& name, nsIMsgFolder** pFolder); + nsresult GetTrashFolder(nsIMsgFolder** pTrashFolder); + bool TrashOrDescendentOfTrash(nsIMsgFolder* folder); + static bool ShouldCheckAllFolders(nsIImapIncomingServer* imapServer); + nsresult GetServerKey(nsACString& serverKey); + nsresult DisplayStatusMsg(nsIImapUrl* aImapUrl, const nsAString& msg); + + // nsresult RenameLocal(const char *newName); + nsresult AddDirectorySeparator(nsIFile* path); + nsresult CreateSubFolders(nsIFile* path); + nsresult GetDatabase() override; + + nsresult GetFolderOwnerUserName(nsACString& userName); + nsImapNamespace* GetNamespaceForFolder(); + void SetNamespaceForFolder(nsImapNamespace* ns); + + nsMsgIMAPFolderACL* GetFolderACL(); + nsresult CreateACLRightsStringForFolder(nsAString& rightsString); + nsresult GetBodysToDownload(nsTArray<nsMsgKey>* keysOfMessagesToDownload); + // Uber message copy service + nsresult CopyMessagesWithStream(nsIMsgFolder* srcFolder, + nsTArray<RefPtr<nsIMsgDBHdr>> const& messages, + bool isMove, bool isCrossServerOp, + nsIMsgWindow* msgWindow, + nsIMsgCopyServiceListener* listener, + bool allowUndo); + nsresult CopyStreamMessage(nsIMsgDBHdr* message, nsIMsgFolder* dstFolder, + nsIMsgWindow* msgWindow, bool isMove); + nsresult InitCopyState(nsISupports* srcSupport, + nsTArray<RefPtr<nsIMsgDBHdr>> const& messages, + bool isMove, bool selectedState, bool acrossServers, + uint32_t newMsgFlags, const nsACString& newMsgKeywords, + nsIMsgCopyServiceListener* listener, + nsIMsgWindow* msgWindow, bool allowUndo); + nsresult GetMoveCoalescer(); + nsresult PlaybackCoalescedOperations(); + virtual nsresult CreateBaseMessageURI(const nsACString& aURI) override; + // offline-ish methods + nsresult GetClearedOriginalOp(nsIMsgOfflineImapOperation* op, + nsIMsgOfflineImapOperation** originalOp, + nsIMsgDatabase** originalDB); + nsresult GetOriginalOp(nsIMsgOfflineImapOperation* op, + nsIMsgOfflineImapOperation** originalOp, + nsIMsgDatabase** originalDB); + MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult CopyMessagesOffline( + nsIMsgFolder* srcFolder, nsTArray<RefPtr<nsIMsgDBHdr>> const& messages, + bool isMove, nsIMsgWindow* msgWindow, + nsIMsgCopyServiceListener* listener); + void SetPendingAttributes(const nsTArray<RefPtr<nsIMsgDBHdr>>& messages, + bool aIsMove, bool aSetOffline); + + nsresult CopyOfflineMsgBody(nsIMsgFolder* srcFolder, nsIMsgDBHdr* destHdr, + nsIMsgDBHdr* origHdr, nsIInputStream* inputStream, + nsIOutputStream* outputStream); + + void GetTrashFolderName(nsAString& aFolderName); + bool ShowPreviewText(); + + // Pseudo-Offline operation playback timer + static void PlaybackTimerCallback(nsITimer* aTimer, void* aClosure); + + // Allocate and initialize associated auto-sync state object. + void InitAutoSyncState(); + + virtual nsresult GetOfflineFileStream(nsMsgKey msgKey, uint64_t* offset, + uint32_t* size, + nsIInputStream** aFileStream) override; + + bool m_initialized; + bool m_haveDiscoveredAllFolders; + nsCOMPtr<nsIMsgParseMailMsgState> m_msgParser; + nsCOMPtr<nsIMsgFilterList> m_filterList; + nsCOMPtr<nsIMsgFilterPlugin> m_filterPlugin; // XXX should be a list + // used with filter plugins to know when we've finished classifying and can + // playback moves + bool m_msgMovedByFilter; + RefPtr<nsImapMoveCoalescer> + m_moveCoalescer; // strictly owned by the nsImapMailFolder + nsTArray<RefPtr<nsIMsgDBHdr>> m_junkMessagesToMarkAsRead; + /// list of keys to be moved to the junk folder + nsTArray<nsMsgKey> mSpamKeysToMove; + /// the junk destination folder + nsCOMPtr<nsIMsgFolder> mSpamFolder; + nsMsgKey m_curMsgUid; + uint32_t m_uidValidity; + + // These three vars are used to store counts from STATUS or SELECT command + // They include deleted messages, so they can differ from the generic + // folder total and unread counts. + int32_t m_numServerRecentMessages; + int32_t m_numServerUnseenMessages; + int32_t m_numServerTotalMessages; + // if server supports UIDNEXT, we store it here. + int32_t m_nextUID; + + int32_t m_nextMessageByteLength; + nsCOMPtr<nsIUrlListener> m_urlListener; + bool m_urlRunning; + + // undo move/copy transaction support + RefPtr<nsMsgTxn> m_pendingUndoTxn; + RefPtr<nsImapMailCopyState> m_copyState; + char m_hierarchyDelimiter; + int32_t m_boxFlags; + nsCString m_onlineFolderName; + nsCString m_ownerUserName; // username of the "other user," as in + // "Other Users' Mailboxes" + + nsCString m_adminUrl; // url to run to set admin privileges for this folder + nsImapNamespace* m_namespace; + bool m_verifiedAsOnlineFolder; + bool m_explicitlyVerify; // whether or not we need to explicitly verify this + // through LIST + bool m_folderIsNamespace; + bool m_folderNeedsSubscribing; + bool m_folderNeedsAdded; + bool m_folderNeedsACLListed; + bool m_performingBiff; + bool m_updatingFolder; + bool m_applyIncomingFilters; // apply filters to this folder, even if not the + // inbox + nsMsgIMAPFolderACL* m_folderACL; + uint32_t m_aclFlags; + uint32_t m_supportedUserFlags; + + // determines if we are on GMail server + bool m_isGmailServer; + // offline imap support + bool m_downloadingFolderForOfflineUse; + bool m_filterListRequiresBody; + + // auto-sync (automatic message download) support + RefPtr<nsAutoSyncState> m_autoSyncStateObj; + + // Quota support. + nsTArray<RefPtr<nsIMsgQuota>> m_folderQuota; + bool m_folderQuotaCommandIssued; + bool m_folderQuotaDataIsValid; + + // Pseudo-Offline Playback support + nsPlaybackRequest* m_pendingPlaybackReq; + nsCOMPtr<nsITimer> m_playbackTimer; + nsTArray<RefPtr<nsImapMoveCopyMsgTxn>> m_pendingOfflineMoves; + // hash table of mapping between messageids and message keys + // for pseudo hdrs. + nsTHashMap<nsCStringHashKey, nsMsgKey> m_pseudoHdrs; + + nsTArray<nsMsgKey> m_keysToFetch; + uint32_t m_totalKeysToFetch; + + /** + * delete if appropriate local storage for messages in this folder + * + * @parm aMessages array (of nsIMsgDBHdr) of messages to delete + * (or an array of message keys) + * @parm aSrcFolder the folder containing the messages (optional) + */ + void DeleteStoreMessages(const nsTArray<RefPtr<nsIMsgDBHdr>>& aMessages); + void DeleteStoreMessages(const nsTArray<nsMsgKey>& aMessages); + static void DeleteStoreMessages(const nsTArray<nsMsgKey>& aMessages, + nsIMsgFolder* aFolder); + /** + * This method is used to locate a folder where a msg could be present, not + * just the folder where the message first arrives, this method searches for + * the existence of msg in all the folders/labels that we retrieve from + * X-GM-LABELS also. + * @param msgKey key of the msg for which we are trying to get the folder; + * @param aMsgFolder required folder; + */ + nsresult GetOfflineMsgFolder(nsMsgKey msgKey, nsIMsgFolder** aMsgFolder); +}; +#endif diff --git a/comm/mailnews/imap/src/nsImapNamespace.cpp b/comm/mailnews/imap/src/nsImapNamespace.cpp new file mode 100644 index 0000000000..4fd2dcf7db --- /dev/null +++ b/comm/mailnews/imap/src/nsImapNamespace.cpp @@ -0,0 +1,513 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" // for pre-compiled headers + +#include "nsImapCore.h" +#include "nsImapNamespace.h" +#include "../public/nsIImapHostSessionList.h" +#include "nsImapUrl.h" +#include "nsString.h" +#include "nsServiceManagerUtils.h" + +//////////////////// nsImapNamespace +//////////////////////////////////////////////////////////////// + +#define NS_IIMAPHOSTSESSIONLIST_CID \ + { \ + 0x479ce8fc, 0xe725, 0x11d2, { \ + 0xa5, 0x05, 0x00, 0x60, 0xb0, 0xfc, 0x04, 0xb7 \ + } \ + } +static NS_DEFINE_CID(kCImapHostSessionListCID, NS_IIMAPHOSTSESSIONLIST_CID); + +nsImapNamespace::nsImapNamespace(EIMAPNamespaceType type, const char* prefix, + char delimiter, bool from_prefs) { + m_namespaceType = type; + m_prefix = PL_strdup(prefix); + m_fromPrefs = from_prefs; + + m_delimiter = delimiter; + m_delimiterFilledIn = + !m_fromPrefs; // if it's from the prefs, we can't be sure about the + // delimiter until we list it. +} + +nsImapNamespace::~nsImapNamespace() { PR_FREEIF(m_prefix); } + +void nsImapNamespace::SetDelimiter(char delimiter, bool delimiterFilledIn) { + m_delimiter = delimiter; + m_delimiterFilledIn = delimiterFilledIn; +} + +// returns -1 if this box is not part of this namespace, +// or the length of the prefix if it is part of this namespace +int nsImapNamespace::MailboxMatchesNamespace(const char* boxname) { + if (!boxname) return -1; + + // If the namespace is part of the boxname + if (!m_prefix || !*m_prefix) return 0; + + if (PL_strstr(boxname, m_prefix) == boxname) return PL_strlen(m_prefix); + + // If the boxname is part of the prefix + // (Used for matching Personal mailbox with Personal/ namespace, etc.) + if (PL_strstr(m_prefix, boxname) == m_prefix) return PL_strlen(boxname); + return -1; +} + +nsImapNamespaceList* nsImapNamespaceList::CreatensImapNamespaceList() { + nsImapNamespaceList* rv = new nsImapNamespaceList(); + return rv; +} + +nsImapNamespaceList::nsImapNamespaceList() {} + +int nsImapNamespaceList::GetNumberOfNamespaces() { + return m_NamespaceList.Length(); +} + +nsresult nsImapNamespaceList::InitFromString(const char* nameSpaceString, + EIMAPNamespaceType nstype) { + nsresult rv = NS_OK; + if (nameSpaceString) { + int numNamespaces = UnserializeNamespaces(nameSpaceString, nullptr, 0); + char** prefixes = (char**)PR_CALLOC(numNamespaces * sizeof(char*)); + if (prefixes) { + int len = UnserializeNamespaces(nameSpaceString, prefixes, numNamespaces); + for (int i = 0; i < len; i++) { + char* thisns = prefixes[i]; + char delimiter = '/'; // a guess + if (PL_strlen(thisns) >= 1) delimiter = thisns[PL_strlen(thisns) - 1]; + nsImapNamespace* ns = + new nsImapNamespace(nstype, thisns, delimiter, true); + if (ns) AddNewNamespace(ns); + PR_FREEIF(thisns); + } + PR_Free(prefixes); + } + } + + return rv; +} + +nsresult nsImapNamespaceList::OutputToString(nsCString& string) { + nsresult rv = NS_OK; + return rv; +} + +int nsImapNamespaceList::GetNumberOfNamespaces(EIMAPNamespaceType type) { + int nodeIndex = 0, count = 0; + for (nodeIndex = m_NamespaceList.Length() - 1; nodeIndex >= 0; nodeIndex--) { + nsImapNamespace* nspace = m_NamespaceList.ElementAt(nodeIndex); + if (nspace->GetType() == type) { + count++; + } + } + return count; +} + +int nsImapNamespaceList::AddNewNamespace(nsImapNamespace* ns) { + // If the namespace is from the NAMESPACE response, then we should see if + // there are any namespaces previously set by the preferences, or the default + // namespace. If so, remove these. + + if (!ns->GetIsNamespaceFromPrefs()) { + int nodeIndex; + // iterate backwards because we delete elements + for (nodeIndex = m_NamespaceList.Length() - 1; nodeIndex >= 0; + nodeIndex--) { + nsImapNamespace* nspace = m_NamespaceList.ElementAt(nodeIndex); + // if we find existing namespace(s) that matches the + // new one, we'll just remove the old ones and let the + // new one get added when we've finished checking for + // matching namespaces or namespaces that came from prefs. + if (nspace && (nspace->GetIsNamespaceFromPrefs() || + (!PL_strcmp(ns->GetPrefix(), nspace->GetPrefix()) && + ns->GetType() == nspace->GetType() && + ns->GetDelimiter() == nspace->GetDelimiter()))) { + m_NamespaceList.RemoveElementAt(nodeIndex); + delete nspace; + } + } + } + + // Add the new namespace to the list. This must come after the removing code, + // or else we could never add the initial kDefaultNamespace type to the list. + m_NamespaceList.AppendElement(ns); + + return 0; +} + +// chrisf - later, fix this to know the real concept of "default" namespace of a +// given type +nsImapNamespace* nsImapNamespaceList::GetDefaultNamespaceOfType( + EIMAPNamespaceType type) { + nsImapNamespace *rv = 0, *firstOfType = 0; + + int nodeIndex, count = m_NamespaceList.Length(); + for (nodeIndex = 0; nodeIndex < count && !rv; nodeIndex++) { + nsImapNamespace* ns = m_NamespaceList.ElementAt(nodeIndex); + if (ns->GetType() == type) { + if (!firstOfType) firstOfType = ns; + if (!(*(ns->GetPrefix()))) { + // This namespace's prefix is "" + // Therefore it is the default + rv = ns; + } + } + } + if (!rv) rv = firstOfType; + return rv; +} + +nsImapNamespaceList::~nsImapNamespaceList() { + ClearNamespaces(true, true, true); +} + +// ClearNamespaces removes and deletes the namespaces specified, and if there +// are no namespaces left, +void nsImapNamespaceList::ClearNamespaces(bool deleteFromPrefsNamespaces, + bool deleteServerAdvertisedNamespaces, + bool reallyDelete) { + int nodeIndex; + + // iterate backwards because we delete elements + for (nodeIndex = m_NamespaceList.Length() - 1; nodeIndex >= 0; nodeIndex--) { + nsImapNamespace* ns = m_NamespaceList.ElementAt(nodeIndex); + if (ns->GetIsNamespaceFromPrefs()) { + if (deleteFromPrefsNamespaces) { + m_NamespaceList.RemoveElementAt(nodeIndex); + if (reallyDelete) delete ns; + } + } else if (deleteServerAdvertisedNamespaces) { + m_NamespaceList.RemoveElementAt(nodeIndex); + if (reallyDelete) delete ns; + } + } +} + +nsImapNamespace* nsImapNamespaceList::GetNamespaceNumber(int nodeIndex) { + NS_ASSERTION(nodeIndex >= 0 && nodeIndex < GetNumberOfNamespaces(), + "invalid IMAP namespace node index"); + if (nodeIndex < 0) nodeIndex = 0; + + // XXX really could be just ElementAt; that's why we have the assertion + return m_NamespaceList.SafeElementAt(nodeIndex); +} + +nsImapNamespace* nsImapNamespaceList::GetNamespaceNumber( + int nodeIndex, EIMAPNamespaceType type) { + int nodeCount, count = 0; + for (nodeCount = m_NamespaceList.Length() - 1; nodeCount >= 0; nodeCount--) { + nsImapNamespace* nspace = m_NamespaceList.ElementAt(nodeCount); + if (nspace->GetType() == type) { + count++; + if (count == nodeIndex) return nspace; + } + } + return nullptr; +} + +nsImapNamespace* nsImapNamespaceList::GetNamespaceForMailbox( + const char* boxname) { + // We want to find the LONGEST substring that matches the beginning of this + // mailbox's path. This accounts for nested namespaces (i.e. "Public/" and + // "Public/Users/") + + // Also, we want to match the namespace's mailbox to that namespace also: + // The Personal box will match the Personal/ namespace, etc. + + // these lists shouldn't be too long (99% chance there won't be more than 3 or + // 4) so just do a linear search + + int lengthMatched = -1; + int currentMatchedLength = -1; + nsImapNamespace* rv = nullptr; + int nodeIndex = 0; + + if (!PL_strcasecmp(boxname, "INBOX")) + return GetDefaultNamespaceOfType(kPersonalNamespace); + + for (nodeIndex = m_NamespaceList.Length() - 1; nodeIndex >= 0; nodeIndex--) { + nsImapNamespace* nspace = m_NamespaceList.ElementAt(nodeIndex); + currentMatchedLength = nspace->MailboxMatchesNamespace(boxname); + if (currentMatchedLength > lengthMatched) { + rv = nspace; + lengthMatched = currentMatchedLength; + } + } + + return rv; +} + +#define SERIALIZER_SEPARATORS "," + +/** + * If len is one, copies the first element of prefixes into + * serializedNamespaces. If len > 1, copies len strings from prefixes into + * serializedNamespaces as a comma-separated list of quoted strings. + */ +nsresult nsImapNamespaceList::SerializeNamespaces( + char** prefixes, int len, nsCString& serializedNamespaces) { + if (len <= 0) return NS_OK; + + if (len == 1) { + serializedNamespaces.Assign(prefixes[0]); + return NS_OK; + } + + for (int i = 0; i < len; i++) { + if (i > 0) serializedNamespaces.Append(','); + + serializedNamespaces.Append('"'); + serializedNamespaces.Append(prefixes[i]); + serializedNamespaces.Append('"'); + } + return NS_OK; +} + +/* str is the string which needs to be unserialized. + If prefixes is NULL, simply returns the number of namespaces in str. (len is + ignored) If prefixes is not NULL, it should be an array of length len which + is to be filled in with newly-allocated string. Returns the number of + strings filled in. +*/ +int nsImapNamespaceList::UnserializeNamespaces(const char* str, char** prefixes, + int len) { + if (!str) return 0; + if (!prefixes) { + if (str[0] != '"') return 1; + + int count = 0; + char* ourstr = PL_strdup(str); + char* origOurStr = ourstr; + if (ourstr) { + char* token = NS_strtok(SERIALIZER_SEPARATORS, &ourstr); + while (token != nullptr) { + token = NS_strtok(SERIALIZER_SEPARATORS, &ourstr); + count++; + } + PR_Free(origOurStr); + } + return count; + } + + if ((str[0] != '"') && (len >= 1)) { + prefixes[0] = PL_strdup(str); + return 1; + } + + int count = 0; + char* ourstr = PL_strdup(str); + char* origOurStr = ourstr; + if (ourstr) { + char* token = NS_strtok(SERIALIZER_SEPARATORS, &ourstr); + while ((count < len) && (token != nullptr)) { + char *current = PL_strdup(token), *where = current; + if (where[0] == '"') where++; + if (where[PL_strlen(where) - 1] == '"') where[PL_strlen(where) - 1] = 0; + prefixes[count] = PL_strdup(where); + PR_FREEIF(current); + token = NS_strtok(SERIALIZER_SEPARATORS, &ourstr); + count++; + } + PR_Free(origOurStr); + } + return count; +} + +// static +nsCString nsImapNamespaceList::AllocateCanonicalFolderName( + const char* onlineFolderName, char delimiter) { + nsCString canonicalPath; + if (delimiter) { + char* tmp = + nsImapUrl::ReplaceCharsInCopiedString(onlineFolderName, delimiter, '/'); + canonicalPath.Assign(tmp); + PR_Free(tmp); + } else { + canonicalPath.Assign(onlineFolderName); + } + canonicalPath.ReplaceSubstring("\\/", "/"); + return canonicalPath; +} + +nsImapNamespace* nsImapNamespaceList::GetNamespaceForFolder( + const char* hostName, const char* canonicalFolderName, char delimiter) { + if (!hostName || !canonicalFolderName) return nullptr; + + nsImapNamespace* resultNamespace = nullptr; + nsresult rv; + char* convertedFolderName = nsImapNamespaceList::AllocateServerFolderName( + canonicalFolderName, delimiter); + + if (convertedFolderName) { + nsCOMPtr<nsIImapHostSessionList> hostSessionList = + do_GetService(kCImapHostSessionListCID, &rv); + if (NS_FAILED(rv)) return nullptr; + hostSessionList->GetNamespaceForMailboxForHost( + hostName, convertedFolderName, resultNamespace); + PR_Free(convertedFolderName); + } else { + NS_ASSERTION(false, "couldn't get converted folder name"); + } + + return resultNamespace; +} + +/* static */ +char* nsImapNamespaceList::AllocateServerFolderName( + const char* canonicalFolderName, char delimiter) { + if (delimiter) + return nsImapUrl::ReplaceCharsInCopiedString(canonicalFolderName, '/', + delimiter); + return NS_xstrdup(canonicalFolderName); +} + +/* + GetFolderOwnerNameFromPath takes as inputs a folder name + in canonical form, and a namespace for that folder. + The namespace MUST be of type kOtherUsersNamespace, hence the folder MUST be + owned by another user. This function extracts the folder owner's name from + the canonical name of the folder, and returns the owner's name. +*/ +/* static */ +nsCString nsImapNamespaceList::GetFolderOwnerNameFromPath( + nsImapNamespace* namespaceForFolder, const char* canonicalFolderName) { + if (!namespaceForFolder || !canonicalFolderName) { + NS_ERROR("null namespace or canonical folder name"); + return ""_ns; + } + + // Convert the canonical path to the online path. + nsAutoCString convertedFolderName(canonicalFolderName); + char delimiter = namespaceForFolder->GetDelimiter(); + if (delimiter) { + convertedFolderName.ReplaceChar('/', delimiter); + } + + // Trim off the prefix. + uint32_t prefixLen = + nsDependentCString(namespaceForFolder->GetPrefix()).Length(); + if (convertedFolderName.Length() <= prefixLen) { + NS_ERROR("server folder name invalid"); + return ""_ns; + } + + // Trim off anything after the owner name. + nsCString owner(Substring(convertedFolderName, prefixLen)); + int32_t i = owner.FindChar(delimiter); + if (i != kNotFound) { + owner.Truncate(i); + } + return owner; +} + +/* +GetFolderIsNamespace returns TRUE if the given folder is the folder representing +a namespace. +*/ + +bool nsImapNamespaceList::GetFolderIsNamespace( + const char* hostName, const char* canonicalFolderName, char delimiter, + nsImapNamespace* namespaceForFolder) { + NS_ASSERTION(namespaceForFolder, "null namespace"); + + bool rv = false; + + const char* prefix = namespaceForFolder->GetPrefix(); + NS_ASSERTION(prefix, "namespace has no prefix"); + if (!prefix || !*prefix) // empty namespace prefix + return false; + + char* convertedFolderName = + AllocateServerFolderName(canonicalFolderName, delimiter); + if (convertedFolderName) { + bool lastCharIsDelimiter = (prefix[strlen(prefix) - 1] == delimiter); + + if (lastCharIsDelimiter) { + rv = ((strncmp(convertedFolderName, prefix, + strlen(convertedFolderName)) == 0) && + (strlen(convertedFolderName) == strlen(prefix) - 1)); + } else { + rv = (strcmp(convertedFolderName, prefix) == 0); + } + + PR_Free(convertedFolderName); + } else { + NS_ASSERTION(false, "couldn't allocate server folder name"); + } + + return rv; +} + +/* + SuggestHierarchySeparatorForNamespace takes a namespace from libmsg + and a hierarchy delimiter. If the namespace has not been filled in from + online NAMESPACE command yet, it fills in the suggested delimiter to be + used from then on (until it is overridden by an online response). +*/ + +void nsImapNamespaceList::SuggestHierarchySeparatorForNamespace( + nsImapNamespace* namespaceForFolder, char delimiterFromFolder) { + NS_ASSERTION(namespaceForFolder, "need namespace"); + if (namespaceForFolder && !namespaceForFolder->GetIsDelimiterFilledIn()) + namespaceForFolder->SetDelimiter(delimiterFromFolder, false); +} + +/* + GenerateFullFolderNameWithDefaultNamespace takes a folder name in canonical + form, converts to online form and calculates the full online server name + including the namespace prefix of the default namespace of the + given type, in the form: PR_smprintf("%s%s", prefix, onlineServerName) if + there is a NULL owner PR_smprintf("%s%s%c%s", prefix, owner, delimiter, + onlineServerName) if there is an owner. It then converts this back to + canonical form and returns it. + It returns empty string if there is no namespace of the given type. + If nsUsed is not passed in as NULL, then *nsUsed is filled in and returned; it + is the namespace used for generating the folder name. +*/ +nsCString nsImapNamespaceList::GenerateFullFolderNameWithDefaultNamespace( + const char* hostName, const char* canonicalFolderName, const char* owner, + EIMAPNamespaceType nsType, nsImapNamespace** nsUsed) { + nsresult rv = NS_OK; + + nsCOMPtr<nsIImapHostSessionList> hostSession = + do_GetService(kCImapHostSessionListCID, &rv); + NS_ENSURE_SUCCESS(rv, ""_ns); + nsImapNamespace* ns; + nsCString fullFolderName; + rv = hostSession->GetDefaultNamespaceOfTypeForHost(hostName, nsType, ns); + NS_ENSURE_SUCCESS(rv, ""_ns); + if (ns) { + if (nsUsed) *nsUsed = ns; + const char* prefix = ns->GetPrefix(); + char* convertedFolderName = + AllocateServerFolderName(canonicalFolderName, ns->GetDelimiter()); + if (convertedFolderName) { + char* convertedReturnName = nullptr; + if (owner) { + convertedReturnName = PR_smprintf( + "%s%s%c%s", prefix, owner, ns->GetDelimiter(), convertedFolderName); + } else { + convertedReturnName = PR_smprintf("%s%s", prefix, convertedFolderName); + } + + if (convertedReturnName) { + fullFolderName = AllocateCanonicalFolderName(convertedReturnName, + ns->GetDelimiter()); + PR_Free(convertedReturnName); + } + PR_Free(convertedFolderName); + } else { + NS_ASSERTION(false, "couldn't allocate server folder name"); + } + } else { + // Could not find other users namespace on the given host + NS_WARNING("couldn't find namespace for given host"); + } + return fullFolderName; +} diff --git a/comm/mailnews/imap/src/nsImapNamespace.h b/comm/mailnews/imap/src/nsImapNamespace.h new file mode 100644 index 0000000000..5d11d73120 --- /dev/null +++ b/comm/mailnews/imap/src/nsImapNamespace.h @@ -0,0 +1,87 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef _nsImapNamespace_H_ +#define _nsImapNamespace_H_ + +#include "nsImapCore.h" +#include "nsTArray.h" + +class nsImapNamespace { + public: + nsImapNamespace(EIMAPNamespaceType type, const char* prefix, char delimiter, + bool from_prefs); + + ~nsImapNamespace(); + + EIMAPNamespaceType GetType() { return m_namespaceType; } + const char* GetPrefix() { return m_prefix; } + char GetDelimiter() { return m_delimiter; } + void SetDelimiter(char delimiter, bool delimiterFilledIn); + bool GetIsDelimiterFilledIn() { return m_delimiterFilledIn; } + bool GetIsNamespaceFromPrefs() { return m_fromPrefs; } + + // returns -1 if this box is not part of this namespace, + // or the length of the prefix if it is part of this namespace + int MailboxMatchesNamespace(const char* boxname); + + protected: + EIMAPNamespaceType m_namespaceType; + char* m_prefix; + char m_delimiter; + bool m_fromPrefs; + bool m_delimiterFilledIn; +}; + +// represents an array of namespaces for a given host +class nsImapNamespaceList { + public: + ~nsImapNamespaceList(); + + static nsImapNamespaceList* CreatensImapNamespaceList(); + + nsresult InitFromString(const char* nameSpaceString, + EIMAPNamespaceType nstype); + nsresult OutputToString(nsCString& OutputString); + int UnserializeNamespaces(const char* str, char** prefixes, int len); + nsresult SerializeNamespaces(char** prefixes, int len, + nsCString& serializedNamespace); + + void ClearNamespaces(bool deleteFromPrefsNamespaces, + bool deleteServerAdvertisedNamespaces, + bool reallyDelete); + int GetNumberOfNamespaces(); + int GetNumberOfNamespaces(EIMAPNamespaceType); + nsImapNamespace* GetNamespaceNumber(int nodeIndex); + nsImapNamespace* GetNamespaceNumber(int nodeIndex, EIMAPNamespaceType); + + nsImapNamespace* GetDefaultNamespaceOfType(EIMAPNamespaceType type); + int AddNewNamespace(nsImapNamespace* ns); + nsImapNamespace* GetNamespaceForMailbox(const char* boxname); + static nsImapNamespace* GetNamespaceForFolder(const char* hostName, + const char* canonicalFolderName, + char delimiter); + static bool GetFolderIsNamespace(const char* hostName, + const char* canonicalFolderName, + char delimiter, + nsImapNamespace* namespaceForFolder); + static nsCString GetFolderOwnerNameFromPath( + nsImapNamespace* namespaceForFolder, const char* canonicalFolderName); + static void SuggestHierarchySeparatorForNamespace( + nsImapNamespace* namespaceForFolder, char delimiterFromFolder); + static nsCString GenerateFullFolderNameWithDefaultNamespace( + const char* hostName, const char* canonicalFolderName, const char* owner, + EIMAPNamespaceType nsType, nsImapNamespace** nsUsed); + + protected: + static char* AllocateServerFolderName(const char* canonicalFolderName, + char delimiter); + static nsCString AllocateCanonicalFolderName(const char* onlineFolderName, + char delimiter); + nsImapNamespaceList(); // use CreatensImapNamespaceList to create one + + nsTArray<nsImapNamespace*> m_NamespaceList; +}; +#endif diff --git a/comm/mailnews/imap/src/nsImapOfflineSync.cpp b/comm/mailnews/imap/src/nsImapOfflineSync.cpp new file mode 100644 index 0000000000..6b43a106bb --- /dev/null +++ b/comm/mailnews/imap/src/nsImapOfflineSync.cpp @@ -0,0 +1,1175 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" +#include "netCore.h" +#include "nsNetUtil.h" +#include "nsImapOfflineSync.h" +#include "nsImapMailFolder.h" +#include "nsMsgFolderFlags.h" +#include "nsMsgMessageFlags.h" +#include "nsIMsgMailNewsUrl.h" +#include "nsIMsgAccountManager.h" +#include "nsINntpIncomingServer.h" +#include "nsDirectoryServiceDefs.h" +#include "nsISeekableStream.h" +#include "nsIMsgCopyService.h" +#include "nsImapProtocol.h" +#include "nsMsgUtils.h" +#include "nsIAutoSyncManager.h" +#include "mozilla/Unused.h" + +NS_IMPL_ISUPPORTS(nsImapOfflineSync, nsIUrlListener, nsIMsgCopyServiceListener, + nsIDBChangeListener, nsIImapOfflineSync) + +nsImapOfflineSync::nsImapOfflineSync() { + m_singleFolderToUpdate = nullptr; + m_window = nullptr; + mCurrentPlaybackOpType = nsIMsgOfflineImapOperation::kFlagsChanged; + m_mailboxupdatesStarted = false; + m_mailboxupdatesFinished = false; + m_createdOfflineFolders = false; + m_pseudoOffline = false; + m_KeyIndex = 0; + mCurrentUIDValidity = nsMsgKey_None; + m_listener = nullptr; +} + +NS_IMETHODIMP +nsImapOfflineSync::Init(nsIMsgWindow* window, nsIUrlListener* listener, + nsIMsgFolder* singleFolderOnly, bool isPseudoOffline) { + m_window = window; + m_listener = listener; + m_singleFolderToUpdate = singleFolderOnly; + m_pseudoOffline = isPseudoOffline; + + // not the perfect place for this, but I think it will work. + if (m_window) m_window->SetStopped(false); + + return NS_OK; +} + +nsImapOfflineSync::~nsImapOfflineSync() {} + +void nsImapOfflineSync::SetWindow(nsIMsgWindow* window) { m_window = window; } + +NS_IMETHODIMP nsImapOfflineSync::OnStartRunningUrl(nsIURI* url) { + return NS_OK; +} + +NS_IMETHODIMP +nsImapOfflineSync::OnStopRunningUrl(nsIURI* url, nsresult exitCode) { + nsresult rv = exitCode; + + // where do we make sure this gets cleared when we start running urls? + bool stopped = false; + if (m_window) m_window->GetStopped(&stopped); + + if (m_curTempFile) { + m_curTempFile->Remove(false); + m_curTempFile = nullptr; + } + // NS_BINDING_ABORTED is used for the user pressing stop, which + // should cause us to abort the offline process. Other errors + // should allow us to continue. + if (stopped) { + if (m_listener) m_listener->OnStopRunningUrl(url, NS_BINDING_ABORTED); + return NS_OK; + } + nsCOMPtr<nsIImapUrl> imapUrl = do_QueryInterface(url); + + if (imapUrl) + nsImapProtocol::LogImapUrl(NS_SUCCEEDED(rv) ? "offline imap url succeeded " + : "offline imap url failed ", + imapUrl); + + // If we succeeded, or it was an imap move/copy that timed out, clear the + // operation. + bool moveCopy = + mCurrentPlaybackOpType == nsIMsgOfflineImapOperation::kMsgCopy || + mCurrentPlaybackOpType == nsIMsgOfflineImapOperation::kMsgMoved; + if (NS_SUCCEEDED(exitCode) || exitCode == NS_MSG_ERROR_IMAP_COMMAND_FAILED || + (moveCopy && exitCode == NS_ERROR_NET_TIMEOUT)) { + ClearCurrentOps(); + rv = ProcessNextOperation(); + } + // else if it's a non-stop error, and we're doing multiple folders, + // go to the next folder. + else if (!m_singleFolderToUpdate) { + if (AdvanceToNextFolder()) + rv = ProcessNextOperation(); + else if (m_listener) + m_listener->OnStopRunningUrl(url, rv); + } + + return rv; +} + +/** + * Leaves m_currentServer at the next imap or local mail "server" that + * might have offline events to playback, and m_folderQueue holding + * a (reversed) list of all the folders to consider for that server. + * If no more servers, m_currentServer will be left at nullptr and the + * function returns false. + */ +bool nsImapOfflineSync::AdvanceToNextServer() { + nsresult rv = NS_OK; + + if (m_allServers.IsEmpty()) { + NS_ASSERTION(!m_currentServer, "this shouldn't be set"); + m_currentServer = nullptr; + nsCOMPtr<nsIMsgAccountManager> accountManager = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + NS_ASSERTION(accountManager && NS_SUCCEEDED(rv), + "couldn't get account mgr"); + if (!accountManager || NS_FAILED(rv)) return false; + + rv = accountManager->GetAllServers(m_allServers); + NS_ENSURE_SUCCESS(rv, false); + } + size_t serverIndex = 0; + if (m_currentServer) { + serverIndex = m_allServers.IndexOf(m_currentServer); + if (serverIndex == m_allServers.NoIndex) { + serverIndex = 0; + } else { + // Move to the next server + ++serverIndex; + } + } + m_currentServer = nullptr; + nsCOMPtr<nsIMsgFolder> rootFolder; + + while (serverIndex < m_allServers.Length()) { + nsCOMPtr<nsIMsgIncomingServer> server(m_allServers[serverIndex]); + serverIndex++; + + nsCOMPtr<nsINntpIncomingServer> newsServer = do_QueryInterface(server); + if (newsServer) // news servers aren't involved in offline imap + continue; + + if (server) { + m_currentServer = server; + server->GetRootFolder(getter_AddRefs(rootFolder)); + if (rootFolder) { + rv = rootFolder->GetDescendants(m_folderQueue); + if (NS_SUCCEEDED(rv)) { + if (!m_folderQueue.IsEmpty()) { + // We'll be popping folders off the end as they are processed. + m_folderQueue.Reverse(); + return true; + } + } + } + } + } + return false; +} + +/** + * Sets m_currentFolder to the next folder to process. + * + * @return True if next folder to process was found, otherwise false. + */ +bool nsImapOfflineSync::AdvanceToNextFolder() { + // we always start by changing flags + mCurrentPlaybackOpType = nsIMsgOfflineImapOperation::kFlagsChanged; + + if (m_currentFolder) { + m_currentFolder->SetMsgDatabase(nullptr); + m_currentFolder = nullptr; + } + + bool hasMore = false; + if (m_currentServer) { + hasMore = !m_folderQueue.IsEmpty(); + } + if (!hasMore) { + hasMore = AdvanceToNextServer(); + } + if (hasMore) { + m_currentFolder = m_folderQueue.PopLastElement(); + } + ClearDB(); + return m_currentFolder; +} + +void nsImapOfflineSync::AdvanceToFirstIMAPFolder() { + m_currentServer = nullptr; + nsCOMPtr<nsIMsgImapMailFolder> imapFolder; + while (!imapFolder && AdvanceToNextFolder()) { + imapFolder = do_QueryInterface(m_currentFolder); + } +} + +void nsImapOfflineSync::ProcessFlagOperation(nsIMsgOfflineImapOperation* op) { + nsCOMPtr<nsIMsgOfflineImapOperation> currentOp = op; + nsTArray<nsMsgKey> matchingFlagKeys; + uint32_t currentKeyIndex = m_KeyIndex; + + imapMessageFlagsType matchingFlags; + currentOp->GetNewFlags(&matchingFlags); + bool flagsMatch = true; + do { // loop for all messages with the same flags + if (flagsMatch) { + nsMsgKey curKey; + currentOp->GetMessageKey(&curKey); + matchingFlagKeys.AppendElement(curKey); + currentOp->SetPlayingBack(true); + m_currentOpsToClear.AppendObject(currentOp); + } + currentOp = nullptr; + imapMessageFlagsType newFlags = kNoImapMsgFlag; + imapMessageFlagsType flagOperation = kNoImapMsgFlag; + if (++currentKeyIndex < m_CurrentKeys.Length()) + m_currentDB->GetOfflineOpForKey(m_CurrentKeys[currentKeyIndex], false, + getter_AddRefs(currentOp)); + if (currentOp) { + currentOp->GetFlagOperation(&flagOperation); + currentOp->GetNewFlags(&newFlags); + } + flagsMatch = (flagOperation & nsIMsgOfflineImapOperation::kFlagsChanged) && + (newFlags == matchingFlags); + } while (currentOp); + + if (!matchingFlagKeys.IsEmpty()) { + nsAutoCString uids; + nsImapMailFolder::AllocateUidStringFromKeys(matchingFlagKeys, uids); + uint32_t curFolderFlags; + m_currentFolder->GetFlags(&curFolderFlags); + + if (uids.get() && (curFolderFlags & nsMsgFolderFlags::ImapBox)) { + nsresult rv = NS_OK; + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = + do_QueryInterface(m_currentFolder); + nsCOMPtr<nsIURI> uriToSetFlags; + if (imapFolder) { + rv = imapFolder->SetImapFlags(uids.get(), matchingFlags, + getter_AddRefs(uriToSetFlags)); + if (NS_SUCCEEDED(rv) && uriToSetFlags) { + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = + do_QueryInterface(uriToSetFlags); + if (mailnewsUrl) mailnewsUrl->RegisterListener(this); + } + } + } + } else + ProcessNextOperation(); +} + +void nsImapOfflineSync::ProcessKeywordOperation( + nsIMsgOfflineImapOperation* op) { + nsCOMPtr<nsIMsgOfflineImapOperation> currentOp = op; + nsTArray<nsMsgKey> matchingKeywordKeys; + uint32_t currentKeyIndex = m_KeyIndex; + + nsAutoCString keywords; + if (mCurrentPlaybackOpType == nsIMsgOfflineImapOperation::kAddKeywords) + currentOp->GetKeywordsToAdd(getter_Copies(keywords)); + else + currentOp->GetKeywordsToRemove(getter_Copies(keywords)); + bool keywordsMatch = true; + do { // loop for all messages with the same keywords + if (keywordsMatch) { + nsMsgKey curKey; + currentOp->GetMessageKey(&curKey); + matchingKeywordKeys.AppendElement(curKey); + currentOp->SetPlayingBack(true); + m_currentOpsToClear.AppendObject(currentOp); + } + currentOp = nullptr; + if (++currentKeyIndex < m_CurrentKeys.Length()) + m_currentDB->GetOfflineOpForKey(m_CurrentKeys[currentKeyIndex], false, + getter_AddRefs(currentOp)); + if (currentOp) { + nsAutoCString curOpKeywords; + nsOfflineImapOperationType operation; + currentOp->GetOperation(&operation); + if (mCurrentPlaybackOpType == nsIMsgOfflineImapOperation::kAddKeywords) + currentOp->GetKeywordsToAdd(getter_Copies(curOpKeywords)); + else + currentOp->GetKeywordsToRemove(getter_Copies(curOpKeywords)); + keywordsMatch = (operation & mCurrentPlaybackOpType) && + (curOpKeywords.Equals(keywords)); + } + } while (currentOp); + + if (!matchingKeywordKeys.IsEmpty()) { + uint32_t curFolderFlags; + m_currentFolder->GetFlags(&curFolderFlags); + + if (curFolderFlags & nsMsgFolderFlags::ImapBox) { + nsresult rv = NS_OK; + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = + do_QueryInterface(m_currentFolder); + nsCOMPtr<nsIURI> uriToStoreCustomKeywords; + if (imapFolder) { + rv = imapFolder->StoreCustomKeywords( + m_window, + (mCurrentPlaybackOpType == nsIMsgOfflineImapOperation::kAddKeywords) + ? keywords + : EmptyCString(), + (mCurrentPlaybackOpType == + nsIMsgOfflineImapOperation::kRemoveKeywords) + ? keywords + : EmptyCString(), + matchingKeywordKeys, getter_AddRefs(uriToStoreCustomKeywords)); + if (NS_SUCCEEDED(rv) && uriToStoreCustomKeywords) { + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = + do_QueryInterface(uriToStoreCustomKeywords); + if (mailnewsUrl) mailnewsUrl->RegisterListener(this); + } + } + } + } else + ProcessNextOperation(); +} + +// XXX This should not be void but return an error to indicate which low +// level routine failed. +void nsImapOfflineSync::ProcessAppendMsgOperation( + nsIMsgOfflineImapOperation* currentOp, int32_t opType) { + nsMsgKey msgKey; + currentOp->GetMessageKey(&msgKey); + nsCOMPtr<nsIMsgDBHdr> mailHdr; + nsresult rv = m_currentDB->GetMsgHdrForKey(msgKey, getter_AddRefs(mailHdr)); + if (NS_FAILED(rv) || !mailHdr) { + m_currentDB->RemoveOfflineOp(currentOp); + ProcessNextOperation(); + return; + } + + uint64_t messageOffset; + uint32_t messageSize; + mailHdr->GetMessageOffset(&messageOffset); + mailHdr->GetOfflineMessageSize(&messageSize); + nsCOMPtr<nsIFile> tmpFile; + + if (NS_WARN_IF(NS_FAILED(GetSpecialDirectoryWithFileName( + NS_OS_TEMP_DIR, "nscpmsg.txt", getter_AddRefs(tmpFile))))) + return; + + if (NS_WARN_IF( + NS_FAILED(tmpFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 00600)))) + return; + + nsCOMPtr<nsIOutputStream> outputStream; + rv = MsgNewBufferedFileOutputStream(getter_AddRefs(outputStream), tmpFile, + PR_WRONLY | PR_CREATE_FILE, 00600); + if (NS_WARN_IF(NS_FAILED(rv) || !outputStream)) return; + + // We break out of the loop to get to the clean-up code. + bool setPlayingBack = false; + do { + nsCString moveDestination; + currentOp->GetDestinationFolderURI(moveDestination); + + nsCOMPtr<nsIMsgFolder> destFolder; + rv = GetOrCreateFolder(moveDestination, getter_AddRefs(destFolder)); + if (NS_WARN_IF(NS_FAILED(rv))) break; + + nsCOMPtr<nsIInputStream> offlineStoreInputStream; + rv = destFolder->GetMsgInputStream(mailHdr, + getter_AddRefs(offlineStoreInputStream)); + if (NS_WARN_IF((NS_FAILED(rv) || !offlineStoreInputStream))) break; + + nsCOMPtr<nsISeekableStream> seekStream = + do_QueryInterface(offlineStoreInputStream); + MOZ_ASSERT(seekStream, "non seekable stream - can't read from offline msg"); + if (!seekStream) break; + + // From this point onwards, we need to set "playing back". + setPlayingBack = true; + + rv = seekStream->Seek(PR_SEEK_SET, messageOffset); + if (NS_WARN_IF(NS_FAILED(rv))) break; + + // Copy the dest folder offline store msg to the temp file. + int32_t inputBufferSize = FILE_IO_BUFFER_SIZE; + char* inputBuffer = (char*)PR_Malloc(inputBufferSize); + int32_t bytesLeft; + uint32_t bytesRead, bytesWritten; + + bytesLeft = messageSize; + rv = inputBuffer ? NS_OK : NS_ERROR_OUT_OF_MEMORY; + while (bytesLeft > 0 && NS_SUCCEEDED(rv)) { + int32_t bytesToRead = std::min(inputBufferSize, bytesLeft); + rv = offlineStoreInputStream->Read(inputBuffer, bytesToRead, &bytesRead); + if (NS_WARN_IF(NS_FAILED(rv)) || bytesRead == 0) break; + rv = outputStream->Write(inputBuffer, bytesRead, &bytesWritten); + if (NS_WARN_IF(NS_FAILED(rv))) break; + MOZ_ASSERT(bytesWritten == bytesRead, + "wrote out incorrect number of bytes"); + bytesLeft -= bytesRead; + } + PR_FREEIF(inputBuffer); + + // rv could have an error from Read/Write. + nsresult rv2 = outputStream->Close(); + if (NS_FAILED(rv2)) { + NS_WARNING("ouputStream->Close() failed"); + } + outputStream = nullptr; // Don't try to close it again below. + + // rv: Read/Write, rv2: Close + if (NS_FAILED(rv) || NS_FAILED(rv2)) { + // This Remove() will fail under Windows if the output stream + // fails to close above. + mozilla::Unused << NS_WARN_IF(NS_FAILED(tmpFile->Remove(false))); + break; + } + + nsCOMPtr<nsIFile> cloneTmpFile; + // clone the tmp file to defeat nsIFile's stat/size caching. + tmpFile->Clone(getter_AddRefs(cloneTmpFile)); + m_curTempFile = cloneTmpFile; + nsCOMPtr<nsIMsgCopyService> copyService = + do_GetService("@mozilla.org/messenger/messagecopyservice;1"); + + // CopyFileMessage returns error async to this->OnStopCopy + // if copyService is null, let's crash here and now. + rv = copyService->CopyFileMessage(cloneTmpFile, destFolder, + nullptr, // nsIMsgDBHdr* msgToReplace + true, // isDraftOrTemplate + 0, // new msg flags + EmptyCString(), this, m_window); + MOZ_ASSERT(NS_SUCCEEDED(rv), + "CopyFileMessage() failed. Fatal. Error in call setup?"); + } while (false); + + if (setPlayingBack) { + currentOp->SetPlayingBack(true); + m_currentOpsToClear.AppendObject(currentOp); + m_currentDB->DeleteHeader(mailHdr, nullptr, true, true); + } + + // Close the output stream if it's not already closed. + if (outputStream) + mozilla::Unused << NS_WARN_IF(NS_FAILED(outputStream->Close())); +} + +void nsImapOfflineSync::ClearCurrentOps() { + int32_t opCount = m_currentOpsToClear.Count(); + for (int32_t i = opCount - 1; i >= 0; i--) { + m_currentOpsToClear[i]->SetPlayingBack(false); + m_currentOpsToClear[i]->ClearOperation(mCurrentPlaybackOpType); + m_currentOpsToClear.RemoveObjectAt(i); + } +} + +void nsImapOfflineSync::ProcessMoveOperation(nsIMsgOfflineImapOperation* op) { + nsTArray<nsMsgKey> matchingFlagKeys; + uint32_t currentKeyIndex = m_KeyIndex; + nsCString moveDestination; + op->GetDestinationFolderURI(moveDestination); + bool moveMatches = true; + nsCOMPtr<nsIMsgOfflineImapOperation> currentOp = op; + do { // loop for all messages with the same destination + if (moveMatches) { + nsMsgKey curKey; + currentOp->GetMessageKey(&curKey); + matchingFlagKeys.AppendElement(curKey); + currentOp->SetPlayingBack(true); + m_currentOpsToClear.AppendObject(currentOp); + } + currentOp = nullptr; + + if (++currentKeyIndex < m_CurrentKeys.Length()) { + nsCString nextDestination; + nsresult rv = m_currentDB->GetOfflineOpForKey( + m_CurrentKeys[currentKeyIndex], false, getter_AddRefs(currentOp)); + moveMatches = false; + if (NS_SUCCEEDED(rv) && currentOp) { + nsOfflineImapOperationType opType; + currentOp->GetOperation(&opType); + if (opType & nsIMsgOfflineImapOperation::kMsgMoved) { + currentOp->GetDestinationFolderURI(nextDestination); + moveMatches = moveDestination.Equals(nextDestination); + } + } + } + } while (currentOp); + + nsCOMPtr<nsIMsgFolder> destFolder; + FindFolder(moveDestination, getter_AddRefs(destFolder)); + // if the dest folder doesn't really exist, these operations are + // going to fail, so clear them out and move on. + if (!destFolder) { + NS_WARNING("trying to playing back move to non-existent folder"); + ClearCurrentOps(); + ProcessNextOperation(); + return; + } + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = + do_QueryInterface(m_currentFolder); + if (imapFolder && DestFolderOnSameServer(destFolder)) { + uint32_t curFolderFlags; + m_currentFolder->GetFlags(&curFolderFlags); + bool curFolderOffline = curFolderFlags & nsMsgFolderFlags::Offline; + imapFolder->ReplayOfflineMoveCopy(matchingFlagKeys, true, destFolder, this, + m_window, curFolderOffline); + } else { + nsresult rv; + nsTArray<RefPtr<nsIMsgDBHdr>> messages; + for (uint32_t keyIndex = 0; keyIndex < matchingFlagKeys.Length(); + keyIndex++) { + nsCOMPtr<nsIMsgDBHdr> mailHdr = nullptr; + rv = m_currentFolder->GetMessageHeader( + matchingFlagKeys.ElementAt(keyIndex), getter_AddRefs(mailHdr)); + if (NS_SUCCEEDED(rv) && mailHdr) { + uint32_t msgSize; + // in case of a move, the header has already been deleted, + // so we've really got a fake header. We need to get its flags and + // size from the offline op to have any chance of doing the move. + mailHdr->GetMessageSize(&msgSize); + if (!msgSize) { + imapMessageFlagsType newImapFlags; + uint32_t msgFlags = 0; + op->GetMsgSize(&msgSize); + op->GetNewFlags(&newImapFlags); + // first three bits are the same + msgFlags |= (newImapFlags & 0x07); + if (newImapFlags & kImapMsgForwardedFlag) + msgFlags |= nsMsgMessageFlags::Forwarded; + mailHdr->SetFlags(msgFlags); + mailHdr->SetMessageSize(msgSize); + } + messages.AppendElement(mailHdr); + } + } + nsCOMPtr<nsIMsgCopyService> copyService = + do_GetService("@mozilla.org/messenger/messagecopyservice;1", &rv); + if (copyService) { + copyService->CopyMessages(m_currentFolder, messages, destFolder, true, + this, m_window, false); + } + } +} + +// I'm tempted to make this a method on nsIMsgFolder, but that interface +// is already so huge, and there are only a few places in the code that do this. +// If there end up to be more places that need this, then we can reconsider. +bool nsImapOfflineSync::DestFolderOnSameServer(nsIMsgFolder* destFolder) { + nsCOMPtr<nsIMsgIncomingServer> srcServer; + nsCOMPtr<nsIMsgIncomingServer> dstServer; + + bool sameServer = 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* aCurrentOp) { + nsCOMPtr<nsIMsgOfflineImapOperation> currentOp = aCurrentOp; + + nsTArray<nsMsgKey> matchingFlagKeys; + uint32_t currentKeyIndex = m_KeyIndex; + nsCString copyDestination; + currentOp->GetCopyDestination(0, getter_Copies(copyDestination)); + bool copyMatches = true; + nsresult rv; + + do { // loop for all messages with the same destination + if (copyMatches) { + nsMsgKey curKey; + currentOp->GetMessageKey(&curKey); + matchingFlagKeys.AppendElement(curKey); + currentOp->SetPlayingBack(true); + m_currentOpsToClear.AppendObject(currentOp); + } + currentOp = nullptr; + + if (++currentKeyIndex < m_CurrentKeys.Length()) { + nsCString nextDestination; + rv = m_currentDB->GetOfflineOpForKey(m_CurrentKeys[currentKeyIndex], + false, getter_AddRefs(currentOp)); + copyMatches = false; + if (NS_SUCCEEDED(rv) && currentOp) { + nsOfflineImapOperationType opType; + currentOp->GetOperation(&opType); + if (opType & nsIMsgOfflineImapOperation::kMsgCopy) { + currentOp->GetCopyDestination(0, getter_Copies(nextDestination)); + copyMatches = copyDestination.Equals(nextDestination); + } + } + } + } while (currentOp); + + nsAutoCString uids; + nsCOMPtr<nsIMsgFolder> destFolder; + FindFolder(copyDestination, getter_AddRefs(destFolder)); + // if the dest folder doesn't really exist, these operations are + // 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; + } + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = + do_QueryInterface(m_currentFolder); + if (imapFolder && DestFolderOnSameServer(destFolder)) { + uint32_t curFolderFlags; + m_currentFolder->GetFlags(&curFolderFlags); + bool curFolderOffline = curFolderFlags & nsMsgFolderFlags::Offline; + rv = imapFolder->ReplayOfflineMoveCopy(matchingFlagKeys, false, destFolder, + this, m_window, curFolderOffline); + } else { + nsTArray<RefPtr<nsIMsgDBHdr>> messages; + for (uint32_t keyIndex = 0; keyIndex < matchingFlagKeys.Length(); + keyIndex++) { + nsCOMPtr<nsIMsgDBHdr> mailHdr = nullptr; + rv = m_currentFolder->GetMessageHeader( + matchingFlagKeys.ElementAt(keyIndex), getter_AddRefs(mailHdr)); + if (NS_SUCCEEDED(rv) && mailHdr) messages.AppendElement(mailHdr); + } + nsCOMPtr<nsIMsgCopyService> copyService = + do_GetService("@mozilla.org/messenger/messagecopyservice;1", &rv); + if (copyService) + copyService->CopyMessages(m_currentFolder, messages, destFolder, false, + this, m_window, false); + } +} + +void nsImapOfflineSync::ProcessEmptyTrash() { + m_currentFolder->EmptyTrash(this); + ClearDB(); // EmptyTrash closes and deletes the trash db. +} + +// returns true if we found a folder to create, false if we're done creating +// folders. +bool nsImapOfflineSync::CreateOfflineFolders() { + while (m_currentFolder) { + uint32_t flags; + m_currentFolder->GetFlags(&flags); + bool offlineCreate = (flags & nsMsgFolderFlags::CreatedOffline) != 0; + if (offlineCreate) { + if (CreateOfflineFolder(m_currentFolder)) return true; + } + AdvanceToNextFolder(); + } + return false; +} + +bool nsImapOfflineSync::CreateOfflineFolder(nsIMsgFolder* folder) { + nsCOMPtr<nsIMsgFolder> parent; + folder->GetParent(getter_AddRefs(parent)); + + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(parent); + nsCOMPtr<nsIURI> createFolderURI; + nsCString onlineName; + imapFolder->GetOnlineName(onlineName); + + NS_ConvertASCIItoUTF16 folderName(onlineName); + nsresult rv = imapFolder->PlaybackOfflineFolderCreate( + folderName, nullptr, getter_AddRefs(createFolderURI)); + if (createFolderURI && NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = + do_QueryInterface(createFolderURI); + if (mailnewsUrl) mailnewsUrl->RegisterListener(this); + } + return NS_SUCCEEDED(rv) ? true + : false; // this is asynch, we have to return and be + // called again by the OfflineOpExitFunction +} + +int32_t nsImapOfflineSync::GetCurrentUIDValidity() { + if (m_currentFolder) { + nsCOMPtr<nsIImapMailFolderSink> imapFolderSink = + do_QueryInterface(m_currentFolder); + if (imapFolderSink) imapFolderSink->GetUidValidity(&mCurrentUIDValidity); + } + return mCurrentUIDValidity; +} + +/** + * Playing back offline operations is one giant state machine that runs through + * ProcessNextOperation. + * The first state is creating online any folders created offline (we do this + * first, so we can play back any operations in them in the next pass) + */ +NS_IMETHODIMP +nsImapOfflineSync::ProcessNextOperation() { + nsresult rv = NS_OK; + + // if we haven't created offline folders, and we're updating all folders, + // first, find offline folders to create. + if (!m_createdOfflineFolders) { + if (m_singleFolderToUpdate) { + if (!m_pseudoOffline) { + AdvanceToFirstIMAPFolder(); + if (CreateOfflineFolders()) return NS_OK; + } + } else { + if (CreateOfflineFolders()) return NS_OK; + m_currentServer = nullptr; + AdvanceToNextFolder(); + } + m_createdOfflineFolders = true; + } + // if updating one folder only, restore m_currentFolder to that folder + if (m_singleFolderToUpdate) m_currentFolder = m_singleFolderToUpdate; + + uint32_t folderFlags; + nsCOMPtr<nsIDBFolderInfo> folderInfo; + while (m_currentFolder && !m_currentDB) { + m_currentFolder->GetFlags(&folderFlags); + // need to check if folder has offline events, /* or is configured for + // offline */ shouldn't need to check if configured for offline use, since + // any folder with events should have nsMsgFolderFlags::OfflineEvents set. + if (folderFlags & + (nsMsgFolderFlags::OfflineEvents /* | nsMsgFolderFlags::Offline */)) { + nsCOMPtr<nsIMsgDatabase> db; + m_currentFolder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), + getter_AddRefs(db)); + if (db) { + m_currentDB = do_QueryInterface(db, &rv); + m_currentDB->AddListener(this); + } + } + + if (m_currentDB) { + m_CurrentKeys.Clear(); + m_KeyIndex = 0; + if (NS_FAILED(m_currentDB->ListAllOfflineOpIds(m_CurrentKeys)) || + m_CurrentKeys.IsEmpty()) { + ClearDB(); + folderInfo = nullptr; // can't hold onto folderInfo longer than db + m_currentFolder->ClearFlag(nsMsgFolderFlags::OfflineEvents); + } else { + // trash any ghost msgs + bool deletedGhostMsgs = false; + for (uint32_t fakeIndex = 0; fakeIndex < m_CurrentKeys.Length(); + fakeIndex++) { + nsCOMPtr<nsIMsgOfflineImapOperation> currentOp; + m_currentDB->GetOfflineOpForKey(m_CurrentKeys[fakeIndex], false, + getter_AddRefs(currentOp)); + if (currentOp) { + nsOfflineImapOperationType opType; + currentOp->GetOperation(&opType); + + if (opType == nsIMsgOfflineImapOperation::kMoveResult) { + nsMsgKey curKey; + currentOp->GetMessageKey(&curKey); + m_currentDB->RemoveOfflineOp(currentOp); + deletedGhostMsgs = 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 notification 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) { + bool hdrIsRead; + m_currentDB->IsRead(curKey, &hdrIsRead); + imapFolder->ChangePendingTotal(1); + if (!hdrIsRead) imapFolder->ChangePendingUnread(1); + imapFolder->AddMoveResultPseudoKey(curKey); + } + m_currentDB->DeleteMessage(curKey, nullptr, false); + } + } + } + + if (deletedGhostMsgs) m_currentFolder->SummaryChanged(); + + m_CurrentKeys.Clear(); + if (NS_FAILED(m_currentDB->ListAllOfflineOpIds(m_CurrentKeys)) || + m_CurrentKeys.IsEmpty()) { + ClearDB(); + } else if (folderFlags & nsMsgFolderFlags::ImapBox) { + // if pseudo offline, falls through to playing ops back. + if (!m_pseudoOffline) { + // there are operations to playback so check uid validity + SetCurrentUIDValidity(0); // force initial invalid state + // do a lite select here and hook ourselves up as a listener. + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = + do_QueryInterface(m_currentFolder, &rv); + if (imapFolder) rv = imapFolder->LiteSelect(this, m_window); + // this is async, we will be called again by OnStopRunningUrl. + return rv; + } + } + } + } + + if (!m_currentDB) { + // only advance if we are doing all folders + if (!m_singleFolderToUpdate) + AdvanceToNextFolder(); + else + m_currentFolder = nullptr; // force update of this folder now. + } + } + + if (m_currentFolder) m_currentFolder->GetFlags(&folderFlags); + // do the current operation + if (m_currentDB) { + bool currentFolderFinished = false; + if (!folderInfo) m_currentDB->GetDBFolderInfo(getter_AddRefs(folderInfo)); + // user canceled the lite select! if GetCurrentUIDValidity() == 0 + if (folderInfo && (m_KeyIndex < m_CurrentKeys.Length()) && + (m_pseudoOffline || (GetCurrentUIDValidity() != 0) || + !(folderFlags & nsMsgFolderFlags::ImapBox))) { + int32_t curFolderUidValidity; + folderInfo->GetImapUidValidity(&curFolderUidValidity); + bool uidvalidityChanged = + (!m_pseudoOffline && folderFlags & nsMsgFolderFlags::ImapBox) && + (GetCurrentUIDValidity() != curFolderUidValidity); + nsCOMPtr<nsIMsgOfflineImapOperation> currentOp; + if (uidvalidityChanged) + DeleteAllOfflineOpsForCurrentDB(); + else + m_currentDB->GetOfflineOpForKey(m_CurrentKeys[m_KeyIndex], false, + getter_AddRefs(currentOp)); + + if (currentOp) { + nsOfflineImapOperationType opType; + currentOp->GetOperation(&opType); + // loop until we find the next db record that matches the current + // playback operation + while (currentOp && !(opType & mCurrentPlaybackOpType)) { + // remove operations with no type. + if (!opType) m_currentDB->RemoveOfflineOp(currentOp); + currentOp = nullptr; + ++m_KeyIndex; + if (m_KeyIndex < m_CurrentKeys.Length()) + m_currentDB->GetOfflineOpForKey(m_CurrentKeys[m_KeyIndex], false, + getter_AddRefs(currentOp)); + if (currentOp) currentOp->GetOperation(&opType); + } + // if we did not find a db record that matches the current playback + // operation, then move to the next playback operation and recurse. + if (!currentOp) { + // we are done with the current type + if (mCurrentPlaybackOpType == + nsIMsgOfflineImapOperation::kFlagsChanged) { + mCurrentPlaybackOpType = nsIMsgOfflineImapOperation::kAddKeywords; + // recurse to deal with next type of operation + m_KeyIndex = 0; + ProcessNextOperation(); + } else if (mCurrentPlaybackOpType == + nsIMsgOfflineImapOperation::kAddKeywords) { + mCurrentPlaybackOpType = + nsIMsgOfflineImapOperation::kRemoveKeywords; + // recurse to deal with next type of operation + m_KeyIndex = 0; + ProcessNextOperation(); + } else if (mCurrentPlaybackOpType == + nsIMsgOfflineImapOperation::kRemoveKeywords) { + mCurrentPlaybackOpType = nsIMsgOfflineImapOperation::kMsgCopy; + // recurse to deal with next type of operation + m_KeyIndex = 0; + ProcessNextOperation(); + } else if (mCurrentPlaybackOpType == + nsIMsgOfflineImapOperation::kMsgCopy) { + mCurrentPlaybackOpType = nsIMsgOfflineImapOperation::kMsgMoved; + // recurse to deal with next type of operation + m_KeyIndex = 0; + ProcessNextOperation(); + } else if (mCurrentPlaybackOpType == + nsIMsgOfflineImapOperation::kMsgMoved) { + mCurrentPlaybackOpType = nsIMsgOfflineImapOperation::kAppendDraft; + // recurse to deal with next type of operation + m_KeyIndex = 0; + ProcessNextOperation(); + } else if (mCurrentPlaybackOpType == + nsIMsgOfflineImapOperation::kAppendDraft) { + mCurrentPlaybackOpType = + nsIMsgOfflineImapOperation::kAppendTemplate; + // recurse to deal with next type of operation + m_KeyIndex = 0; + ProcessNextOperation(); + } else if (mCurrentPlaybackOpType == + nsIMsgOfflineImapOperation::kAppendTemplate) { + mCurrentPlaybackOpType = nsIMsgOfflineImapOperation::kDeleteAllMsgs; + m_KeyIndex = 0; + ProcessNextOperation(); + } else { + DeleteAllOfflineOpsForCurrentDB(); + currentFolderFinished = true; + } + + } else { + if (mCurrentPlaybackOpType == + nsIMsgOfflineImapOperation::kFlagsChanged) + ProcessFlagOperation(currentOp); + else if (mCurrentPlaybackOpType == + nsIMsgOfflineImapOperation::kAddKeywords || + mCurrentPlaybackOpType == + nsIMsgOfflineImapOperation::kRemoveKeywords) + ProcessKeywordOperation(currentOp); + else if (mCurrentPlaybackOpType == + nsIMsgOfflineImapOperation::kMsgCopy) + ProcessCopyOperation(currentOp); + else if (mCurrentPlaybackOpType == + nsIMsgOfflineImapOperation::kMsgMoved) + ProcessMoveOperation(currentOp); + else if (mCurrentPlaybackOpType == + nsIMsgOfflineImapOperation::kAppendDraft) + ProcessAppendMsgOperation(currentOp, + nsIMsgOfflineImapOperation::kAppendDraft); + else if (mCurrentPlaybackOpType == + nsIMsgOfflineImapOperation::kAppendTemplate) + ProcessAppendMsgOperation( + currentOp, nsIMsgOfflineImapOperation::kAppendTemplate); + else if (mCurrentPlaybackOpType == + nsIMsgOfflineImapOperation::kDeleteAllMsgs) { + // empty trash is going to delete the db, so we'd better release the + // reference to the offline operation first. + currentOp = nullptr; + ProcessEmptyTrash(); + } else + NS_WARNING("invalid playback op type"); + } + } else + currentFolderFinished = true; + } else + currentFolderFinished = true; + + if (currentFolderFinished) { + ClearDB(); + if (!m_singleFolderToUpdate) { + AdvanceToNextFolder(); + ProcessNextOperation(); + return NS_OK; + } + m_currentFolder = nullptr; + } + } + + if (!m_currentFolder && !m_mailboxupdatesStarted) { + m_mailboxupdatesStarted = true; + + // if we are updating more than one folder then we need the iterator + if (!m_singleFolderToUpdate) { + m_currentServer = nullptr; + AdvanceToNextFolder(); + } + if (m_singleFolderToUpdate) { + m_singleFolderToUpdate->ClearFlag(nsMsgFolderFlags::OfflineEvents); + m_singleFolderToUpdate->UpdateFolder(m_window); + } + } + // if we get here, then I *think* we're done. Not sure, though. +#ifdef DEBUG_bienvenu + printf("done with offline imap sync\n"); +#endif + nsCOMPtr<nsIUrlListener> saveListener = m_listener; + m_listener = nullptr; + + if (saveListener) + saveListener->OnStopRunningUrl(nullptr /* don't know url */, rv); + return rv; +} + +void nsImapOfflineSync::DeleteAllOfflineOpsForCurrentDB() { + m_KeyIndex = 0; + nsCOMPtr<nsIMsgOfflineImapOperation> currentOp; + m_currentDB->GetOfflineOpForKey(m_CurrentKeys[m_KeyIndex], false, + getter_AddRefs(currentOp)); + while (currentOp) { + // NS_ASSERTION(currentOp->GetOperationFlags() == 0); + // delete any ops that have already played back + m_currentDB->RemoveOfflineOp(currentOp); + currentOp = nullptr; + + if (++m_KeyIndex < m_CurrentKeys.Length()) + m_currentDB->GetOfflineOpForKey(m_CurrentKeys[m_KeyIndex], false, + getter_AddRefs(currentOp)); + } + m_currentDB->Commit(nsMsgDBCommitType::kLargeCommit); + // turn off nsMsgFolderFlags::OfflineEvents + if (m_currentFolder) + m_currentFolder->ClearFlag(nsMsgFolderFlags::OfflineEvents); +} + +nsImapOfflineDownloader::nsImapOfflineDownloader(nsIMsgWindow* aMsgWindow, + nsIUrlListener* aListener) + : nsImapOfflineSync() { + Init(aMsgWindow, aListener, nullptr, false); + // pause auto-sync service + nsresult rv; + nsCOMPtr<nsIAutoSyncManager> autoSyncMgr = + do_GetService(NS_AUTOSYNCMANAGER_CONTRACTID, &rv); + if (NS_SUCCEEDED(rv)) autoSyncMgr->Pause(); +} + +nsImapOfflineDownloader::~nsImapOfflineDownloader() {} + +NS_IMETHODIMP +nsImapOfflineDownloader::ProcessNextOperation() { + nsresult rv = NS_OK; + m_mailboxupdatesStarted = true; + + if (!m_mailboxupdatesFinished) { + if (AdvanceToNextServer()) { + nsCOMPtr<nsIMsgFolder> rootMsgFolder; + m_currentServer->GetRootFolder(getter_AddRefs(rootMsgFolder)); + nsCOMPtr<nsIMsgFolder> inbox; + if (rootMsgFolder) { + // Update the INBOX first so the updates on the remaining + // folders pickup the results of any filter moves. + rootMsgFolder->GetFolderWithFlags(nsMsgFolderFlags::Inbox, + getter_AddRefs(inbox)); + if (inbox) { + nsCOMPtr<nsIMsgFolder> offlineImapFolder; + nsCOMPtr<nsIMsgImapMailFolder> imapInbox = do_QueryInterface(inbox); + if (imapInbox) { + rootMsgFolder->GetFolderWithFlags( + nsMsgFolderFlags::Offline, getter_AddRefs(offlineImapFolder)); + if (!offlineImapFolder) { + // no imap folders configured for offline use - check if the + // account is set up so that we always download inbox msg bodies + // for offline use + nsCOMPtr<nsIImapIncomingServer> imapServer = + do_QueryInterface(m_currentServer); + if (imapServer) { + bool downloadBodiesOnGetNewMail = false; + imapServer->GetDownloadBodiesOnGetNewMail( + &downloadBodiesOnGetNewMail); + if (downloadBodiesOnGetNewMail) offlineImapFolder = inbox; + } + } + } + // if this isn't an imap inbox, or we have an offline imap sub-folder, + // then update the inbox. otherwise, it's an imap inbox for an account + // with no folders configured for offline use, so just advance to the + // next server. + if (!imapInbox || offlineImapFolder) { + // here we should check if this a pop3 server/inbox, and the user + // doesn't want to download pop3 mail for offline use. + if (!imapInbox) { + } + rv = inbox->GetNewMessages(m_window, this); + if (NS_SUCCEEDED(rv)) return rv; // otherwise, fall through. + } + } + } + return ProcessNextOperation(); // recurse and do next server. + } + m_allServers.Clear(); + m_mailboxupdatesFinished = true; + } + + while (AdvanceToNextFolder()) { + uint32_t folderFlags; + + ClearDB(); + nsCOMPtr<nsIMsgImapMailFolder> imapFolder; + if (m_currentFolder) imapFolder = do_QueryInterface(m_currentFolder); + m_currentFolder->GetFlags(&folderFlags); + // need to check if folder has offline events, or is configured for offline + if (imapFolder && folderFlags & nsMsgFolderFlags::Offline && + !(folderFlags & nsMsgFolderFlags::Virtual)) { + rv = m_currentFolder->DownloadAllForOffline(this, m_window); + if (NS_SUCCEEDED(rv) || rv == NS_BINDING_ABORTED) return rv; + // if this fails and the user didn't cancel/stop, fall through to code + // that advances to next folder + } + } + if (m_listener) m_listener->OnStopRunningUrl(nullptr, NS_OK); + return rv; +} + +NS_IMETHODIMP nsImapOfflineSync::OnStartCopy() { + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* void OnProgress (in uint32_t aProgress, in uint32_t aProgressMax); */ +NS_IMETHODIMP nsImapOfflineSync::OnProgress(uint32_t aProgress, + uint32_t aProgressMax) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* void SetMessageKey (in uint32_t aKey); */ +NS_IMETHODIMP nsImapOfflineSync::SetMessageKey(uint32_t aKey) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* [noscript] void GetMessageId (in nsCString aMessageId); */ +NS_IMETHODIMP nsImapOfflineSync::GetMessageId(nsACString& messageId) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* void OnStopCopy (in nsresult aStatus); */ +NS_IMETHODIMP nsImapOfflineSync::OnStopCopy(nsresult aStatus) { + return OnStopRunningUrl(nullptr, aStatus); +} + +void nsImapOfflineSync::ClearDB() { + m_currentOpsToClear.Clear(); + if (m_currentDB) m_currentDB->RemoveListener(this); + m_currentDB = nullptr; +} + +NS_IMETHODIMP +nsImapOfflineSync::OnHdrPropertyChanged(nsIMsgDBHdr* aHdrToChange, + const nsACString& property, + bool aPreChange, uint32_t* aStatus, + nsIDBChangeListener* aInstigator) { + return NS_OK; +} + +NS_IMETHODIMP +nsImapOfflineSync::OnHdrFlagsChanged(nsIMsgDBHdr* aHdrChanged, + uint32_t aOldFlags, uint32_t aNewFlags, + nsIDBChangeListener* aInstigator) { + return NS_OK; +} + +NS_IMETHODIMP +nsImapOfflineSync::OnHdrDeleted(nsIMsgDBHdr* aHdrChanged, nsMsgKey aParentKey, + int32_t aFlags, + nsIDBChangeListener* aInstigator) { + return NS_OK; +} + +NS_IMETHODIMP +nsImapOfflineSync::OnHdrAdded(nsIMsgDBHdr* aHdrAdded, nsMsgKey aParentKey, + int32_t aFlags, + nsIDBChangeListener* aInstigator) { + return NS_OK; +} + +/* void OnParentChanged (in nsMsgKey aKeyChanged, in nsMsgKey oldParent, in + * nsMsgKey newParent, in nsIDBChangeListener aInstigator); */ +NS_IMETHODIMP +nsImapOfflineSync::OnParentChanged(nsMsgKey aKeyChanged, nsMsgKey oldParent, + nsMsgKey newParent, + nsIDBChangeListener* aInstigator) { + return NS_OK; +} + +/* void OnAnnouncerGoingAway (in nsIDBChangeAnnouncer instigator); */ +NS_IMETHODIMP +nsImapOfflineSync::OnAnnouncerGoingAway(nsIDBChangeAnnouncer* instigator) { + ClearDB(); + return NS_OK; +} + +NS_IMETHODIMP nsImapOfflineSync::OnEvent(nsIMsgDatabase* aDB, + const char* aEvent) { + return NS_OK; +} + +/* void OnReadChanged (in nsIDBChangeListener instigator); */ +NS_IMETHODIMP +nsImapOfflineSync::OnReadChanged(nsIDBChangeListener* instigator) { + return NS_OK; +} + +/* void OnJunkScoreChanged (in nsIDBChangeListener instigator); */ +NS_IMETHODIMP +nsImapOfflineSync::OnJunkScoreChanged(nsIDBChangeListener* instigator) { + return NS_OK; +} diff --git a/comm/mailnews/imap/src/nsImapOfflineSync.h b/comm/mailnews/imap/src/nsImapOfflineSync.h new file mode 100644 index 0000000000..6a8ccc8f99 --- /dev/null +++ b/comm/mailnews/imap/src/nsImapOfflineSync.h @@ -0,0 +1,95 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef _nsImapOfflineSync_H_ +#define _nsImapOfflineSync_H_ + +#include "mozilla/Attributes.h" +#include "nsIMsgDatabase.h" +#include "nsIUrlListener.h" +#include "nsIMsgOfflineImapOperation.h" +#include "nsIMsgWindow.h" +#include "nsIMsgFolder.h" +#include "nsCOMArray.h" +#include "nsIDBChangeListener.h" +#include "nsIImapOfflineSync.h" + +class nsImapOfflineSync : public nsIUrlListener, + public nsIMsgCopyServiceListener, + public nsIDBChangeListener, + public nsIImapOfflineSync { + public: // set to one folder to playback one folder only + nsImapOfflineSync(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIURLLISTENER + NS_DECL_NSIMSGCOPYSERVICELISTENER + NS_DECL_NSIDBCHANGELISTENER + NS_DECL_NSIIMAPOFFLINESYNC + + int32_t GetCurrentUIDValidity(); + void SetCurrentUIDValidity(int32_t uidvalidity) { + mCurrentUIDValidity = uidvalidity; + } + + void SetPseudoOffline(bool pseudoOffline) { m_pseudoOffline = pseudoOffline; } + bool ProcessingStaleFolderUpdate() { + return m_singleFolderToUpdate != nullptr; + } + + bool CreateOfflineFolder(nsIMsgFolder* folder); + void SetWindow(nsIMsgWindow* window); + + protected: + virtual ~nsImapOfflineSync(); + + bool CreateOfflineFolders(); + bool DestFolderOnSameServer(nsIMsgFolder* destFolder); + bool AdvanceToNextServer(); + bool AdvanceToNextFolder(); + void AdvanceToFirstIMAPFolder(); + void DeleteAllOfflineOpsForCurrentDB(); + void ClearCurrentOps(); + // Clears m_currentDB, and unregister listener. + void ClearDB(); + void ProcessFlagOperation(nsIMsgOfflineImapOperation* currentOp); + void ProcessKeywordOperation(nsIMsgOfflineImapOperation* op); + void ProcessMoveOperation(nsIMsgOfflineImapOperation* currentOp); + void ProcessCopyOperation(nsIMsgOfflineImapOperation* currentOp); + void ProcessEmptyTrash(); + void ProcessAppendMsgOperation(nsIMsgOfflineImapOperation* currentOp, + nsOfflineImapOperationType opType); + + nsCOMPtr<nsIMsgFolder> m_currentFolder; + nsCOMPtr<nsIMsgFolder> m_singleFolderToUpdate; + nsCOMPtr<nsIMsgWindow> m_window; + nsTArray<RefPtr<nsIMsgIncomingServer>> m_allServers; + nsCOMPtr<nsIMsgIncomingServer> m_currentServer; + // Folders left to consider on m_currentServer. + nsTArray<RefPtr<nsIMsgFolder>> m_folderQueue; + + nsCOMPtr<nsIFile> m_curTempFile; + + nsTArray<nsMsgKey> m_CurrentKeys; + nsCOMArray<nsIMsgOfflineImapOperation> m_currentOpsToClear; + uint32_t m_KeyIndex; + nsCOMPtr<nsIMsgOfflineOpsDatabase> m_currentDB; + nsCOMPtr<nsIUrlListener> m_listener; + int32_t mCurrentUIDValidity; + int32_t mCurrentPlaybackOpType; // kFlagsChanged -> kMsgCopy -> kMsgMoved + bool m_mailboxupdatesStarted; + bool m_mailboxupdatesFinished; + bool m_pseudoOffline; // for queueing online events in offline db + bool m_createdOfflineFolders; +}; + +class nsImapOfflineDownloader : public nsImapOfflineSync { + public: + nsImapOfflineDownloader(nsIMsgWindow* window, nsIUrlListener* listener); + virtual ~nsImapOfflineDownloader(); + NS_IMETHOD ProcessNextOperation() override; // this kicks off download +}; + +#endif diff --git a/comm/mailnews/imap/src/nsImapProtocol.cpp b/comm/mailnews/imap/src/nsImapProtocol.cpp new file mode 100644 index 0000000000..b0a9214749 --- /dev/null +++ b/comm/mailnews/imap/src/nsImapProtocol.cpp @@ -0,0 +1,9915 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// as does this +#include "msgCore.h" // for pre-compiled headers +#include "nsMsgUtils.h" + +#include "nsImapStringBundle.h" +#include "nsVersionComparator.h" + +#include "nsThreadUtils.h" +#include "nsIMsgStatusFeedback.h" +#include "nsImapCore.h" +#include "nsImapProtocol.h" +#include "nsIMsgMailNewsUrl.h" +#include "../public/nsIImapHostSessionList.h" +#include "nsImapMailFolder.h" +#include "nsIMsgAccountManager.h" +#include "nsImapServerResponseParser.h" +#include "nspr.h" +#include "plbase64.h" +#include "nsIEventTarget.h" +#include "nsIImapService.h" +#include "nsISocketTransportService.h" +#include "nsIStreamListenerTee.h" +#include "nsIInputStreamPump.h" +#include "nsNetUtil.h" +#include "nsIDBFolderInfo.h" +#include "nsIPipe.h" +#include "nsIMsgFolder.h" +#include "nsMsgMessageFlags.h" +#include "nsTextFormatter.h" +#include "nsTransportUtils.h" +#include "nsIMsgHdr.h" +#include "nsMsgI18N.h" +// for the memory cache... +#include "nsICacheEntry.h" +#include "nsICacheStorage.h" +#include "nsICacheEntryOpenCallback.h" +#include "CacheObserver.h" +#include "nsIURIMutator.h" + +#include "nsIDocShell.h" +#include "nsILoadInfo.h" +#include "nsCOMPtr.h" +#include "nsMimeTypes.h" +#include "nsIInterfaceRequestor.h" +#include "nsXPCOMCIDInternal.h" +#include "nsIXULAppInfo.h" +#include "nsSocketTransportService2.h" +#include "nsSyncRunnableHelpers.h" +#include "nsICancelable.h" + +// netlib required files +#include "nsIStreamListener.h" +#include "nsIMsgIncomingServer.h" +#include "nsIImapIncomingServer.h" +#include "nsIPrefBranch.h" +#include "nsIPrefService.h" +#include "nsIPrefLocalizedString.h" +#include "nsImapUtils.h" +#include "nsIStreamConverterService.h" +#include "nsIProxyInfo.h" +#include "nsITLSSocketControl.h" +#include "nsITransportSecurityInfo.h" +#include "nsProxyRelease.h" +#include "nsDebug.h" +#include "nsMsgCompressIStream.h" +#include "nsMsgCompressOStream.h" +#include "mozilla/Logging.h" +#include "mozilla/Attributes.h" +#include "mozilla/SlicedInputStream.h" +#include "nsIPrincipal.h" +#include "nsContentSecurityManager.h" + +// imap event sinks +#include "nsIImapMailFolderSink.h" +#include "nsIImapServerSink.h" +#include "nsIImapMessageSink.h" + +#include "mozilla/dom/InternalResponse.h" +#include "mozilla/NullPrincipal.h" + +// TLS alerts +#include "NSSErrorsService.h" + +#include "mozilla/SyncRunnable.h" + +using namespace mozilla; + +LazyLogModule IMAP("IMAP"); +LazyLogModule IMAP_CS("IMAP_CS"); +LazyLogModule IMAPCache("IMAPCache"); + +#define ONE_SECOND ((uint32_t)1000) // one second + +#define OUTPUT_BUFFER_SIZE (4096 * 2) + +#define IMAP_ENV_HEADERS "From To Cc Bcc Subject Date Message-ID " +#define IMAP_DB_HEADERS \ + "Priority X-Priority References Newsgroups In-Reply-To Content-Type " \ + "Reply-To" +#define IMAP_ENV_AND_DB_HEADERS IMAP_ENV_HEADERS IMAP_DB_HEADERS +static const PRIntervalTime kImapSleepTime = PR_MillisecondsToInterval(60000); +static int32_t gPromoteNoopToCheckCount = 0; +static const uint32_t kFlagChangesBeforeCheck = 10; +static const int32_t kMaxSecondsBeforeCheck = 600; + +class AutoProxyReleaseMsgWindow { + public: + AutoProxyReleaseMsgWindow() : mMsgWindow() {} + ~AutoProxyReleaseMsgWindow() { + NS_ReleaseOnMainThread("AutoProxyReleaseMsgWindow::mMsgWindow", + dont_AddRef(mMsgWindow)); + } + nsIMsgWindow** StartAssignment() { + MOZ_ASSERT(!mMsgWindow); + return &mMsgWindow; + } + operator nsIMsgWindow*() { return mMsgWindow; } + + private: + nsIMsgWindow* mMsgWindow; +}; + +nsIMsgWindow** getter_AddRefs(AutoProxyReleaseMsgWindow& aSmartPtr) { + return aSmartPtr.StartAssignment(); +} + +NS_IMPL_ISUPPORTS(nsMsgImapHdrXferInfo, nsIImapHeaderXferInfo) + +nsMsgImapHdrXferInfo::nsMsgImapHdrXferInfo() : m_hdrInfos(kNumHdrsToXfer) { + m_nextFreeHdrInfo = 0; +} + +nsMsgImapHdrXferInfo::~nsMsgImapHdrXferInfo() {} + +NS_IMETHODIMP nsMsgImapHdrXferInfo::GetNumHeaders(int32_t* aNumHeaders) { + *aNumHeaders = m_nextFreeHdrInfo; + return NS_OK; +} + +NS_IMETHODIMP nsMsgImapHdrXferInfo::GetHeader(int32_t hdrIndex, + nsIImapHeaderInfo** aResult) { + // If the header index is more than (or equal to) our next free pointer, then + // its a header we haven't really got and the caller has done something + // wrong. + NS_ENSURE_TRUE(hdrIndex < m_nextFreeHdrInfo, NS_ERROR_NULL_POINTER); + + NS_IF_ADDREF(*aResult = m_hdrInfos.SafeObjectAt(hdrIndex)); + if (!*aResult) return NS_ERROR_NULL_POINTER; + return NS_OK; +} + +static const int32_t kInitLineHdrCacheSize = 512; // should be about right + +nsIImapHeaderInfo* nsMsgImapHdrXferInfo::StartNewHdr() { + if (m_nextFreeHdrInfo >= kNumHdrsToXfer) return nullptr; + + nsIImapHeaderInfo* result = m_hdrInfos.SafeObjectAt(m_nextFreeHdrInfo++); + if (result) return result; + + nsMsgImapLineDownloadCache* lineCache = new nsMsgImapLineDownloadCache(); + if (!lineCache) return nullptr; + + lineCache->GrowBuffer(kInitLineHdrCacheSize); + + m_hdrInfos.AppendObject(lineCache); + + return lineCache; +} + +// maybe not needed... +void nsMsgImapHdrXferInfo::FinishCurrentHdr() { + // nothing to do? +} + +void nsMsgImapHdrXferInfo::ResetAll() { + int32_t count = m_hdrInfos.Count(); + for (int32_t i = 0; i < count; i++) { + nsIImapHeaderInfo* hdrInfo = m_hdrInfos[i]; + if (hdrInfo) hdrInfo->ResetCache(); + } + m_nextFreeHdrInfo = 0; +} + +void nsMsgImapHdrXferInfo::ReleaseAll() { + m_hdrInfos.Clear(); + m_nextFreeHdrInfo = 0; +} + +NS_IMPL_ISUPPORTS(nsMsgImapLineDownloadCache, nsIImapHeaderInfo) + +// **** helper class for downloading line **** +nsMsgImapLineDownloadCache::nsMsgImapLineDownloadCache() { + fLineInfo = (msg_line_info*)PR_CALLOC(sizeof(msg_line_info)); + fLineInfo->uidOfMessage = nsMsgKey_None; + m_msgSize = 0; +} + +nsMsgImapLineDownloadCache::~nsMsgImapLineDownloadCache() { + PR_Free(fLineInfo); +} + +uint32_t nsMsgImapLineDownloadCache::CurrentUID() { + return fLineInfo->uidOfMessage; +} + +uint32_t nsMsgImapLineDownloadCache::SpaceAvailable() { + MOZ_ASSERT(kDownLoadCacheSize >= m_bufferPos); + if (kDownLoadCacheSize <= m_bufferPos) return 0; + return kDownLoadCacheSize - m_bufferPos; +} + +msg_line_info* nsMsgImapLineDownloadCache::GetCurrentLineInfo() { + AppendBuffer("", 1); // null terminate the buffer + fLineInfo->adoptedMessageLine = GetBuffer(); + return fLineInfo; +} + +NS_IMETHODIMP nsMsgImapLineDownloadCache::ResetCache() { + ResetWritePos(); + return NS_OK; +} + +bool nsMsgImapLineDownloadCache::CacheEmpty() { return m_bufferPos == 0; } + +NS_IMETHODIMP nsMsgImapLineDownloadCache::CacheLine(const char* line, + uint32_t uid) { + fLineInfo->uidOfMessage = uid; + return AppendString(line); +} + +/* attribute nsMsgKey msgUid; */ +NS_IMETHODIMP nsMsgImapLineDownloadCache::GetMsgUid(nsMsgKey* aMsgUid) { + *aMsgUid = fLineInfo->uidOfMessage; + return NS_OK; +} +NS_IMETHODIMP nsMsgImapLineDownloadCache::SetMsgUid(nsMsgKey aMsgUid) { + fLineInfo->uidOfMessage = aMsgUid; + return NS_OK; +} + +/* attribute long msgSize; */ +NS_IMETHODIMP nsMsgImapLineDownloadCache::GetMsgSize(int32_t* aMsgSize) { + *aMsgSize = m_msgSize; + return NS_OK; +} + +NS_IMETHODIMP nsMsgImapLineDownloadCache::SetMsgSize(int32_t aMsgSize) { + m_msgSize = aMsgSize; + return NS_OK; +} + +/* readonly attribute ACString msgHdrs; */ +NS_IMETHODIMP nsMsgImapLineDownloadCache::GetMsgHdrs(nsACString& aMsgHdrs) { + AppendBuffer("", 1); // null terminate the buffer + aMsgHdrs.Assign(GetBuffer()); + return NS_OK; +} + +// The following macros actually implement addref, release and query interface +// for our component. +NS_IMPL_ADDREF_INHERITED(nsImapProtocol, nsMsgProtocol) +NS_IMPL_RELEASE_INHERITED(nsImapProtocol, nsMsgProtocol) + +NS_INTERFACE_MAP_BEGIN(nsImapProtocol) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIImapProtocol) + NS_INTERFACE_MAP_ENTRY(nsIImapProtocol) + NS_INTERFACE_MAP_ENTRY(nsIProtocolProxyCallback) + NS_INTERFACE_MAP_ENTRY(nsIInputStreamCallback) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) + NS_INTERFACE_MAP_ENTRY(nsIImapProtocolSink) + NS_INTERFACE_MAP_ENTRY(nsIMsgAsyncPromptListener) +NS_INTERFACE_MAP_END + +static int32_t gTooFastTime = 2; +static int32_t gIdealTime = 4; +static int32_t gChunkAddSize = 16384; +static int32_t gChunkSize = 250000; +static int32_t gChunkThreshold = gChunkSize + gChunkSize / 2; +static bool gChunkSizeDirty = false; +static bool gFetchByChunks = true; +static bool gInitialized = false; +static bool gHideUnusedNamespaces = true; +static bool gHideOtherUsersFromList = false; +static bool gUseEnvelopeCmd = false; +static bool gUseLiteralPlus = true; +static bool gExpungeAfterDelete = false; +static bool gCheckDeletedBeforeExpunge = false; // bug 235004 +static int32_t gResponseTimeout = 100; +static int32_t gAppendTimeout = gResponseTimeout / 5; +static nsImapProtocol::TCPKeepalive gTCPKeepalive; +static bool gUseDiskCache2 = true; // Use disk cache instead of memory cache +static nsCOMPtr<nsICacheStorage> gCache2Storage; + +// let delete model control expunging, i.e., don't ever expunge when the +// user chooses the imap delete model, otherwise, expunge when over the +// threshold. This is the normal TB behavior. +static const int32_t kAutoExpungeDeleteModel = 0; // default +// Expunge whenever the folder is opened regardless of delete model or number +// of marked deleted messages present in the folder. +static const int32_t kAutoExpungeAlways = 1; +// Expunge when over the threshold, independent of the delete model. +static const int32_t kAutoExpungeOnThreshold = 2; +// Set mail.imap.expunge_option to kAutoExpungeNever to NEVER do an auto- +// expunge. This is useful when doing a bulk transfer of folders and messages +// between imap servers. +static const int32_t kAutoExpungeNever = 3; + +static int32_t gExpungeOption = kAutoExpungeDeleteModel; +static int32_t gExpungeThreshold = 20; + +const int32_t kAppBufSize = 100; +// can't use static nsCString because it shows up as a leak. +static char gAppName[kAppBufSize]; +static char gAppVersion[kAppBufSize]; + +nsresult nsImapProtocol::GlobalInitialization(nsIPrefBranch* aPrefBranch) { + gInitialized = true; + + aPrefBranch->GetIntPref("mail.imap.chunk_fast", + &gTooFastTime); // secs we read too little too fast + aPrefBranch->GetIntPref("mail.imap.chunk_ideal", + &gIdealTime); // secs we read enough in good time + aPrefBranch->GetIntPref( + "mail.imap.chunk_add", + &gChunkAddSize); // buffer size to add when wasting time + aPrefBranch->GetIntPref("mail.imap.chunk_size", &gChunkSize); + aPrefBranch->GetIntPref("mail.imap.min_chunk_size_threshold", + &gChunkThreshold); + aPrefBranch->GetBoolPref("mail.imap.hide_other_users", + &gHideOtherUsersFromList); + aPrefBranch->GetBoolPref("mail.imap.hide_unused_namespaces", + &gHideUnusedNamespaces); + aPrefBranch->GetIntPref("mail.imap.noop_check_count", + &gPromoteNoopToCheckCount); + aPrefBranch->GetBoolPref("mail.imap.use_envelope_cmd", &gUseEnvelopeCmd); + aPrefBranch->GetBoolPref("mail.imap.use_literal_plus", &gUseLiteralPlus); + aPrefBranch->GetBoolPref("mail.imap.expunge_after_delete", + &gExpungeAfterDelete); + aPrefBranch->GetBoolPref("mail.imap.use_disk_cache2", &gUseDiskCache2); + aPrefBranch->GetBoolPref("mail.imap.check_deleted_before_expunge", + &gCheckDeletedBeforeExpunge); + aPrefBranch->GetIntPref("mail.imap.expunge_option", &gExpungeOption); + aPrefBranch->GetIntPref("mail.imap.expunge_threshold_number", + &gExpungeThreshold); + aPrefBranch->GetIntPref("mailnews.tcptimeout", &gResponseTimeout); + gAppendTimeout = gResponseTimeout / 5; + + gTCPKeepalive.enabled.store(false, std::memory_order_relaxed); + gTCPKeepalive.idleTimeS.store(-1, std::memory_order_relaxed); + gTCPKeepalive.retryIntervalS.store(-1, std::memory_order_relaxed); + + nsCOMPtr<nsIXULAppInfo> appInfo(do_GetService(XULAPPINFO_SERVICE_CONTRACTID)); + + if (appInfo) { + nsCString appName, appVersion; + appInfo->GetName(appName); + appInfo->GetVersion(appVersion); + PL_strncpyz(gAppName, appName.get(), kAppBufSize); + PL_strncpyz(gAppVersion, appVersion.get(), kAppBufSize); + } + return NS_OK; +} + +class nsImapTransportEventSink final : public nsITransportEventSink { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSITRANSPORTEVENTSINK + + private: + friend class nsImapProtocol; + + virtual ~nsImapTransportEventSink() = default; + nsresult ApplyTCPKeepalive(nsISocketTransport* aTransport); + + nsCOMPtr<nsITransportEventSink> m_proxy; +}; + +NS_IMPL_ISUPPORTS(nsImapTransportEventSink, nsITransportEventSink) + +NS_IMETHODIMP +nsImapTransportEventSink::OnTransportStatus(nsITransport* aTransport, + nsresult aStatus, int64_t aProgress, + int64_t aProgressMax) { + if (aStatus == NS_NET_STATUS_CONNECTED_TO) { + nsCOMPtr<nsISocketTransport> sockTrans(do_QueryInterface(aTransport)); + if (!NS_WARN_IF(!sockTrans)) ApplyTCPKeepalive(sockTrans); + } + + if (NS_WARN_IF(!m_proxy)) return NS_OK; + + return m_proxy->OnTransportStatus(aTransport, aStatus, aProgress, + aProgressMax); +} + +nsresult nsImapTransportEventSink::ApplyTCPKeepalive( + nsISocketTransport* aTransport) { + nsresult rv; + + bool kaEnabled = gTCPKeepalive.enabled.load(std::memory_order_relaxed); + if (kaEnabled) { + // TCP keepalive idle time, don't mistake with IMAP IDLE. + int32_t kaIdleTime = + gTCPKeepalive.idleTimeS.load(std::memory_order_relaxed); + int32_t kaRetryInterval = + gTCPKeepalive.retryIntervalS.load(std::memory_order_relaxed); + + if (kaIdleTime < 0 || kaRetryInterval < 0) { + if (NS_WARN_IF(!net::gSocketTransportService)) + return NS_ERROR_NOT_INITIALIZED; + } + if (kaIdleTime < 0) { + rv = net::gSocketTransportService->GetKeepaliveIdleTime(&kaIdleTime); + if (NS_FAILED(rv)) { + MOZ_LOG(IMAP, LogLevel::Error, + ("GetKeepaliveIdleTime() failed, %" PRIx32, + static_cast<uint32_t>(rv))); + return rv; + } + } + if (kaRetryInterval < 0) { + rv = net::gSocketTransportService->GetKeepaliveRetryInterval( + &kaRetryInterval); + if (NS_FAILED(rv)) { + MOZ_LOG(IMAP, LogLevel::Error, + ("GetKeepaliveRetryInterval() failed, %" PRIx32, + static_cast<uint32_t>(rv))); + return rv; + } + } + + MOZ_ASSERT(kaIdleTime > 0); + MOZ_ASSERT(kaRetryInterval > 0); + rv = aTransport->SetKeepaliveVals(kaIdleTime, kaRetryInterval); + if (NS_FAILED(rv)) { + MOZ_LOG(IMAP, LogLevel::Error, + ("SetKeepaliveVals(%" PRId32 ", %" PRId32 ") failed, %" PRIx32, + kaIdleTime, kaRetryInterval, static_cast<uint32_t>(rv))); + return rv; + } + } + + rv = aTransport->SetKeepaliveEnabled(kaEnabled); + if (NS_FAILED(rv)) { + MOZ_LOG(IMAP, LogLevel::Error, + ("SetKeepaliveEnabled(%s) failed, %" PRIx32, + kaEnabled ? "true" : "false", static_cast<uint32_t>(rv))); + return rv; + } + return NS_OK; +} + +// This runnable runs on IMAP thread. +class nsImapProtocolMainLoopRunnable final : public mozilla::Runnable { + public: + explicit nsImapProtocolMainLoopRunnable(nsImapProtocol* aProtocol) + : mozilla::Runnable("nsImapProtocolEventLoopRunnable"), + mProtocol(aProtocol) {} + + NS_IMETHOD Run() { + MOZ_ASSERT(!NS_IsMainThread()); + + if (!mProtocol->RunImapThreadMainLoop()) { + // We already run another IMAP event loop. + return NS_OK; + } + + // Release protocol object on the main thread to avoid destruction of + // nsImapProtocol on the IMAP thread, which causes grief for weak + // references. + NS_ReleaseOnMainThread("nsImapProtocol::this", mProtocol.forget()); + + // shutdown this thread, but do it from the main thread + nsCOMPtr<nsIThread> imapThread(do_GetCurrentThread()); + if (NS_FAILED(NS_DispatchToMainThread( + NS_NewRunnableFunction("nsImapProtorolMainLoopRunnable::Run", + [imapThread = std::move(imapThread)]() { + imapThread->Shutdown(); + })))) { + NS_WARNING("Failed to dispatch nsImapThreadShutdownEvent"); + } + return NS_OK; + } + + private: + RefPtr<nsImapProtocol> mProtocol; +}; + +nsImapProtocol::nsImapProtocol() + : nsMsgProtocol(nullptr), + m_dataAvailableMonitor("imapDataAvailable"), + m_urlReadyToRunMonitor("imapUrlReadyToRun"), + m_pseudoInterruptMonitor("imapPseudoInterrupt"), + m_dataMemberMonitor("imapDataMember"), + m_threadDeathMonitor("imapThreadDeath"), + m_waitForBodyIdsMonitor("imapWaitForBodyIds"), + m_fetchBodyListMonitor("imapFetchBodyList"), + m_passwordReadyMonitor("imapPasswordReady"), + mLock("nsImapProtocol.mLock"), + m_parser(*this) { + m_urlInProgress = false; + m_idle = false; + m_retryUrlOnError = false; + m_useIdle = true; // by default, use it + m_useCondStore = true; + m_useCompressDeflate = true; + m_ignoreExpunges = false; + m_prefAuthMethods = kCapabilityUndefined; + m_failedAuthMethods = 0; + m_currentAuthMethod = kCapabilityUndefined; + m_socketType = nsMsgSocketType::trySTARTTLS; + m_connectionStatus = NS_OK; + m_safeToCloseConnection = false; + m_hostSessionList = nullptr; + m_isGmailServer = false; + m_fetchingWholeMessage = false; + m_allowUTF8Accept = false; + + nsCOMPtr<nsIPrefBranch> prefBranch(do_GetService(NS_PREFSERVICE_CONTRACTID)); + NS_ASSERTION(prefBranch, "FAILED to create the preference service"); + + // read in the accept languages preference + if (prefBranch) { + if (!gInitialized) GlobalInitialization(prefBranch); + + nsCOMPtr<nsIPrefLocalizedString> prefString; + prefBranch->GetComplexValue("intl.accept_languages", + NS_GET_IID(nsIPrefLocalizedString), + getter_AddRefs(prefString)); + if (prefString) prefString->ToString(getter_Copies(mAcceptLanguages)); + + nsCString customDBHeaders; + prefBranch->GetCharPref("mailnews.customDBHeaders", customDBHeaders); + + ParseString(customDBHeaders, ' ', mCustomDBHeaders); + prefBranch->GetBoolPref("mailnews.display.prefer_plaintext", + &m_preferPlainText); + + nsAutoCString customHeaders; + prefBranch->GetCharPref("mailnews.customHeaders", customHeaders); + customHeaders.StripWhitespace(); + ParseString(customHeaders, ':', mCustomHeaders); + + nsresult rv; + bool bVal = false; + rv = prefBranch->GetBoolPref("mail.imap.tcp_keepalive.enabled", &bVal); + if (NS_SUCCEEDED(rv)) + gTCPKeepalive.enabled.store(bVal, std::memory_order_relaxed); + + if (bVal) { + int32_t val; + // TCP keepalive idle time, don't mistake with IMAP IDLE. + rv = prefBranch->GetIntPref("mail.imap.tcp_keepalive.idle_time", &val); + if (NS_SUCCEEDED(rv) && val >= 0) + gTCPKeepalive.idleTimeS.store( + std::min<int32_t>(std::max(val, 1), net::kMaxTCPKeepIdle), + std::memory_order_relaxed); + + rv = prefBranch->GetIntPref("mail.imap.tcp_keepalive.retry_interval", + &val); + if (NS_SUCCEEDED(rv) && val >= 0) + gTCPKeepalive.retryIntervalS.store( + std::min<int32_t>(std::max(val, 1), net::kMaxTCPKeepIntvl), + std::memory_order_relaxed); + } + } + + // ***** Thread support ***** + m_thread = nullptr; + m_imapThreadIsRunning = false; + m_currentServerCommandTagNumber = 0; + m_active = false; + m_folderNeedsSubscribing = false; + m_folderNeedsACLRefreshed = false; + m_threadShouldDie = false; + m_inThreadShouldDie = false; + m_pseudoInterrupted = false; + m_nextUrlReadyToRun = false; + m_idleResponseReadyToHandle = false; + m_trackingTime = false; + m_curFetchSize = 0; + m_startTime = 0; + m_endTime = 0; + m_lastActiveTime = 0; + m_lastProgressTime = 0; + ResetProgressInfo(); + + m_tooFastTime = 0; + m_idealTime = 0; + m_chunkAddSize = 0; + m_chunkStartSize = 0; + m_fetchByChunks = true; + m_sendID = true; + m_chunkSize = 0; + m_chunkThreshold = 0; + m_fromHeaderSeen = false; + m_closeNeededBeforeSelect = false; + m_needNoop = false; + m_noopCount = 0; + m_fetchBodyListIsNew = false; + m_flagChangeCount = 0; + m_lastCheckTime = PR_Now(); + + m_hierarchyNameState = kNoOperationInProgress; + m_discoveryStatus = eContinue; + + // m_dataOutputBuf is used by Send Data + m_dataOutputBuf = (char*)PR_CALLOC(sizeof(char) * OUTPUT_BUFFER_SIZE); + + // used to buffer incoming data by ReadNextLine + m_inputStreamBuffer = new nsMsgLineStreamBuffer( + OUTPUT_BUFFER_SIZE, true /* allocate new lines */, + false /* leave CRLFs on the returned string */); + m_currentBiffState = nsIMsgFolder::nsMsgBiffState_Unknown; + m_progressStringName.Truncate(); + m_stringIndex = IMAP_EMPTY_STRING_INDEX; + m_progressExpectedNumber = 0; + memset(m_progressCurrentNumber, 0, sizeof m_progressCurrentNumber); + + // since these are embedded in the nsImapProtocol object, but passed + // through proxied xpcom methods, just AddRef them here. + m_hdrDownloadCache = new nsMsgImapHdrXferInfo(); + m_downloadLineCache = new nsMsgImapLineDownloadCache(); + + // subscription + m_autoSubscribe = true; + m_autoUnsubscribe = true; + m_autoSubscribeOnOpen = true; + m_deletableChildren = nullptr; + + mFolderLastModSeq = 0; + + Configure(gTooFastTime, gIdealTime, gChunkAddSize, gChunkSize, + gChunkThreshold, gFetchByChunks); + m_forceSelect = false; + m_capabilityResponseOccurred = true; + + m_imapAction = 0; + m_bytesToChannel = 0; + m_passwordStatus = NS_OK; + m_passwordObtained = false; + mFolderTotalMsgCount = 0; + mFolderHighestUID = 0; + m_notifySearchHit = false; + m_preferPlainText = false; + m_uidValidity = kUidUnknown; +} + +nsresult nsImapProtocol::Configure(int32_t TooFastTime, int32_t IdealTime, + int32_t ChunkAddSize, int32_t ChunkSize, + int32_t ChunkThreshold, bool FetchByChunks) { + m_tooFastTime = TooFastTime; // secs we read too little too fast + m_idealTime = IdealTime; // secs we read enough in good time + m_chunkAddSize = ChunkAddSize; // buffer size to add when wasting time + m_chunkStartSize = m_chunkSize = ChunkSize; + m_chunkThreshold = ChunkThreshold; + m_fetchByChunks = FetchByChunks; + + return NS_OK; +} + +NS_IMETHODIMP +nsImapProtocol::Initialize(nsIImapHostSessionList* aHostSessionList, + nsIImapIncomingServer* aServer) { + NS_ASSERTION( + aHostSessionList && aServer, + "oops...trying to initialize with a null host session list or server!"); + if (!aHostSessionList || !aServer) return NS_ERROR_NULL_POINTER; + + nsresult rv = m_downloadLineCache->GrowBuffer(kDownLoadCacheSize); + NS_ENSURE_SUCCESS(rv, rv); + + m_flagState = new nsImapFlagAndUidState(kImapFlagAndUidStateSize); + if (!m_flagState) return NS_ERROR_OUT_OF_MEMORY; + + aServer->GetUseIdle(&m_useIdle); + aServer->GetForceSelect(&m_forceSelect); + aServer->GetUseCondStore(&m_useCondStore); + aServer->GetUseCompressDeflate(&m_useCompressDeflate); + aServer->GetAllowUTF8Accept(&m_allowUTF8Accept); + + m_hostSessionList = aHostSessionList; + m_parser.SetHostSessionList(aHostSessionList); + m_parser.SetFlagState(m_flagState); + + // Initialize the empty mime part string on the main thread. + nsCOMPtr<nsIStringBundle> bundle; + rv = IMAPGetStringBundle(getter_AddRefs(bundle)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = bundle->GetStringFromName("imapEmptyMimePart", m_emptyMimePartString); + NS_ENSURE_SUCCESS(rv, rv); + + // Now initialize the thread for the connection + if (m_thread == nullptr) { + nsCOMPtr<nsIThread> imapThread; + nsresult rv = NS_NewNamedThread("IMAP", getter_AddRefs(imapThread)); + if (NS_FAILED(rv)) { + NS_ASSERTION(imapThread, "Unable to create imap thread."); + return rv; + } + RefPtr<nsImapProtocolMainLoopRunnable> runnable = + new nsImapProtocolMainLoopRunnable(this); + imapThread->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL); + imapThread->GetPRThread(&m_thread); + } + return NS_OK; +} + +nsImapProtocol::~nsImapProtocol() { + PR_Free(m_dataOutputBuf); + + // **** We must be out of the thread main loop function + NS_ASSERTION(!m_imapThreadIsRunning, "Oops, thread is still running."); + MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread()); +} + +const nsCString& nsImapProtocol::GetImapHostName() { + if (m_runningUrl && m_hostName.IsEmpty()) { + nsCOMPtr<nsIURI> url = do_QueryInterface(m_runningUrl); + url->GetAsciiHost(m_hostName); + } + + return m_hostName; +} + +const nsCString& nsImapProtocol::GetImapUserName() { + if (m_userName.IsEmpty() && m_imapServerSink) { + m_imapServerSink->GetOriginalUsername(m_userName); + } + return m_userName; +} + +const char* nsImapProtocol::GetImapServerKey() { + if (m_serverKey.IsEmpty() && m_imapServerSink) { + m_imapServerSink->GetServerKey(m_serverKey); + } + return m_serverKey.get(); +} + +nsresult nsImapProtocol::SetupSinkProxy() { + if (!m_runningUrl) return NS_OK; + nsresult res; + bool newFolderSink = false; + if (!m_imapMailFolderSink) { + nsCOMPtr<nsIImapMailFolderSink> aImapMailFolderSink; + (void)m_runningUrl->GetImapMailFolderSink( + getter_AddRefs(aImapMailFolderSink)); + if (aImapMailFolderSink) { + m_imapMailFolderSink = new ImapMailFolderSinkProxy(aImapMailFolderSink); + newFolderSink = true; + } + } + if (newFolderSink) Log("SetupSinkProxy", nullptr, "got m_imapMailFolderSink"); + + if (!m_imapMessageSink) { + nsCOMPtr<nsIImapMessageSink> aImapMessageSink; + (void)m_runningUrl->GetImapMessageSink(getter_AddRefs(aImapMessageSink)); + if (aImapMessageSink) { + m_imapMessageSink = new ImapMessageSinkProxy(aImapMessageSink); + } else { + return NS_ERROR_ILLEGAL_VALUE; + } + } + if (!m_imapServerSink) { + nsCOMPtr<nsIImapServerSink> aImapServerSink; + res = m_runningUrl->GetImapServerSink(getter_AddRefs(aImapServerSink)); + if (aImapServerSink) { + m_imapServerSink = new ImapServerSinkProxy(aImapServerSink); + m_imapServerSinkLatest = m_imapServerSink; + } else { + return NS_ERROR_ILLEGAL_VALUE; + } + } + if (!m_imapProtocolSink) { + nsCOMPtr<nsIImapProtocolSink> anImapProxyHelper( + do_QueryInterface(NS_ISUPPORTS_CAST(nsIImapProtocolSink*, this), &res)); + m_imapProtocolSink = new ImapProtocolSinkProxy(anImapProxyHelper); + } + return NS_OK; +} + +static void SetSecurityCallbacksFromChannel(nsISocketTransport* aTrans, + nsIChannel* aChannel) { + nsCOMPtr<nsIInterfaceRequestor> callbacks; + aChannel->GetNotificationCallbacks(getter_AddRefs(callbacks)); + + nsCOMPtr<nsILoadGroup> loadGroup; + aChannel->GetLoadGroup(getter_AddRefs(loadGroup)); + + nsCOMPtr<nsIInterfaceRequestor> securityCallbacks; + NS_NewNotificationCallbacksAggregation(callbacks, loadGroup, + getter_AddRefs(securityCallbacks)); + if (securityCallbacks) aTrans->SetSecurityCallbacks(securityCallbacks); +} + +// Setup With Url is intended to set up data which is held on a PER URL basis +// and not a per connection basis. If you have data which is independent of the +// url we are currently running, then you should put it in Initialize(). This is +// only ever called from the UI thread. It is called from LoadImapUrl, right +// before the url gets run - i.e., the url is next in line to run. +// See also ReleaseUrlState(), which frees a bunch of the things set up in here. +nsresult nsImapProtocol::SetupWithUrl(nsIURI* aURL, nsISupports* aConsumer) { + nsresult rv = NS_ERROR_FAILURE; + NS_ASSERTION(aURL, "null URL passed into Imap Protocol"); + if (aURL) { + nsCOMPtr<nsIImapUrl> imapURL = do_QueryInterface(aURL, &rv); + NS_ENSURE_SUCCESS(rv, rv); + m_runningUrl = imapURL; + m_runningUrlLatest = m_runningUrl; + + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(m_runningUrl); + nsCOMPtr<nsIMsgIncomingServer> server = do_QueryReferent(m_server); + if (!server) { + rv = mailnewsUrl->GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + m_server = do_GetWeakReference(server); + } + nsCOMPtr<nsIMsgFolder> folder; + mailnewsUrl->GetFolder(getter_AddRefs(folder)); + mFolderLastModSeq = 0; + mFolderTotalMsgCount = 0; + mFolderHighestUID = 0; + m_uidValidity = kUidUnknown; + if (folder) { + nsCOMPtr<nsIMsgDatabase> folderDB; + nsCOMPtr<nsIDBFolderInfo> folderInfo; + folder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), + getter_AddRefs(folderDB)); + if (folderInfo) { + nsCString modSeqStr; + folderInfo->GetCharProperty(kModSeqPropertyName, modSeqStr); + mFolderLastModSeq = ParseUint64Str(modSeqStr.get()); + folderInfo->GetNumMessages(&mFolderTotalMsgCount); + folderInfo->GetUint32Property(kHighestRecordedUIDPropertyName, 0, + &mFolderHighestUID); + folderInfo->GetImapUidValidity(&m_uidValidity); + } + } + nsCOMPtr<nsIImapIncomingServer> imapServer = do_QueryInterface(server); + nsCOMPtr<nsIStreamListener> aRealStreamListener = + do_QueryInterface(aConsumer); + m_runningUrl->GetMockChannel(getter_AddRefs(m_mockChannel)); + imapServer->GetIsGMailServer(&m_isGmailServer); + if (!m_mockChannel) { + nsCOMPtr<nsIPrincipal> nullPrincipal = + NullPrincipal::CreateWithoutOriginAttributes(); + + // there are several imap operations that aren't initiated via a + // nsIChannel::AsyncOpen call on the mock channel. such as selecting a + // folder. nsImapProtocol now insists on a mock channel when processing a + // url. + nsCOMPtr<nsIChannel> channel; + rv = + NS_NewChannel(getter_AddRefs(channel), aURL, nullPrincipal, + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_OTHER); + m_mockChannel = do_QueryInterface(channel); + NS_ASSERTION(m_mockChannel, + "failed to get a mock channel in nsImapProtocol"); + + // Certain imap operations (not initiated by the IO Service via AsyncOpen) + // can be interrupted by the stop button on the toolbar. We do this by + // using the loadgroup of the docshell for the message pane. We really + // shouldn't be doing this.. See the comment in + // nsMsgMailNewsUrl::GetLoadGroup. + nsCOMPtr<nsILoadGroup> loadGroup; + mailnewsUrl->GetLoadGroup( + getter_AddRefs(loadGroup)); // get the message pane load group + if (loadGroup) + loadGroup->AddRequest(m_mockChannel, nullptr /* context isupports */); + } + + if (m_mockChannel) { + m_mockChannel->SetImapProtocol(this); + // if we have a listener from a mock channel, over-ride the consumer that + // was passed in + nsCOMPtr<nsIStreamListener> channelListener; + m_mockChannel->GetChannelListener(getter_AddRefs(channelListener)); + if (channelListener) // only over-ride if we have a non null channel + // listener + aRealStreamListener = channelListener; + nsCOMPtr<nsIMsgWindow> msgWindow; + GetMsgWindow(getter_AddRefs(msgWindow)); + if (!msgWindow) GetTopmostMsgWindow(getter_AddRefs(msgWindow)); + if (msgWindow) { + // Set up the MockChannel to attempt nsIProgressEventSink callbacks on + // the messageWindow, with fallback to the docShell (and the + // loadgroup). + nsCOMPtr<nsIDocShell> docShell; + msgWindow->GetMessageWindowDocShell(getter_AddRefs(docShell)); + nsCOMPtr<nsIInterfaceRequestor> ir(do_QueryInterface(docShell)); + nsCOMPtr<nsIInterfaceRequestor> interfaceRequestor; + msgWindow->GetNotificationCallbacks(getter_AddRefs(interfaceRequestor)); + nsCOMPtr<nsIInterfaceRequestor> aggregateIR; + NS_NewInterfaceRequestorAggregation(interfaceRequestor, ir, + getter_AddRefs(aggregateIR)); + m_mockChannel->SetNotificationCallbacks(aggregateIR); + } + } + + // since we'll be making calls directly from the imap thread to the channel + // listener, we need to turn it into a proxy object....we'll assume that the + // listener is on the same thread as the event sink queue + if (aRealStreamListener) { + NS_ASSERTION(!m_channelListener, + "shouldn't already have a channel listener"); + m_channelListener = new StreamListenerProxy(aRealStreamListener); + } + + server->GetHostName(m_hostName); + int32_t authMethod; + (void)server->GetAuthMethod(&authMethod); + InitPrefAuthMethods(authMethod, server); + (void)server->GetSocketType(&m_socketType); + bool shuttingDown; + (void)imapServer->GetShuttingDown(&shuttingDown); + if (!shuttingDown) + (void)imapServer->GetUseIdle(&m_useIdle); + else + m_useIdle = false; + imapServer->GetFetchByChunks(&m_fetchByChunks); + imapServer->GetSendID(&m_sendID); + + nsAutoString trashFolderPath; + if (NS_SUCCEEDED(imapServer->GetTrashFolderName(trashFolderPath))) { + if (m_allowUTF8Accept) + CopyUTF16toUTF8(trashFolderPath, m_trashFolderPath); + else + CopyUTF16toMUTF7(trashFolderPath, m_trashFolderPath); + } + + nsCOMPtr<nsIPrefBranch> prefBranch( + do_GetService(NS_PREFSERVICE_CONTRACTID)); + if (prefBranch) { + bool preferPlainText; + prefBranch->GetBoolPref("mailnews.display.prefer_plaintext", + &preferPlainText); + // If the pref has changed since the last time we ran a url, + // clear the shell cache for this host. (bodyshell no longer exists.) + if (preferPlainText != m_preferPlainText) { + m_preferPlainText = preferPlainText; + } + } + // If enabled, retrieve the clientid so that we can use it later. + bool clientidEnabled = false; + if (NS_SUCCEEDED(server->GetClientidEnabled(&clientidEnabled)) && + clientidEnabled) + server->GetClientid(m_clientId); + else { + m_clientId.Truncate(); + } + + bool proxyCallback = false; + if (m_runningUrl && !m_transport /* and we don't have a transport yet */) { + if (m_mockChannel) { + rv = MsgExamineForProxyAsync(m_mockChannel, this, + getter_AddRefs(m_proxyRequest)); + if (NS_FAILED(rv)) { + rv = SetupWithUrlCallback(nullptr); + } else { + proxyCallback = true; + } + } + } + + if (!proxyCallback) rv = LoadImapUrlInternal(); + } + + return rv; +} + +// nsIProtocolProxyCallback +NS_IMETHODIMP +nsImapProtocol::OnProxyAvailable(nsICancelable* aRequest, nsIChannel* aChannel, + nsIProxyInfo* aProxyInfo, nsresult aStatus) { + // If we're called with NS_BINDING_ABORTED, the IMAP thread already died, + // so we can't carry on. Otherwise, no checking of 'aStatus' here, see + // nsHttpChannel::OnProxyAvailable(). Status is non-fatal and we just kick on. + if (aStatus == NS_BINDING_ABORTED) return NS_ERROR_FAILURE; + + nsresult rv = SetupWithUrlCallback(aProxyInfo); + if (NS_FAILED(rv)) { + // Cancel the protocol and be done. + if (m_mockChannel) m_mockChannel->Cancel(rv); + return rv; + } + + rv = LoadImapUrlInternal(); + if (NS_FAILED(rv)) { + if (m_mockChannel) m_mockChannel->Cancel(rv); + } + + return rv; +} + +nsresult nsImapProtocol::SetupWithUrlCallback(nsIProxyInfo* aProxyInfo) { + m_proxyRequest = nullptr; + + nsresult rv; + + nsCOMPtr<nsISocketTransportService> socketService = + do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &rv); + if (NS_FAILED(rv)) return rv; + + Log("SetupWithUrlCallback", nullptr, "clearing IMAP_CONNECTION_IS_OPEN"); + ClearFlag(IMAP_CONNECTION_IS_OPEN); + const char* connectionType = nullptr; + + if (m_socketType == nsMsgSocketType::SSL) + connectionType = "ssl"; + else if (m_socketType == nsMsgSocketType::alwaysSTARTTLS) + connectionType = "starttls"; + // This can go away once we think everyone is migrated + // away from the trySTARTTLS socket type. + else if (m_socketType == nsMsgSocketType::trySTARTTLS) + connectionType = "starttls"; + + int32_t port = -1; + nsCOMPtr<nsIURI> uri = do_QueryInterface(m_runningUrl, &rv); + if (NS_FAILED(rv)) return rv; + uri->GetPort(&port); + + AutoTArray<nsCString, 1> connectionTypeArray; + if (connectionType) connectionTypeArray.AppendElement(connectionType); + // NOTE: Some errors won't show up until the first read attempt (SSL bad + // certificate errors, for example). + rv = socketService->CreateTransport(connectionTypeArray, m_hostName, port, + aProxyInfo, nullptr, + getter_AddRefs(m_transport)); + if (NS_FAILED(rv) && m_socketType == nsMsgSocketType::trySTARTTLS) { + connectionType = nullptr; + m_socketType = nsMsgSocketType::plain; + rv = socketService->CreateTransport(connectionTypeArray, m_hostName, port, + aProxyInfo, nullptr, + getter_AddRefs(m_transport)); + } + + // remember so we can know whether we can issue a start tls or not... + m_connectionType = connectionType; + if (m_transport && m_mockChannel) { + uint8_t qos; + rv = GetQoSBits(&qos); + if (NS_SUCCEEDED(rv)) m_transport->SetQoSBits(qos); + + // Ensure that the socket can get the notification callbacks + SetSecurityCallbacksFromChannel(m_transport, m_mockChannel); + + // open buffered, blocking input stream + rv = m_transport->OpenInputStream(nsITransport::OPEN_BLOCKING, 0, 0, + getter_AddRefs(m_inputStream)); + if (NS_FAILED(rv)) return rv; + + // open buffered, blocking output stream + rv = m_transport->OpenOutputStream(nsITransport::OPEN_BLOCKING, 0, 0, + getter_AddRefs(m_outputStream)); + if (NS_FAILED(rv)) return rv; + SetFlag(IMAP_CONNECTION_IS_OPEN); + } + + return rv; +} + +// when the connection is done processing the current state, free any per url +// state data... +void nsImapProtocol::ReleaseUrlState(bool rerunning) { + // clear out the socket's reference to the notification callbacks for this + // transaction + { + MutexAutoLock mon(mLock); + if (m_transport) { + m_transport->SetSecurityCallbacks(nullptr); + m_transport->SetEventSink(nullptr, nullptr); + } + } + + if (m_mockChannel && !rerunning) { + // Proxy the close of the channel to the ui thread. + if (m_imapMailFolderSink) + m_imapMailFolderSink->CloseMockChannel(m_mockChannel); + else + m_mockChannel->Close(); + + { + // grab a lock so m_mockChannel doesn't get cleared out + // from under us. + MutexAutoLock mon(mLock); + if (m_mockChannel) { + // Proxy the release of the channel to the main thread. This is + // something that the xpcom proxy system should do for us! + NS_ReleaseOnMainThread("nsImapProtocol::m_mockChannel", + m_mockChannel.forget()); + } + } + } + + m_imapMessageSink = nullptr; + + // Proxy the release of the listener to the main thread. This is something + // that the xpcom proxy system should do for us! + { + // grab a lock so the m_channelListener doesn't get cleared. + MutexAutoLock mon(mLock); + if (m_channelListener) { + NS_ReleaseOnMainThread("nsImapProtocol::m_channelListener", + m_channelListener.forget()); + } + } + m_channelInputStream = nullptr; + m_channelOutputStream = nullptr; + + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl; + nsCOMPtr<nsIImapMailFolderSink> saveFolderSink; + + { + MutexAutoLock mon(mLock); + if (m_runningUrl) { + mailnewsurl = do_QueryInterface(m_runningUrl); + // It is unclear what 'saveFolderSink' is used for, most likely to hold + // a reference for a little longer. See bug 1324893 and bug 391259. + saveFolderSink = m_imapMailFolderSink; + + m_runningUrl = + nullptr; // force us to release our last reference on the url + m_urlInProgress = false; + } + } + // Need to null this out whether we have an m_runningUrl or not + m_imapMailFolderSink = nullptr; + + // we want to make sure the imap protocol's last reference to the url gets + // released back on the UI thread. This ensures that the objects the imap url + // hangs on to properly get released back on the UI thread. + if (mailnewsurl) { + NS_ReleaseOnMainThread("nsImapProtocol::m_runningUrl", + mailnewsurl.forget()); + } + saveFolderSink = nullptr; +} + +class nsImapCancelProxy : public mozilla::Runnable { + public: + explicit nsImapCancelProxy(nsICancelable* aProxyRequest) + : mozilla::Runnable("nsImapCancelProxy"), mRequest(aProxyRequest) {} + NS_IMETHOD Run() { + if (mRequest) mRequest->Cancel(NS_BINDING_ABORTED); + return NS_OK; + } + + private: + nsCOMPtr<nsICancelable> mRequest; +}; + +bool nsImapProtocol::RunImapThreadMainLoop() { + PR_CEnterMonitor(this); + NS_ASSERTION(!m_imapThreadIsRunning, + "Oh. oh. thread is already running. What's wrong here?"); + if (m_imapThreadIsRunning) { + PR_CExitMonitor(this); + return false; + } + + m_imapThreadIsRunning = true; + PR_CExitMonitor(this); + + // call the platform specific main loop .... + ImapThreadMainLoop(); + + if (m_proxyRequest) { + // Cancel proxy on main thread. + RefPtr<nsImapCancelProxy> cancelProxy = + new nsImapCancelProxy(m_proxyRequest); + NS_DispatchAndSpinEventLoopUntilComplete( + "nsImapProtocol::RunImapThreadMainLoop"_ns, + GetMainThreadSerialEventTarget(), cancelProxy.forget()); + m_proxyRequest = nullptr; + } + + if (m_runningUrl) { + NS_ReleaseOnMainThread("nsImapProtocol::m_runningUrl", + m_runningUrl.forget()); + } + + // close streams via UI thread if it's not already done + if (m_imapProtocolSink) m_imapProtocolSink->CloseStreams(); + + m_imapMailFolderSink = nullptr; + m_imapMailFolderSinkSelected = nullptr; + m_imapMessageSink = nullptr; + + return true; +} + +// +// Must be called from UI thread only +// +NS_IMETHODIMP nsImapProtocol::CloseStreams() { + // make sure that it is called by the UI thread + MOZ_ASSERT(NS_IsMainThread(), + "CloseStreams() should not be called from an off UI thread"); + + { + MutexAutoLock mon(mLock); + if (m_transport) { + // make sure the transport closes (even if someone is still indirectly + // referencing it). + m_transport->Close(NS_ERROR_ABORT); + m_transport = nullptr; + } + m_inputStream = nullptr; + m_outputStream = nullptr; + m_channelListener = nullptr; + if (m_mockChannel) { + m_mockChannel->Close(); + m_mockChannel = nullptr; + } + m_channelInputStream = nullptr; + m_channelOutputStream = nullptr; + + // Close scope because we must let go of the monitor before calling + // RemoveConnection to unblock anyone who tries to get a monitor to the + // protocol object while holding onto a monitor to the server. + } + nsCOMPtr<nsIMsgIncomingServer> me_server = do_QueryReferent(m_server); + if (me_server) { + nsresult result; + nsCOMPtr<nsIImapIncomingServer> aImapServer( + do_QueryInterface(me_server, &result)); + if (NS_SUCCEEDED(result)) aImapServer->RemoveConnection(this); + me_server = nullptr; + } + m_server = nullptr; + // take this opportunity of being on the UI thread to + // persist chunk prefs if they've changed + if (gChunkSizeDirty) { + nsCOMPtr<nsIPrefBranch> prefBranch( + do_GetService(NS_PREFSERVICE_CONTRACTID)); + if (prefBranch) { + prefBranch->SetIntPref("mail.imap.chunk_size", gChunkSize); + prefBranch->SetIntPref("mail.imap.min_chunk_size_threshold", + gChunkThreshold); + gChunkSizeDirty = false; + } + } + return NS_OK; +} + +NS_IMETHODIMP nsImapProtocol::GetUrlWindow(nsIMsgMailNewsUrl* aUrl, + nsIMsgWindow** aMsgWindow) { + NS_ENSURE_ARG_POINTER(aUrl); + NS_ENSURE_ARG_POINTER(aMsgWindow); + return aUrl->GetMsgWindow(aMsgWindow); +} + +NS_IMETHODIMP nsImapProtocol::SetupMainThreadProxies() { + return SetupSinkProxy(); +} + +NS_IMETHODIMP nsImapProtocol::OnInputStreamReady(nsIAsyncInputStream* inStr) { + // should we check if it's a close vs. data available? + if (m_idle) { + uint64_t bytesAvailable = 0; + (void)inStr->Available(&bytesAvailable); + // check if data available - might be a close + if (bytesAvailable != 0) { + ReentrantMonitorAutoEnter mon(m_urlReadyToRunMonitor); + m_lastActiveTime = PR_Now(); + m_idleResponseReadyToHandle = true; + mon.Notify(); + } + } + return NS_OK; +} + +// this is to be called from the UI thread. It sets m_threadShouldDie, +// and then signals the imap thread, which, when it wakes up, should exit. +// The imap thread cleanup code will check m_safeToCloseConnection. +NS_IMETHODIMP +nsImapProtocol::TellThreadToDie(bool aIsSafeToClose) { + MOZ_DIAGNOSTIC_ASSERT( + NS_IsMainThread(), + "TellThreadToDie(aIsSafeToClose) should only be called from UI thread"); + MutexAutoLock mon(mLock); + + nsCOMPtr<nsIMsgIncomingServer> me_server = do_QueryReferent(m_server); + if (me_server) { + nsresult rv; + nsCOMPtr<nsIImapIncomingServer> aImapServer( + do_QueryInterface(me_server, &rv)); + if (NS_SUCCEEDED(rv)) aImapServer->RemoveConnection(this); + m_server = nullptr; + me_server = nullptr; + } + { + ReentrantMonitorAutoEnter deathMon(m_threadDeathMonitor); + m_safeToCloseConnection = aIsSafeToClose; + m_threadShouldDie = true; + } + ReentrantMonitorAutoEnter readyMon(m_urlReadyToRunMonitor); + m_nextUrlReadyToRun = true; + readyMon.Notify(); + return NS_OK; +} + +/** + * Dispatch socket thread to to determine if connection is alive. + */ +nsresult nsImapProtocol::IsTransportAlive(bool* alive) { + nsresult rv; + auto GetIsAlive = [transport = nsCOMPtr{m_transport}, &rv, alive]() mutable { + rv = transport->IsAlive(alive); + }; + nsCOMPtr<nsIEventTarget> socketThread( + do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID)); + if (socketThread) { + mozilla::SyncRunnable::DispatchToThread( + socketThread, + NS_NewRunnableFunction("nsImapProtocol::IsTransportAlive", GetIsAlive)); + } else { + rv = NS_ERROR_NOT_AVAILABLE; + } + return rv; +} + +/** + * Dispatch socket thread to initiate STARTTLS handshakes. + */ +nsresult nsImapProtocol::TransportStartTLS() { + nsresult rv = NS_ERROR_NOT_AVAILABLE; + nsCOMPtr<nsITLSSocketControl> tlsSocketControl; + if (m_transport && + NS_SUCCEEDED( + m_transport->GetTlsSocketControl(getter_AddRefs(tlsSocketControl))) && + tlsSocketControl) { + auto CallStartTLS = [sockCon = nsCOMPtr{tlsSocketControl}, &rv]() mutable { + rv = sockCon->StartTLS(); + }; + nsCOMPtr<nsIEventTarget> socketThread( + do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID)); + if (socketThread) { + mozilla::SyncRunnable::DispatchToThread( + socketThread, NS_NewRunnableFunction( + "nsImapProtocol::TransportStartTLS", CallStartTLS)); + } else { + rv = NS_ERROR_NOT_AVAILABLE; + } + } + return rv; +} + +/** + * Dispatch socket thread to obtain transport security information. + */ +void nsImapProtocol::GetTransportSecurityInfo( + nsITransportSecurityInfo** aSecurityInfo) { + *aSecurityInfo = nullptr; + nsCOMPtr<nsIEventTarget> socketThread( + do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID)); + nsCOMPtr<nsITLSSocketControl> tlsSocketControl; + if (socketThread && + NS_SUCCEEDED( + m_transport->GetTlsSocketControl(getter_AddRefs(tlsSocketControl))) && + tlsSocketControl) { + if (socketThread) { + nsCOMPtr<nsITransportSecurityInfo> secInfo; + auto GetSecurityInfo = [&tlsSocketControl, &secInfo]() mutable { + tlsSocketControl->GetSecurityInfo(getter_AddRefs(secInfo)); + }; + mozilla::SyncRunnable::DispatchToThread( + socketThread, + NS_NewRunnableFunction("nsImapProtocol::GetTransportSecurityInfo", + GetSecurityInfo)); + NS_IF_ADDREF(*aSecurityInfo = secInfo); + } + } +} + +void nsImapProtocol::TellThreadToDie() { + nsresult rv = NS_OK; + MOZ_DIAGNOSTIC_ASSERT( + !NS_IsMainThread(), + "TellThreadToDie() should not be called from UI thread"); + + // prevent re-entering this method because it may lock the UI. + if (m_inThreadShouldDie) return; + m_inThreadShouldDie = true; + + // This routine is called only from the imap protocol thread. + // The UI thread causes this to be called by calling TellThreadToDie. + // In that case, m_safeToCloseConnection will be FALSE if it's dropping a + // timed out connection, true when closing a cached connection. + // We're using PR_CEnter/ExitMonitor because Monitors don't like having + // us to hold one monitor and call code that gets a different monitor. And + // some of the methods we call here use Monitors. + PR_CEnterMonitor(this); + + m_urlInProgress = true; // let's say it's busy so no one tries to use + // this about to die connection. + bool urlWritingData = false; + bool connectionIdle = !m_runningUrl; + + if (!connectionIdle) + urlWritingData = m_imapAction == nsIImapUrl::nsImapAppendMsgFromFile || + m_imapAction == nsIImapUrl::nsImapAppendDraftFromFile; + + bool closeNeeded = GetServerStateParser().GetIMAPstate() == + nsImapServerResponseParser::kFolderSelected && + m_safeToCloseConnection; + nsCString command; + // if a url is writing data, we can't even logout, so we're just + // going to close the connection as if the user pressed stop. + if (m_currentServerCommandTagNumber > 0 && !urlWritingData) { + bool isAlive = false; + if (m_transport) rv = IsTransportAlive(&isAlive); + + if (TestFlag(IMAP_CONNECTION_IS_OPEN) && m_idle && isAlive) EndIdle(false); + + if (NS_SUCCEEDED(rv) && isAlive && closeNeeded && + GetDeleteIsMoveToTrash() && TestFlag(IMAP_CONNECTION_IS_OPEN) && + m_outputStream) + ImapClose(true, connectionIdle); + + if (NS_SUCCEEDED(rv) && isAlive && TestFlag(IMAP_CONNECTION_IS_OPEN) && + NS_SUCCEEDED(GetConnectionStatus()) && m_outputStream) + Logout(true, connectionIdle); + } + PR_CExitMonitor(this); + // close streams via UI thread + if (m_imapProtocolSink) { + m_imapProtocolSink->CloseStreams(); + m_imapProtocolSink = nullptr; + } + Log("TellThreadToDie", nullptr, "close socket connection"); + + { + ReentrantMonitorAutoEnter mon(m_threadDeathMonitor); + m_threadShouldDie = true; + } + { + ReentrantMonitorAutoEnter dataMon(m_dataAvailableMonitor); + dataMon.Notify(); + } + ReentrantMonitorAutoEnter urlReadyMon(m_urlReadyToRunMonitor); + urlReadyMon.NotifyAll(); +} + +NS_IMETHODIMP +nsImapProtocol::GetLastActiveTimeStamp(PRTime* aTimeStamp) { + if (aTimeStamp) *aTimeStamp = m_lastActiveTime; + return NS_OK; +} + +static void DoomCacheEntry(nsIMsgMailNewsUrl* url); +NS_IMETHODIMP +nsImapProtocol::PseudoInterruptMsgLoad(nsIMsgFolder* aImapFolder, + nsIMsgWindow* aMsgWindow, + bool* interrupted) { + NS_ENSURE_ARG(interrupted); + + *interrupted = false; + + PR_CEnterMonitor(this); + + if (m_runningUrl && !TestFlag(IMAP_CLEAN_UP_URL_STATE)) { + nsImapAction imapAction; + m_runningUrl->GetImapAction(&imapAction); + + if (imapAction == nsIImapUrl::nsImapMsgFetch) { + nsresult rv = NS_OK; + nsCOMPtr<nsIImapUrl> runningImapURL; + + rv = GetRunningImapURL(getter_AddRefs(runningImapURL)); + if (NS_SUCCEEDED(rv) && runningImapURL) { + nsCOMPtr<nsIMsgFolder> runningImapFolder; + nsCOMPtr<nsIMsgWindow> msgWindow; + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = + do_QueryInterface(runningImapURL); + mailnewsUrl->GetMsgWindow(getter_AddRefs(msgWindow)); + mailnewsUrl->GetFolder(getter_AddRefs(runningImapFolder)); + if (aImapFolder == runningImapFolder && msgWindow == aMsgWindow) { + MOZ_LOG(IMAPCache, LogLevel::Debug, + ("%s: Set PseudoInterrupt", __func__)); + PseudoInterrupt(true); + *interrupted = true; + } + // If we're pseudo-interrupted, doom any incomplete cache entry. + // But if mock channel indicates fetch is complete so cache write is + // done, then don't doom the cache entry. + bool cacheWriteInProgress = true; + if (m_mockChannel) + m_mockChannel->GetWritingToCache(&cacheWriteInProgress); + if (cacheWriteInProgress) { + MOZ_LOG(IMAPCache, LogLevel::Debug, + ("%s: Call DoomCacheEntry()", __func__)); + DoomCacheEntry(mailnewsUrl); + } + } + } + } + PR_CExitMonitor(this); +#ifdef DEBUG_bienvenu + printf("interrupt msg load : %s\n", (*interrupted) ? "TRUE" : "FALSE"); +#endif + return NS_OK; +} + +void nsImapProtocol::ImapThreadMainLoop() { + MOZ_LOG(IMAP, LogLevel::Debug, + ("ImapThreadMainLoop entering [this=%p]", this)); + + PRIntervalTime sleepTime = kImapSleepTime; + bool idlePending = false; + while (!DeathSignalReceived()) { + nsresult rv = NS_OK; + bool urlReadyToRun; + + // wait for a URL or idle response to process... + { + ReentrantMonitorAutoEnter mon(m_urlReadyToRunMonitor); + + while (NS_SUCCEEDED(rv) && !DeathSignalReceived() && + !m_nextUrlReadyToRun && !m_idleResponseReadyToHandle && + !m_threadShouldDie) { + rv = mon.Wait(sleepTime); + if (idlePending) break; + } + + urlReadyToRun = m_nextUrlReadyToRun; + m_nextUrlReadyToRun = false; + } + // This will happen if the UI thread signals us to die + if (m_threadShouldDie) { + TellThreadToDie(); + break; + } + + if (NS_FAILED(rv) && PR_PENDING_INTERRUPT_ERROR == PR_GetError()) { + printf("error waiting for monitor\n"); + break; + } + + // If an idle response has occurred, handle only it on this pass through + // the loop. + if (m_idleResponseReadyToHandle && !m_threadShouldDie) { + m_idleResponseReadyToHandle = false; + HandleIdleResponses(); + if (urlReadyToRun) { + // A URL is also ready. Process it on next loop. + m_nextUrlReadyToRun = true; + urlReadyToRun = false; + } + } + + if (urlReadyToRun && m_runningUrl) { + if (m_currentServerCommandTagNumber && m_transport) { + bool isAlive; + rv = IsTransportAlive(&isAlive); + // if the transport is not alive, and we've ever sent a command with + // this connection, kill it. otherwise, we've probably just not finished + // setting it so don't kill it! + if (NS_FAILED(rv) || !isAlive) { + // This says we never started running the url, which is the case. + m_runningUrl->SetRerunningUrl(false); + RetryUrl(); + return; + } + } + // + // NOTE: Although we cleared m_nextUrlReadyToRun above, it may now be set + // again by LoadImapUrlInternal(), which runs on the main thread. + // Because of this, we must not clear m_nextUrlReadyToRun here. + // + if (ProcessCurrentURL()) { + // Another URL has been setup to run. Process it on next loop. + m_nextUrlReadyToRun = true; + m_imapMailFolderSink = nullptr; + } else { + // No more URLs setup to run. Set idle pending if user has configured + // idle and if a URL is not in progress and if the server has IDLE + // capability. Just set idlePending since want to wait a short time + // to see if more URLs occurs before actually entering idle. + if (!idlePending && m_useIdle && !m_urlInProgress && + GetServerStateParser().GetCapabilityFlag() & kHasIdleCapability && + GetServerStateParser().GetIMAPstate() == + nsImapServerResponseParser::kFolderSelected) { + // Set-up short wait time in milliseconds + static const PRIntervalTime kIdleWait = + PR_MillisecondsToInterval(2000); + sleepTime = kIdleWait; + idlePending = true; + Log("ImapThreadMainLoop", nullptr, "idlePending set"); + } else { + // if not idle used, don't need to remember folder sink + m_imapMailFolderSink = nullptr; + } + } + } else { + // No URL to run detected on wake up. + if (idlePending) { + // Have seen no URLs for the short time (kIdleWait) so go into idle mode + // and set the loop sleep time back to its original longer time. + Idle(); + if (!m_idle) { + // Server rejected IDLE. Treat like IDLE not enabled or available. + m_imapMailFolderSink = nullptr; + } + idlePending = false; + sleepTime = kImapSleepTime; + } + } + if (!GetServerStateParser().Connected()) break; +#ifdef DEBUG_bienvenu + else + printf("ready to run but no url and not idle\n"); +#endif + // This can happen if the UI thread closes cached connections in the + // OnStopRunningUrl notification. + if (m_threadShouldDie) TellThreadToDie(); + } + m_imapThreadIsRunning = false; + + MOZ_LOG(IMAP, LogLevel::Debug, + ("ImapThreadMainLoop leaving [this=%p]", this)); +} + +// This handles the response to Idle() command and handles responses sent by +// the server after in idle mode. Returns true if a BAD or NO response is +// not seen which is needed when called from Idle(). +bool nsImapProtocol::HandleIdleResponses() { + bool rvIdleOk = true; + bool untagged = false; + NS_ASSERTION(PL_strstr(m_currentCommand.get(), " IDLE"), "not IDLE!"); + do { + ParseIMAPandCheckForNewMail(); + rvIdleOk = rvIdleOk && !GetServerStateParser().CommandFailed(); + untagged = untagged || GetServerStateParser().UntaggedResponse(); + } while (m_inputStreamBuffer->NextLineAvailable() && + GetServerStateParser().Connected()); + + // If still connected and rvIdleOk is true and an untagged response was + // detected and have the sink pointer, OnNewIdleMessage will invoke a URL to + // update the folder. Otherwise just setup so we get notified of idle response + // data on the socket transport thread by OnInputStreamReady() above, which + // will trigger the imap thread main loop to run and call this function again. + if (GetServerStateParser().Connected() && rvIdleOk) { + if (m_imapMailFolderSinkSelected && untagged) { + Log("HandleIdleResponses", nullptr, "idle response"); + m_imapMailFolderSinkSelected->OnNewIdleMessages(); + } else { + // Enable async wait mode. Occurs when Idle() called. + nsCOMPtr<nsIAsyncInputStream> asyncInputStream = + do_QueryInterface(m_inputStream); + if (asyncInputStream) { + asyncInputStream->AsyncWait(this, 0, 0, nullptr); + Log("HandleIdleResponses", nullptr, "idle mode async waiting"); + } + } + } + return rvIdleOk; +} + +void nsImapProtocol::EstablishServerConnection() { +#define ESC_LENGTH(x) (sizeof(x) - 1) +#define ESC_OK "* OK" +#define ESC_OK_LEN ESC_LENGTH(ESC_OK) +#define ESC_PREAUTH "* PREAUTH" +#define ESC_PREAUTH_LEN ESC_LENGTH(ESC_PREAUTH) +#define ESC_CAPABILITY_STAR "* " +#define ESC_CAPABILITY_STAR_LEN ESC_LENGTH(ESC_CAPABILITY_STAR) +#define ESC_CAPABILITY_OK "* OK [" +#define ESC_CAPABILITY_OK_LEN ESC_LENGTH(ESC_CAPABILITY_OK) +#define ESC_CAPABILITY_GREETING (ESC_CAPABILITY_OK "CAPABILITY") +#define ESC_CAPABILITY_GREETING_LEN ESC_LENGTH(ESC_CAPABILITY_GREETING) + + char* serverResponse = CreateNewLineFromSocket(); // read in the greeting + // record the fact that we've received a greeting for this connection so we + // don't ever try to do it again.. + if (serverResponse) SetFlag(IMAP_RECEIVED_GREETING); + + if (!PL_strncasecmp(serverResponse, ESC_OK, ESC_OK_LEN)) { + SetConnectionStatus(NS_OK); + + if (!PL_strncasecmp(serverResponse, ESC_CAPABILITY_GREETING, + ESC_CAPABILITY_GREETING_LEN)) { + nsAutoCString tmpstr(serverResponse); + int32_t endIndex = tmpstr.FindChar(']', ESC_CAPABILITY_GREETING_LEN); + if (endIndex >= 0) { + // Allocate the new buffer here. This buffer will be passed to + // ParseIMAPServerResponse() where it will be used to fill the + // fCurrentLine field and will be freed by the next call to + // ResetLexAnalyzer(). + char* fakeServerResponse = (char*)PR_Malloc(PL_strlen(serverResponse)); + // Munge the greeting into something that would pass for an IMAP + // server's response to a "CAPABILITY" command. + strcpy(fakeServerResponse, ESC_CAPABILITY_STAR); + strcat(fakeServerResponse, serverResponse + ESC_CAPABILITY_OK_LEN); + fakeServerResponse[endIndex - ESC_CAPABILITY_OK_LEN + + ESC_CAPABILITY_STAR_LEN] = '\0'; + // Tell the response parser that we just issued a "CAPABILITY" and + // got the following back. + GetServerStateParser().ParseIMAPServerResponse("1 CAPABILITY", true, + fakeServerResponse); + } + } + } else if (!PL_strncasecmp(serverResponse, ESC_PREAUTH, ESC_PREAUTH_LEN)) { + // PREAUTH greeting received. We've been pre-authenticated by the server. + // We can skip sending a password and transition right into the + // kAuthenticated state; but we won't if the user has configured STARTTLS. + // (STARTTLS can only occur with the server in non-authenticated state.) + if (!(m_socketType == nsMsgSocketType::alwaysSTARTTLS || + m_socketType == nsMsgSocketType::trySTARTTLS)) { + GetServerStateParser().PreauthSetAuthenticatedState(); + + if (GetServerStateParser().GetCapabilityFlag() == kCapabilityUndefined) + Capability(); + + if (!(GetServerStateParser().GetCapabilityFlag() & + (kIMAP4Capability | kIMAP4rev1Capability | kIMAP4other))) { + // AlertUserEventUsingId(MK_MSG_IMAP_SERVER_NOT_IMAP4); + SetConnectionStatus(NS_ERROR_FAILURE); // stop netlib + } else { + // let's record the user as authenticated. + m_imapServerSink->SetUserAuthenticated(true); + m_hostSessionList->SetPasswordVerifiedOnline(GetImapServerKey()); + + ProcessAfterAuthenticated(); + // the connection was a success + SetConnectionStatus(NS_OK); + } + } else { + // STARTTLS is configured so don't transition to authenticated state. Just + // alert the user, log the error and drop the connection. This may + // indicate a man-in-the middle attack if the user is not expecting + // PREAUTH. The user must change the connection security setting to other + // than STARTTLS to allow PREAUTH to be accepted on subsequent IMAP + // connections. + AlertUserEventUsingName("imapServerDisconnected"); + const nsCString& hostName = GetImapHostName(); + MOZ_LOG( + IMAP, LogLevel::Error, + ("PREAUTH received from IMAP server %s because STARTTLS selected. " + "Connection dropped", + hostName.get())); + SetConnectionStatus(NS_ERROR_FAILURE); // stop netlib + } + } + + PR_Free(serverResponse); // we don't care about the greeting yet... + +#undef ESC_LENGTH +#undef ESC_OK +#undef ESC_OK_LEN +#undef ESC_PREAUTH +#undef ESC_PREAUTH_LEN +#undef ESC_CAPABILITY_STAR +#undef ESC_CAPABILITY_STAR_LEN +#undef ESC_CAPABILITY_OK +#undef ESC_CAPABILITY_OK_LEN +#undef ESC_CAPABILITY_GREETING +#undef ESC_CAPABILITY_GREETING_LEN +} + +// This can get called from the UI thread or an imap thread. +// It makes sure we don't get left with partial messages in +// the memory cache. +static void DoomCacheEntry(nsIMsgMailNewsUrl* url) { + bool readingFromMemCache = false; + nsCOMPtr<nsIImapUrl> imapUrl = do_QueryInterface(url); + imapUrl->GetMsgLoadingFromCache(&readingFromMemCache); + if (!readingFromMemCache) { + nsCOMPtr<nsICacheEntry> cacheEntry; + url->GetMemCacheEntry(getter_AddRefs(cacheEntry)); + if (cacheEntry) { + MOZ_LOG(IMAPCache, LogLevel::Debug, + ("%s: Call AsyncDoom(), url=%s", __func__, + url->GetSpecOrDefault().get())); + cacheEntry->AsyncDoom(nullptr); + } + } +} + +/** + * ProcessCurrentURL() runs the current URL (m_runningUrl). + * Things to remember: + * - IMAP protocol URLs don't correspond directly to IMAP commands. A single + * URL might cause multiple IMAP commands to be issued. + * - This is all synchronous. But that's OK, because we're running in our + * own thread. + * + * @return true if another url was run, false otherwise. + */ +bool nsImapProtocol::ProcessCurrentURL() { + nsresult rv = NS_OK; + if (m_idle) EndIdle(); + + if (m_retryUrlOnError) { + // we clear this flag if we're re-running immediately, because that + // means we never sent a start running url notification, and later we + // don't send start running notification if we think we're rerunning + // the url (see first call to SetUrlState below). This means we won't + // send a start running notification, which means our stop running + // notification will be ignored because we don't think we were running. + m_runningUrl->SetRerunningUrl(false); + return RetryUrl(); + } + Log("ProcessCurrentURL", nullptr, "entering"); + (void)GetImapHostName(); // force m_hostName to get set. + + bool logonFailed = false; + bool anotherUrlRun = false; + bool rerunningUrl = false; + bool isExternalUrl; + bool validUrl = true; + + PseudoInterrupt(false); // clear this if left over from previous url. + + m_runningUrl->GetRerunningUrl(&rerunningUrl); + m_runningUrl->GetExternalLinkUrl(&isExternalUrl); + m_runningUrl->GetValidUrl(&validUrl); + m_runningUrl->GetImapAction(&m_imapAction); + + if (isExternalUrl) { + if (m_imapAction == nsIImapUrl::nsImapSelectFolder) { + // we need to send a start request so that the doc loader + // will call HandleContent on the imap service so we + // can abort this url, and run a new url in a new msg window + // to run the folder load url and get off this crazy merry-go-round. + if (m_channelListener) { + m_channelListener->OnStartRequest(m_mockChannel); + } + return false; + } + } + + if (!m_imapMailFolderSink && m_imapProtocolSink) { + // This occurs when running another URL in the main thread loop + rv = m_imapProtocolSink->SetupMainThreadProxies(); + NS_ENSURE_SUCCESS(rv, false); + } + + // Reinitialize the parser + GetServerStateParser().InitializeState(); + GetServerStateParser().SetConnected(true); + + // acknowledge that we are running the url now.. + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl = + do_QueryInterface(m_runningUrl, &rv); + nsAutoCString urlSpec; + rv = mailnewsurl->GetSpec(urlSpec); + NS_ENSURE_SUCCESS(rv, false); + Log("ProcessCurrentURL", urlSpec.get(), + (validUrl) ? " = currentUrl" : " is not valid"); + if (!validUrl) return false; + + if (NS_SUCCEEDED(rv) && mailnewsurl && m_imapMailFolderSink && !rerunningUrl) + m_imapMailFolderSink->SetUrlState(this, mailnewsurl, true, false, NS_OK); + + // if we are set up as a channel, we should notify our channel listener that + // we are starting... so pass in ourself as the channel and not the underlying + // socket or file channel the protocol happens to be using + if (m_channelListener) // ### not sure we want to do this if rerunning url... + { + m_channelListener->OnStartRequest(m_mockChannel); + } + // If we haven't received the greeting yet, we need to make sure we strip + // it out of the input before we start to do useful things... + if (!TestFlag(IMAP_RECEIVED_GREETING)) EstablishServerConnection(); + + // Step 1: If we have not moved into the authenticated state yet then do so + // by attempting to logon. + if (!DeathSignalReceived() && NS_SUCCEEDED(GetConnectionStatus()) && + (GetServerStateParser().GetIMAPstate() == + nsImapServerResponseParser::kNonAuthenticated)) { + /* if we got here, the server's greeting should not have been PREAUTH */ + // If greeting did not contain a capability response and if user has not + // configured STARTTLS, request capabilities. If STARTTLS configured, + // capabilities will be requested after TLS handshakes are complete. + if ((GetServerStateParser().GetCapabilityFlag() == kCapabilityUndefined) && + (m_socketType != nsMsgSocketType::alwaysSTARTTLS)) { + Capability(); + } + + // If capability response has yet to occur and STARTTLS is not + // configured then drop the connection since this should not happen. Also + // drop the connection if capability response has occurred and + // the imap version is unacceptable. Show alert only for wrong version. + if (((GetServerStateParser().GetCapabilityFlag() == kCapabilityUndefined) && + (m_socketType != nsMsgSocketType::alwaysSTARTTLS)) || + (GetServerStateParser().GetCapabilityFlag() && + !(GetServerStateParser().GetCapabilityFlag() & + (kIMAP4Capability | kIMAP4rev1Capability | kIMAP4other)))) { + if (!DeathSignalReceived() && NS_SUCCEEDED(GetConnectionStatus()) && + GetServerStateParser().GetCapabilityFlag()) + AlertUserEventUsingName("imapServerNotImap4"); + + SetConnectionStatus(NS_ERROR_FAILURE); // stop netlib + } else { + if ((m_connectionType.EqualsLiteral("starttls") && + (m_socketType == nsMsgSocketType::trySTARTTLS && + (GetServerStateParser().GetCapabilityFlag() & + kHasStartTLSCapability))) || + m_socketType == nsMsgSocketType::alwaysSTARTTLS) { + StartTLS(); // Send imap STARTTLS command + if (GetServerStateParser().LastCommandSuccessful()) { + NS_ENSURE_TRUE(m_transport, false); + MOZ_ASSERT(!NS_IsMainThread()); + rv = TransportStartTLS(); // Initiate STARTTLS handshakes + if (NS_SUCCEEDED(rv)) { + // Transition to secure state is now enabled but handshakes and + // negotiation has not yet occurred. Make sure that + // the stream input response buffer is drained to avoid false + // responses to subsequent commands (capability, login etc), + // i.e., due to possible MitM attack doing pre-TLS response + // injection. We are discarding any possible malicious data + // stored prior to TransportStartTLS(). + // Note: If any non-TLS related data arrives while transitioning + // to secure state (after TransportStartTLS()), it will + // cause the TLS negotiation to fail so any injected data is never + // accessed since the transport connection will be dropped. + char discardBuf[80]; + uint64_t numBytesInStream = 0; + uint32_t numBytesRead; + rv = m_inputStream->Available(&numBytesInStream); + nsCOMPtr<nsIInputStream> kungFuGrip = m_inputStream; + // Read and discard any data available in socket buffer. + while (numBytesInStream > 0 && NS_SUCCEEDED(rv)) { + rv = m_inputStream->Read( + discardBuf, + std::min(uint64_t(sizeof discardBuf), numBytesInStream), + &numBytesRead); + numBytesInStream -= numBytesRead; + } + kungFuGrip = nullptr; + + // Discard any data lines previously read from socket buffer. + m_inputStreamBuffer->ClearBuffer(); + + // Force re-issue of "capability", because servers may + // enable other auth features (e.g. remove LOGINDISABLED + // and add AUTH=PLAIN). Sending imap data here first triggers + // the TLS negotiation handshakes. + Capability(); + + // If user has set pref mail.server.serverX.socketType to 1 + // (trySTARTTLS, now deprecated in UI) and Capability() + // succeeds, indicating TLS handshakes succeeded, set and + // latch the socketType to 2 (alwaysSTARTTLS) for this server. + if ((m_socketType == nsMsgSocketType::trySTARTTLS) && + GetServerStateParser().LastCommandSuccessful()) + m_imapServerSink->UpdateTrySTARTTLSPref(true); + + // Courier imap doesn't return STARTTLS capability if we've done + // a STARTTLS! But we need to remember this capability so we'll + // try to use STARTTLS next time. + // Update: This may not be a problem since "next time" will be + // on a new connection that is not yet in secure state. So the + // capability greeting *will* contain STARTTLS. I observed and + // tested this on Courier imap server. But keep this to be sure. + eIMAPCapabilityFlags capabilityFlag = + GetServerStateParser().GetCapabilityFlag(); + if (!(capabilityFlag & kHasStartTLSCapability)) { + capabilityFlag |= kHasStartTLSCapability; + GetServerStateParser().SetCapabilityFlag(capabilityFlag); + CommitCapability(); + } + } + if (NS_FAILED(rv)) { + nsAutoCString logLine("Enable of STARTTLS failed. Error 0x"); + logLine.AppendInt(static_cast<uint32_t>(rv), 16); + Log("ProcessCurrentURL", nullptr, logLine.get()); + if (m_socketType == nsMsgSocketType::alwaysSTARTTLS) { + SetConnectionStatus(rv); // stop netlib + if (m_transport) m_transport->Close(rv); + } else if (m_socketType == nsMsgSocketType::trySTARTTLS) + m_imapServerSink->UpdateTrySTARTTLSPref(false); + } + } else if (m_socketType == nsMsgSocketType::alwaysSTARTTLS) { + SetConnectionStatus(NS_ERROR_FAILURE); // stop netlib + if (m_transport) m_transport->Close(rv); + } else if (m_socketType == nsMsgSocketType::trySTARTTLS) { + // STARTTLS failed, so downgrade socket type + m_imapServerSink->UpdateTrySTARTTLSPref(false); + } + } else if (m_socketType == nsMsgSocketType::trySTARTTLS) { + // we didn't know the server supported TLS when we created + // the socket, so we're going to retry with a STARTTLS socket + if (GetServerStateParser().GetCapabilityFlag() & + kHasStartTLSCapability) { + ClearFlag(IMAP_CONNECTION_IS_OPEN); + TellThreadToDie(); + SetConnectionStatus(NS_ERROR_FAILURE); + return RetryUrl(); + } + // trySTARTTLS set, but server doesn't have TLS capability, + // so downgrade socket type + m_imapServerSink->UpdateTrySTARTTLSPref(false); + m_socketType = nsMsgSocketType::plain; + } + if (!DeathSignalReceived() && (NS_SUCCEEDED(GetConnectionStatus()))) { + logonFailed = !TryToLogon(); + } + if (m_retryUrlOnError) return RetryUrl(); + } + } // if death signal not received + + // We assume one IMAP thread is used for exactly one server, only. + if (m_transport && !m_securityInfo) { + MOZ_ASSERT(!NS_IsMainThread()); + GetTransportSecurityInfo(getter_AddRefs(m_securityInfo)); + } + + if (!DeathSignalReceived() && (NS_SUCCEEDED(GetConnectionStatus()))) { + // if the server supports a language extension then we should + // attempt to issue the language extension. + if (GetServerStateParser().GetCapabilityFlag() & kHasLanguageCapability) + Language(); + + if (m_runningUrl) { + bool foundMailboxesAlready = false; + m_hostSessionList->GetHaveWeEverDiscoveredFoldersForHost( + GetImapServerKey(), foundMailboxesAlready); + if (!foundMailboxesAlready) FindMailboxesIfNecessary(); + } + + nsImapState imapState = nsIImapUrl::ImapStatusNone; + if (m_runningUrl) m_runningUrl->GetRequiredImapState(&imapState); + + if (imapState == nsIImapUrl::nsImapAuthenticatedState) + ProcessAuthenticatedStateURL(); + else // must be a url that requires us to be in the selected state + ProcessSelectedStateURL(); + + if (m_retryUrlOnError) return RetryUrl(); + + // The URL has now been processed + if ((!logonFailed && NS_FAILED(GetConnectionStatus())) || + DeathSignalReceived()) + HandleCurrentUrlError(); + + } else if (!logonFailed) + HandleCurrentUrlError(); + + // if we are set up as a channel, we should notify our channel listener that + // we are stopping... so pass in ourself as the channel and not the underlying + // socket or file channel the protocol happens to be using + if (m_channelListener) { + NS_ASSERTION(m_mockChannel, "no request"); + if (m_mockChannel) { + nsresult status; + m_mockChannel->GetStatus(&status); + if (!GetServerStateParser().LastCommandSuccessful() && + NS_SUCCEEDED(status)) + status = NS_MSG_ERROR_IMAP_COMMAND_FAILED; + rv = m_channelListener->OnStopRequest(m_mockChannel, status); + } + } + bool suspendUrl = false; + m_runningUrl->GetMoreHeadersToDownload(&suspendUrl); + if (mailnewsurl && m_imapMailFolderSink) { + rv = GetConnectionStatus(); + // There are error conditions to check even if the connection is OK. + if (NS_SUCCEEDED(rv)) { + if (logonFailed) { + rv = NS_ERROR_FAILURE; + } else if (GetServerStateParser().CommandFailed()) { + rv = NS_MSG_ERROR_IMAP_COMMAND_FAILED; + } + } + if (NS_FAILED(rv)) { + MOZ_LOG( + IMAP, LogLevel::Debug, + ("URL failed with code 0x%" PRIx32 " (%s)", static_cast<uint32_t>(rv), + mailnewsurl->GetSpecOrDefault().get())); + // If discovery URL fails, clear the in-progress flag. + if (m_imapAction == nsIImapUrl::nsImapDiscoverAllBoxesUrl) { + m_hostSessionList->SetDiscoveryForHostInProgress(GetImapServerKey(), + false); + } + } + // Inform any nsIUrlListeners that the URL has finished. This will invoke + // nsIUrlListener.onStopRunningUrl(). + m_imapMailFolderSink->SetUrlState(this, mailnewsurl, false, suspendUrl, rv); + + // Doom the cache entry if shutting down or thread is terminated. + if (NS_FAILED(rv) && DeathSignalReceived() && m_mockChannel) { + MOZ_LOG(IMAPCache, LogLevel::Debug, + ("ProcessCurrentURL(): Call DoomCacheEntry()")); + DoomCacheEntry(mailnewsurl); + } + } else { + // That's seen at times in debug sessions. + NS_WARNING("missing url or sink"); + } + + // disable timeouts before caching connection. + if (m_transport) + m_transport->SetTimeout(nsISocketTransport::TIMEOUT_READ_WRITE, + PR_UINT32_MAX); + + SetFlag(IMAP_CLEAN_UP_URL_STATE); + + nsCOMPtr<nsISupports> copyState; + if (m_runningUrl) m_runningUrl->GetCopyState(getter_AddRefs(copyState)); + // this is so hokey...we MUST clear any local references to the url + // BEFORE calling ReleaseUrlState + mailnewsurl = nullptr; + + if (suspendUrl) m_imapServerSink->SuspendUrl(m_runningUrl); + // save the imap folder sink since we need it to do the CopyNextStreamMessage + RefPtr<ImapMailFolderSinkProxy> imapMailFolderSink = m_imapMailFolderSink; + // release the url as we are done with it... + ReleaseUrlState(false); + ResetProgressInfo(); + + ClearFlag(IMAP_CLEAN_UP_URL_STATE); + + if (imapMailFolderSink) { + if (copyState) { + rv = imapMailFolderSink->CopyNextStreamMessage( + GetServerStateParser().LastCommandSuccessful() && + NS_SUCCEEDED(GetConnectionStatus()), + copyState); + if (NS_FAILED(rv)) + MOZ_LOG(IMAP, LogLevel::Info, + ("CopyNextStreamMessage failed: %" PRIx32, + static_cast<uint32_t>(rv))); + + NS_ReleaseOnMainThread("nsImapProtocol, copyState", copyState.forget()); + } + // we might need this to stick around for IDLE support + m_imapMailFolderSink = imapMailFolderSink; + imapMailFolderSink = nullptr; + } else + MOZ_LOG(IMAP, LogLevel::Info, ("null imapMailFolderSink")); + + // now try queued urls, now that we've released this connection. + if (m_imapServerSink) { + if (NS_SUCCEEDED(GetConnectionStatus())) + rv = m_imapServerSink->LoadNextQueuedUrl(this, &anotherUrlRun); + else // if we don't do this, they'll just sit and spin until + // we run some other url on this server. + { + Log("ProcessCurrentURL", nullptr, "aborting queued urls"); + rv = m_imapServerSink->AbortQueuedUrls(); + } + } + + // if we didn't run another url, release the server sink to + // cut circular refs. + if (!anotherUrlRun) m_imapServerSink = nullptr; + + if (NS_FAILED(GetConnectionStatus()) || !GetServerStateParser().Connected() || + GetServerStateParser().SyntaxError()) { + if (m_imapServerSink) m_imapServerSink->RemoveServerConnection(this); + + if (!DeathSignalReceived()) { + TellThreadToDie(); + } + } else { + if (m_imapServerSink) { + bool shuttingDown; + m_imapServerSink->GetServerShuttingDown(&shuttingDown); + if (shuttingDown) m_useIdle = false; + } + } + return anotherUrlRun; +} + +bool nsImapProtocol::RetryUrl() { + nsCOMPtr<nsIImapUrl> kungFuGripImapUrl = m_runningUrl; + nsCOMPtr<nsIImapMockChannel> saveMockChannel; + + // the mock channel might be null - that's OK. + if (m_imapServerSink) + (void)m_imapServerSink->PrepareToRetryUrl(kungFuGripImapUrl, + getter_AddRefs(saveMockChannel)); + + ReleaseUrlState(true); + if (m_imapServerSink) { + m_imapServerSink->RemoveServerConnection(this); + m_imapServerSink->RetryUrl(kungFuGripImapUrl, saveMockChannel); + } + + // Hack for Bug 1586494. + // (this is a workaround to try and prevent a specific crash, and + // does nothing clarify the threading mess!) + // RetryUrl() is only ever called from the imap thread. + // Mockchannel dtor insists upon being run on the main thread. + // So make sure we don't accidentally cause the mockchannel to die right now. + if (saveMockChannel) { + NS_ReleaseOnMainThread("nsImapProtocol::RetryUrl", + saveMockChannel.forget()); + } + + return (m_imapServerSink != nullptr); // we're running a url (the same url) +} + +// ignoreBadAndNOResponses --> don't throw a error dialog if this command +// results in a NO or Bad response from the server..in other words the command +// is "exploratory" and we don't really care if it succeeds or fails. +void nsImapProtocol::ParseIMAPandCheckForNewMail( + const char* commandString, bool aIgnoreBadAndNOResponses) { + if (commandString) + GetServerStateParser().ParseIMAPServerResponse(commandString, + aIgnoreBadAndNOResponses); + else + GetServerStateParser().ParseIMAPServerResponse(m_currentCommand.get(), + aIgnoreBadAndNOResponses); + // **** fix me for new mail biff state ***** +} + +///////////////////////////////////////////////////////////////////////////////////////////// +// End of nsIStreamListenerSupport +////////////////////////////////////////////////////////////////////////////////////////////// + +NS_IMETHODIMP +nsImapProtocol::GetRunningUrl(nsIURI** result) { + if (result && m_runningUrl) + return m_runningUrl->QueryInterface(NS_GET_IID(nsIURI), (void**)result); + return NS_ERROR_NULL_POINTER; +} + +NS_IMETHODIMP nsImapProtocol::GetRunningImapURL(nsIImapUrl** aImapUrl) { + if (aImapUrl && m_runningUrl) + return m_runningUrl->QueryInterface(NS_GET_IID(nsIImapUrl), + (void**)aImapUrl); + return NS_ERROR_NULL_POINTER; +} + +/* + * Writes the data contained in dataBuffer into the current output stream. It + * also informs the transport layer that this data is now available for + * transmission. Returns a positive number for success, 0 for failure (not all + * the bytes were written to the stream, etc). We need to make another pass + * through this file to install an error system (mscott) + */ + +nsresult nsImapProtocol::SendData(const char* dataBuffer, + bool aSuppressLogging) { + nsresult rv = NS_ERROR_NULL_POINTER; + + if (!m_transport) { + Log("SendData", nullptr, "clearing IMAP_CONNECTION_IS_OPEN"); + // the connection died unexpectedly! so clear the open connection flag + ClearFlag(IMAP_CONNECTION_IS_OPEN); + TellThreadToDie(); + SetConnectionStatus(NS_ERROR_FAILURE); + return NS_ERROR_FAILURE; + } + + if (dataBuffer && m_outputStream) { + m_currentCommand = dataBuffer; + if (!aSuppressLogging) + Log("SendData", nullptr, dataBuffer); + else + Log("SendData", nullptr, + "Logging suppressed for this command (it probably contained " + "authentication information)"); + + { + // don't allow someone to close the stream/transport out from under us + // this can happen when the ui thread calls TellThreadToDie. + PR_CEnterMonitor(this); + uint32_t n; + if (m_outputStream) + rv = m_outputStream->Write(dataBuffer, PL_strlen(dataBuffer), &n); + PR_CExitMonitor(this); + } + if (NS_FAILED(rv)) { + Log("SendData", nullptr, "clearing IMAP_CONNECTION_IS_OPEN"); + // the connection died unexpectedly! so clear the open connection flag + ClearFlag(IMAP_CONNECTION_IS_OPEN); + TellThreadToDie(); + SetConnectionStatus(rv); + if (m_runningUrl && !m_retryUrlOnError) { + bool alreadyRerunningUrl; + m_runningUrl->GetRerunningUrl(&alreadyRerunningUrl); + if (!alreadyRerunningUrl) { + m_runningUrl->SetRerunningUrl(true); + m_retryUrlOnError = true; + } + } + } + } + + return rv; +} + +///////////////////////////////////////////////////////////////////////////////////////////// +// Begin protocol state machine functions... +////////////////////////////////////////////////////////////////////////////////////////////// + +// ProcessProtocolState - we override this only so we'll link - it should never +// get called. + +nsresult nsImapProtocol::ProcessProtocolState(nsIURI* url, + nsIInputStream* inputStream, + uint64_t sourceOffset, + uint32_t length) { + return NS_OK; +} + +class UrlListenerNotifierEvent : public mozilla::Runnable { + public: + UrlListenerNotifierEvent(nsIMsgMailNewsUrl* aUrl, nsIImapProtocol* aProtocol) + : mozilla::Runnable("UrlListenerNotifierEvent"), + mUrl(aUrl), + mProtocol(aProtocol) {} + + NS_IMETHOD Run() { + if (mUrl) { + nsCOMPtr<nsIMsgFolder> folder; + mUrl->GetFolder(getter_AddRefs(folder)); + NS_ENSURE_TRUE(folder, NS_OK); + nsCOMPtr<nsIImapMailFolderSink> folderSink(do_QueryInterface(folder)); + // This causes the url listener to get OnStart and Stop notifications. + folderSink->SetUrlState(mProtocol, mUrl, true, false, NS_OK); + folderSink->SetUrlState(mProtocol, mUrl, false, false, NS_OK); + } + return NS_OK; + } + + private: + nsCOMPtr<nsIMsgMailNewsUrl> mUrl; + nsCOMPtr<nsIImapProtocol> mProtocol; +}; + +bool nsImapProtocol::TryToRunUrlLocally(nsIURI* aURL, nsISupports* aConsumer) { + nsresult rv; + nsCOMPtr<nsIImapUrl> imapUrl(do_QueryInterface(aURL, &rv)); + NS_ENSURE_SUCCESS(rv, false); + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(aURL); + nsCString messageIdString; + imapUrl->GetListOfMessageIds(messageIdString); + bool useLocalCache = false; + if (!messageIdString.IsEmpty() && + !HandlingMultipleMessages(messageIdString)) { + nsImapAction action; + imapUrl->GetImapAction(&action); + nsCOMPtr<nsIMsgFolder> folder; + mailnewsUrl->GetFolder(getter_AddRefs(folder)); + NS_ENSURE_TRUE(folder, false); + + folder->HasMsgOffline(strtoul(messageIdString.get(), nullptr, 10), + &useLocalCache); + mailnewsUrl->SetMsgIsInLocalCache(useLocalCache); + // We're downloading a single message for offline use, and it's + // already offline. So we shouldn't do anything, but we do + // need to notify the url listener. + if (useLocalCache && action == nsIImapUrl::nsImapMsgDownloadForOffline) { + nsCOMPtr<nsIRunnable> event = + new UrlListenerNotifierEvent(mailnewsUrl, this); + // Post this as an event because it can lead to re-entrant calls to + // LoadNextQueuedUrl if the listener runs a new url. + if (event) NS_DispatchToCurrentThread(event); + return true; + } + } + if (!useLocalCache) return false; + + nsCOMPtr<nsIImapMockChannel> mockChannel; + imapUrl->GetMockChannel(getter_AddRefs(mockChannel)); + if (!mockChannel) return false; + + nsImapMockChannel* imapChannel = + static_cast<nsImapMockChannel*>(mockChannel.get()); + if (!imapChannel) return false; + + nsCOMPtr<nsILoadGroup> loadGroup; + imapChannel->GetLoadGroup(getter_AddRefs(loadGroup)); + if (!loadGroup) // if we don't have one, the url will snag one from the msg + // window... + mailnewsUrl->GetLoadGroup(getter_AddRefs(loadGroup)); + + if (loadGroup) + loadGroup->RemoveRequest((nsIRequest*)mockChannel, + nullptr /* context isupports */, NS_OK); + + if (imapChannel->ReadFromLocalCache()) { + (void)imapChannel->NotifyStartEndReadFromCache(true); + return true; + } + return false; +} + +// LoadImapUrl takes a url, initializes all of our url specific data by calling +// SetupUrl. Finally, we signal the url to run monitor to let the imap main +// thread loop process the current url (it is waiting on this monitor). There +// is a contract that the imap thread has already been started before we +// attempt to load a url... +// LoadImapUrl() is called by nsImapIncomingServer to run a queued url on a free +// connection. +NS_IMETHODIMP nsImapProtocol::LoadImapUrl(nsIURI* aURL, + nsISupports* aConsumer) { + nsresult rv = NS_ERROR_FAILURE; + if (aURL) { +#ifdef DEBUG_bienvenu + printf("loading url %s\n", aURL->GetSpecOrDefault().get()); +#endif + // We might be able to fulfill the request locally (e.g. fetching a message + // which is already stored offline). + if (TryToRunUrlLocally(aURL, aConsumer)) return NS_OK; + m_urlInProgress = true; + m_imapMailFolderSink = nullptr; + rv = SetupWithUrl(aURL, aConsumer); + m_lastActiveTime = PR_Now(); + } + return rv; +} + +nsresult nsImapProtocol::LoadImapUrlInternal() { + nsresult rv = NS_ERROR_FAILURE; + + if (m_transport && m_mockChannel) { + m_transport->SetTimeout(nsISocketTransport::TIMEOUT_CONNECT, + gResponseTimeout + 60); + int32_t readWriteTimeout = gResponseTimeout; + if (m_runningUrl) { + m_runningUrl->GetImapAction(&m_imapAction); + // This is a silly hack, but the default of 100 seconds is typically way + // too long for things like APPEND, which should come back immediately. + // However, for large messages on some servers the final append response + // time can be longer. So now it is one-fifth of the configured + // `mailnews.tcptimeout' which defaults to 20 seconds. + if (m_imapAction == nsIImapUrl::nsImapAppendMsgFromFile || + m_imapAction == nsIImapUrl::nsImapAppendDraftFromFile) { + readWriteTimeout = gAppendTimeout; + } else if (m_imapAction == nsIImapUrl::nsImapOnlineMove || + m_imapAction == nsIImapUrl::nsImapOnlineCopy) { + nsCString messageIdString; + m_runningUrl->GetListOfMessageIds(messageIdString); + uint32_t copyCount = CountMessagesInIdString(messageIdString.get()); + // If we're move/copying a large number of messages, + // which should be rare, increase the timeout based on number + // of messages. 40 messages per second should be sufficiently slow. + if (copyCount > 2400) // 40 * 60, 60 is default read write timeout + readWriteTimeout = + std::max(readWriteTimeout, (int32_t)copyCount / 40); + } + } + m_transport->SetTimeout(nsISocketTransport::TIMEOUT_READ_WRITE, + readWriteTimeout); + // Set the security info for the mock channel to be the security info for + // our underlying transport. + if (m_securityInfo) { + m_mockChannel->SetSecurityInfo(m_securityInfo); + } + + SetSecurityCallbacksFromChannel(m_transport, m_mockChannel); + + nsCOMPtr<nsITransportEventSink> sinkMC = do_QueryInterface(m_mockChannel); + if (sinkMC) { + nsCOMPtr<nsIThread> thread = do_GetMainThread(); + RefPtr<nsImapTransportEventSink> sink = new nsImapTransportEventSink; + rv = net_NewTransportEventSinkProxy(getter_AddRefs(sink->m_proxy), sinkMC, + thread); + NS_ENSURE_SUCCESS(rv, rv); + m_transport->SetEventSink(sink, nullptr); + } + + // And if we have a cache2 entry that we are saving the message to, set the + // security info on it too. + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(m_runningUrl); + if (mailnewsUrl && m_securityInfo) { + nsCOMPtr<nsICacheEntry> cacheEntry; + mailnewsUrl->GetMemCacheEntry(getter_AddRefs(cacheEntry)); + if (cacheEntry) { + cacheEntry->SetSecurityInfo(m_securityInfo); + } + } + } + + rv = SetupSinkProxy(); // generate proxies for all of the event sinks in the + // url + if (NS_FAILED(rv)) // URL can be invalid. + return rv; + + if (m_transport && m_runningUrl) { + nsImapAction imapAction; + m_runningUrl->GetImapAction(&imapAction); + // if we're shutting down, and not running the kinds of urls we run at + // shutdown, then this should fail because running urls during + // shutdown will very likely fail and potentially hang. + nsCOMPtr<nsIMsgAccountManager> accountMgr = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + bool shuttingDown = false; + (void)accountMgr->GetShutdownInProgress(&shuttingDown); + if (shuttingDown && imapAction != nsIImapUrl::nsImapExpungeFolder && + imapAction != nsIImapUrl::nsImapDeleteAllMsgs && + imapAction != nsIImapUrl::nsImapDeleteFolder) + return NS_ERROR_FAILURE; + + // if we're running a select or delete all, do a noop first. + // this should really be in the connection cache code when we know + // we're pulling out a selected state connection, but maybe we + // can get away with this. + m_needNoop = (imapAction == nsIImapUrl::nsImapSelectFolder || + imapAction == nsIImapUrl::nsImapDeleteAllMsgs); + + // We now have a url to run so signal the monitor for url ready to be + // processed... + ReentrantMonitorAutoEnter urlReadyMon(m_urlReadyToRunMonitor); + m_nextUrlReadyToRun = true; + urlReadyMon.Notify(); + + } // if we have an imap url and a transport + else { + NS_ASSERTION(false, "missing channel or running url"); + } + + return rv; +} + +NS_IMETHODIMP nsImapProtocol::IsBusy(bool* aIsConnectionBusy, + bool* isInboxConnection) { + if (!aIsConnectionBusy || !isInboxConnection) return NS_ERROR_NULL_POINTER; + nsresult rv = NS_OK; + *aIsConnectionBusy = false; + *isInboxConnection = false; + if (!m_transport) { + // this connection might not be fully set up yet. + rv = NS_ERROR_FAILURE; + } else { + if (m_urlInProgress) // do we have a url? That means we're working on it... + *aIsConnectionBusy = true; + + if (GetServerStateParser().GetIMAPstate() == + nsImapServerResponseParser::kFolderSelected && + GetServerStateParser().GetSelectedMailboxName() && + PL_strcasecmp(GetServerStateParser().GetSelectedMailboxName(), + "Inbox") == 0) + *isInboxConnection = true; + } + return rv; +} + +#define IS_SUBSCRIPTION_RELATED_ACTION(action) \ + (action == nsIImapUrl::nsImapSubscribe || \ + action == nsIImapUrl::nsImapUnsubscribe || \ + action == nsIImapUrl::nsImapDiscoverAllBoxesUrl || \ + action == nsIImapUrl::nsImapListFolder) + +// canRunUrl means the connection is not busy, and is in the selected state +// for the desired folder (or authenticated). +// has to wait means it's in the right selected state, but busy. +NS_IMETHODIMP nsImapProtocol::CanHandleUrl(nsIImapUrl* aImapUrl, + bool* aCanRunUrl, bool* hasToWait) { + if (!aCanRunUrl || !hasToWait || !aImapUrl) return NS_ERROR_NULL_POINTER; + nsresult rv = NS_OK; + MutexAutoLock mon(mLock); + + *aCanRunUrl = false; // assume guilty until proven otherwise... + *hasToWait = false; + + if (DeathSignalReceived()) return NS_ERROR_FAILURE; + + bool isBusy = false; + bool isInboxConnection = false; + + if (!m_transport) { + // this connection might not be fully set up yet. + return NS_ERROR_FAILURE; + } + IsBusy(&isBusy, &isInboxConnection); + bool inSelectedState = GetServerStateParser().GetIMAPstate() == + nsImapServerResponseParser::kFolderSelected; + + nsAutoCString curSelectedUrlFolderName; + nsAutoCString pendingUrlFolderName; + if (inSelectedState) + curSelectedUrlFolderName = GetServerStateParser().GetSelectedMailboxName(); + + if (isBusy) { + nsImapState curUrlImapState; + NS_ASSERTION(m_runningUrl, "isBusy, but no running url."); + if (m_runningUrl) { + m_runningUrl->GetRequiredImapState(&curUrlImapState); + if (curUrlImapState == nsIImapUrl::nsImapSelectedState) { + char* folderName = GetFolderPathString(); + if (!curSelectedUrlFolderName.Equals(folderName)) + pendingUrlFolderName.Assign(folderName); + inSelectedState = true; + PR_Free(folderName); + } + } + } + + nsImapState imapState; + nsImapAction actionForProposedUrl; + aImapUrl->GetImapAction(&actionForProposedUrl); + aImapUrl->GetRequiredImapState(&imapState); + + // OK, this is a bit of a hack - we're going to pretend that + // these types of urls requires a selected state connection on + // the folder in question. This isn't technically true, + // but we would much rather use that connection for several reasons, + // one is that some UW servers require us to use that connection + // the other is that we don't want to leave a connection dangling in + // the selected state for the deleted folder. + // If we don't find a connection in that selected state, + // we'll fall back to the first free connection. + bool isSelectedStateUrl = + imapState == nsIImapUrl::nsImapSelectedState || + actionForProposedUrl == nsIImapUrl::nsImapDeleteFolder || + actionForProposedUrl == nsIImapUrl::nsImapRenameFolder || + actionForProposedUrl == nsIImapUrl::nsImapMoveFolderHierarchy || + actionForProposedUrl == nsIImapUrl::nsImapAppendDraftFromFile || + actionForProposedUrl == nsIImapUrl::nsImapAppendMsgFromFile || + actionForProposedUrl == nsIImapUrl::nsImapFolderStatus; + + nsCOMPtr<nsIMsgMailNewsUrl> msgUrl = do_QueryInterface(aImapUrl); + nsCOMPtr<nsIMsgIncomingServer> server; + rv = msgUrl->GetServer(getter_AddRefs(server)); + if (NS_SUCCEEDED(rv)) { + // compare host/user between url and connection. + nsCString urlHostName; + nsCString urlUserName; + rv = server->GetHostName(urlHostName); + NS_ENSURE_SUCCESS(rv, rv); + rv = server->GetUsername(urlUserName); + NS_ENSURE_SUCCESS(rv, rv); + + if ((GetImapHostName().IsEmpty() || + urlHostName.Equals(GetImapHostName(), + nsCaseInsensitiveCStringComparator)) && + (GetImapUserName().IsEmpty() || + urlUserName.Equals(GetImapUserName(), + nsCaseInsensitiveCStringComparator))) { + if (isSelectedStateUrl) { + if (inSelectedState) { + // *** jt - in selected state can only run url with + // matching foldername + char* folderNameForProposedUrl = nullptr; + rv = aImapUrl->CreateServerSourceFolderPathString( + &folderNameForProposedUrl); + if (NS_SUCCEEDED(rv) && folderNameForProposedUrl) { + bool isInbox = + PL_strcasecmp("Inbox", folderNameForProposedUrl) == 0; + if (!curSelectedUrlFolderName.IsEmpty() || + !pendingUrlFolderName.IsEmpty()) { + bool matched = isInbox + ? PL_strcasecmp(curSelectedUrlFolderName.get(), + folderNameForProposedUrl) == 0 + : PL_strcmp(curSelectedUrlFolderName.get(), + folderNameForProposedUrl) == 0; + if (!matched && !pendingUrlFolderName.IsEmpty()) { + matched = isInbox ? PL_strcasecmp(pendingUrlFolderName.get(), + folderNameForProposedUrl) == 0 + : PL_strcmp(pendingUrlFolderName.get(), + folderNameForProposedUrl) == 0; + } + if (matched) { + if (isBusy) + *hasToWait = true; + else + *aCanRunUrl = true; + } + } + } + MOZ_LOG(IMAP, LogLevel::Debug, + ("proposed url = %s folder for connection %s has To Wait = " + "%s can run = %s", + folderNameForProposedUrl, curSelectedUrlFolderName.get(), + (*hasToWait) ? "true" : "false", + (*aCanRunUrl) ? "true" : "false")); + PR_FREEIF(folderNameForProposedUrl); + } + } else // *** jt - an authenticated state url can be run in either + // authenticated or selected state + { + nsImapAction actionForRunningUrl; + + // If proposed url is subscription related, and we are currently running + // a subscription url, then we want to queue the proposed url after the + // current url. Otherwise, we can run this url if we're not busy. If we + // never find a running subscription-related url, the caller will just + // use whatever free connection it can find, which is what we want. + if (IS_SUBSCRIPTION_RELATED_ACTION(actionForProposedUrl)) { + if (isBusy && m_runningUrl) { + m_runningUrl->GetImapAction(&actionForRunningUrl); + if (IS_SUBSCRIPTION_RELATED_ACTION(actionForRunningUrl)) { + *aCanRunUrl = false; + *hasToWait = true; + } + } + } else { + if (!isBusy) *aCanRunUrl = true; + } + } + } + } + return rv; +} + +// Command tag handling stuff. +// Zero tag number indicates never used so set it to an initial random number +// between 1 and 100. Otherwise just increment the uint32_t value unless it +// rolls to zero then set it to 1. Then convert the tag number to a string for +// use in IMAP commands. +void nsImapProtocol::IncrementCommandTagNumber() { + if (m_currentServerCommandTagNumber == 0) { + srand((unsigned)m_lastCheckTime); + m_currentServerCommandTagNumber = 1 + (rand() % 100); + } else if (++m_currentServerCommandTagNumber == 0) { + m_currentServerCommandTagNumber = 1; + } + sprintf(m_currentServerCommandTag, "%u", m_currentServerCommandTagNumber); +} + +const char* nsImapProtocol::GetServerCommandTag() { + return m_currentServerCommandTag; +} + +/** + * ProcessSelectedStateURL() is a helper for ProcessCurrentURL(). It handles + * running URLs which require the connection to be in the selected state. + * It will issue SELECT commands if needed to make sure the correct mailbox + * is selected. + */ +void nsImapProtocol::ProcessSelectedStateURL() { + nsCString mailboxName; + bool bMessageIdsAreUids = true; + bool moreHeadersToDownload; + imapMessageFlagsType msgFlags = 0; + nsCString urlHost; + + // this can't fail, can it? + nsresult res; + res = m_runningUrl->GetImapAction(&m_imapAction); + m_runningUrl->MessageIdsAreUids(&bMessageIdsAreUids); + m_runningUrl->GetMsgFlags(&msgFlags); + m_runningUrl->GetMoreHeadersToDownload(&moreHeadersToDownload); + + res = CreateServerSourceFolderPathString(getter_Copies(mailboxName)); + if (NS_FAILED(res)) + Log("ProcessSelectedStateURL", nullptr, + "error getting source folder path string"); + + if (NS_SUCCEEDED(res) && !DeathSignalReceived()) { + bool selectIssued = false; + if (GetServerStateParser().GetIMAPstate() == + nsImapServerResponseParser::kFolderSelected) { + if (GetServerStateParser().GetSelectedMailboxName() && + PL_strcmp(GetServerStateParser().GetSelectedMailboxName(), + mailboxName.get())) { // we are selected in another folder + if (m_closeNeededBeforeSelect) ImapClose(); + if (GetServerStateParser().LastCommandSuccessful()) { + selectIssued = true; + SelectMailbox(mailboxName.get()); + } + } else if (!GetServerStateParser() + .GetSelectedMailboxName()) { // why are we in the + // selected state with no + // box name? + SelectMailbox(mailboxName.get()); + selectIssued = true; + } else if (moreHeadersToDownload && + m_imapMailFolderSink) // we need to fetch older headers + { + nsTArray<nsMsgKey> msgIdList; + bool more; + m_imapMailFolderSink->GetMsgHdrsToDownload( + &more, &m_progressExpectedNumber, msgIdList); + if (msgIdList.Length() > 0) { + FolderHeaderDump(msgIdList.Elements(), msgIdList.Length()); + m_runningUrl->SetMoreHeadersToDownload(more); + // We're going to be re-running this url. + if (more) m_runningUrl->SetRerunningUrl(true); + } + HeaderFetchCompleted(); + } else { + // get new message counts, if any, from server + if (m_needNoop) { + // For some IMAP servers, to detect new email we must send imap + // SELECT even if already SELECTed on the same mailbox. + if (m_forceSelect) { + SelectMailbox(mailboxName.get()); + selectIssued = true; + } + + m_noopCount++; + if ((gPromoteNoopToCheckCount > 0 && + (m_noopCount % gPromoteNoopToCheckCount) == 0) || + CheckNeeded()) + Check(); + else + Noop(); // I think this is needed when we're using a cached + // connection + m_needNoop = false; + } + } + } else { + // go to selected state + SelectMailbox(mailboxName.get()); + selectIssued = GetServerStateParser().LastCommandSuccessful(); + } + + if (selectIssued) RefreshACLForFolderIfNecessary(mailboxName.get()); + + bool uidValidityOk = true; + if (GetServerStateParser().LastCommandSuccessful() && selectIssued && + (m_imapAction != nsIImapUrl::nsImapSelectFolder) && + (m_imapAction != nsIImapUrl::nsImapLiteSelectFolder)) { + // error on the side of caution, if the fe event fails to set + // uidStruct->returnValidity, then assume that UIDVALIDITY did not roll. + // This is a common case event for attachments that are fetched within a + // browser context. + if (!DeathSignalReceived()) + uidValidityOk = m_uidValidity == kUidUnknown || + m_uidValidity == GetServerStateParser().FolderUID(); + } + + if (!uidValidityOk) + Log("ProcessSelectedStateURL", nullptr, "uid validity not ok"); + if (GetServerStateParser().LastCommandSuccessful() && + !DeathSignalReceived() && + (uidValidityOk || m_imapAction == nsIImapUrl::nsImapDeleteAllMsgs)) { + if (GetServerStateParser().CurrentFolderReadOnly()) { + Log("ProcessSelectedStateURL", nullptr, "current folder read only"); + if (m_imapAction == nsIImapUrl::nsImapAddMsgFlags || + m_imapAction == nsIImapUrl::nsImapSubtractMsgFlags) { + bool canChangeFlag = false; + if (GetServerStateParser().ServerHasACLCapability() && + m_imapMailFolderSink) { + uint32_t aclFlags = 0; + + if (NS_SUCCEEDED(m_imapMailFolderSink->GetAclFlags(&aclFlags)) && + aclFlags != 0) // make sure we have some acl flags + canChangeFlag = ((msgFlags & kImapMsgSeenFlag) && + (aclFlags & IMAP_ACL_STORE_SEEN_FLAG)); + } else + canChangeFlag = (GetServerStateParser().SettablePermanentFlags() & + msgFlags) == msgFlags; + if (!canChangeFlag) return; + } + if (m_imapAction == nsIImapUrl::nsImapExpungeFolder || + m_imapAction == nsIImapUrl::nsImapDeleteMsg || + m_imapAction == nsIImapUrl::nsImapDeleteAllMsgs) + return; + } + switch (m_imapAction) { + case nsIImapUrl::nsImapLiteSelectFolder: + if (GetServerStateParser().LastCommandSuccessful() && + m_imapMailFolderSink && !moreHeadersToDownload) { + m_imapMailFolderSink->SetUidValidity( + GetServerStateParser().FolderUID()); + ProcessMailboxUpdate(false); // handle uidvalidity change + } + break; + case nsIImapUrl::nsImapSaveMessageToDisk: + case nsIImapUrl::nsImapMsgFetch: + case nsIImapUrl::nsImapMsgFetchPeek: + case nsIImapUrl::nsImapMsgDownloadForOffline: + case nsIImapUrl::nsImapMsgPreview: { + nsCString messageIdString; + m_runningUrl->GetListOfMessageIds(messageIdString); + // we don't want to send the flags back in a group + if (HandlingMultipleMessages(messageIdString) || + m_imapAction == nsIImapUrl::nsImapMsgDownloadForOffline || + m_imapAction == nsIImapUrl::nsImapMsgPreview) { + // multiple messages, fetch them all + SetProgressString(IMAP_MESSAGES_STRING_INDEX); + + m_progressCurrentNumber[m_stringIndex] = 0; + m_progressExpectedNumber = + CountMessagesInIdString(messageIdString.get()); + + FetchMessage(messageIdString, + (m_imapAction == nsIImapUrl::nsImapMsgPreview) + ? kBodyStart + : kEveryThingRFC822Peek); + if (m_imapAction == nsIImapUrl::nsImapMsgPreview) + HeaderFetchCompleted(); + SetProgressString(IMAP_EMPTY_STRING_INDEX); + } else { + // A single message ID + nsIMAPeFetchFields whatToFetch = kEveryThingRFC822; + if (m_imapAction == nsIImapUrl::nsImapMsgFetchPeek) + whatToFetch = kEveryThingRFC822Peek; + + // Note: Should no longer fetch a specific imap section (part). + // First, let's see if we're requesting a specific MIME part. + char* imappart = nullptr; + m_runningUrl->GetImapPartToFetch(&imappart); + MOZ_ASSERT(!imappart, "no longer fetching imap section/imappart"); + // downloading a single message: try to do it by bodystructure, + // and/or do it by chunks + // Note: No longer doing bodystructure. + uint32_t messageSize = GetMessageSize(messageIdString); + + // The "wontFit" and cache parameter calculations (customLimit, + // realLimit) are only for debug information logging below. + if (MOZ_LOG_TEST(IMAPCache, LogLevel::Debug)) { + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl = + do_QueryInterface(m_runningUrl); + if (mailnewsurl) { + bool wontFit = net::CacheObserver::EntryIsTooBig( + messageSize, gUseDiskCache2); + int64_t customLimit; + int64_t realLimit; + if (gUseDiskCache2) { + customLimit = net::CacheObserver::MaxDiskEntrySize(); + realLimit = net::CacheObserver::DiskCacheCapacity(); + } else { + customLimit = net::CacheObserver::MaxMemoryEntrySize(); + realLimit = net::CacheObserver::MemoryCacheCapacity(); + } + if (!(customLimit & (int64_t)0x80000000)) + customLimit <<= 10; // multiply by 1024 to get num bytes + else + customLimit = (int32_t)customLimit; // make it negative + realLimit <<= (10 - 3); // 1/8th capacity, num bytes. + if (customLimit > -1 && customLimit < realLimit) + realLimit = customLimit; + MOZ_LOG(IMAPCache, LogLevel::Debug, + ("%s: customLimit=%" PRId64 ", realLimit=%" PRId64, + __func__, customLimit, realLimit)); + MOZ_LOG(IMAPCache, LogLevel::Debug, + ("%s: URL=%s, messageSize=%d, cache too small=%d(bool)", + __func__, mailnewsurl->GetSpecOrDefault().get(), + messageSize, wontFit)); + } + } + // Note again: No longer doing bodystructure. + // Fetch the whole thing, and try to do it in chunks. + MOZ_LOG( + IMAPCache, LogLevel::Debug, + ("%s: Fetch entire message with FetchTryChunking", __func__)); + FetchTryChunking(messageIdString, whatToFetch, bMessageIdsAreUids, + NULL, messageSize, true); + // If fetch was not a peek, ensure that the message displays as + // read (not bold) in case the server fails to mark the message + // as SEEN. + if (GetServerStateParser().LastCommandSuccessful() && + m_imapAction != nsIImapUrl::nsImapMsgFetchPeek) { + uint32_t uid = strtoul(messageIdString.get(), nullptr, 10); + int32_t index; + bool foundIt; + imapMessageFlagsType flags = + m_flagState->GetMessageFlagsFromUID(uid, &foundIt, &index); + if (foundIt) { + flags |= kImapMsgSeenFlag; + m_flagState->SetMessageFlags(index, flags); + } + } + } + } break; + case nsIImapUrl::nsImapExpungeFolder: + Expunge(); + // note fall through to next cases. + [[fallthrough]]; + case nsIImapUrl::nsImapSelectFolder: + case nsIImapUrl::nsImapSelectNoopFolder: + if (!moreHeadersToDownload) ProcessMailboxUpdate(true); + break; + case nsIImapUrl::nsImapMsgHeader: { + nsCString messageIds; + m_runningUrl->GetListOfMessageIds(messageIds); + + FetchMessage(messageIds, kHeadersRFC822andUid); + // if we explicitly ask for headers, as opposed to getting them as a + // result of selecting the folder, or biff, send the + // headerFetchCompleted notification to flush out the header cache. + HeaderFetchCompleted(); + } break; + case nsIImapUrl::nsImapSearch: { + nsAutoCString searchCriteriaString; + m_runningUrl->CreateSearchCriteriaString( + getter_Copies(searchCriteriaString)); + Search(searchCriteriaString.get(), bMessageIdsAreUids); + // drop the results on the floor for now + } break; + case nsIImapUrl::nsImapUserDefinedMsgCommand: { + nsCString messageIdString; + nsCString command; + + m_runningUrl->GetCommand(command); + m_runningUrl->GetListOfMessageIds(messageIdString); + IssueUserDefinedMsgCommand(command.get(), messageIdString.get()); + } break; + case nsIImapUrl::nsImapUserDefinedFetchAttribute: { + nsCString messageIdString; + nsCString attribute; + + m_runningUrl->GetCustomAttributeToFetch(attribute); + m_runningUrl->GetListOfMessageIds(messageIdString); + FetchMsgAttribute(messageIdString, attribute); + } break; + case nsIImapUrl::nsImapMsgStoreCustomKeywords: { + // If the server doesn't support user defined flags, don't try to + // define/set new ones. But if this is an attempt by TB to set or + // reset flags "Junk" or "NonJunk", change "Junk" or "NonJunk" to + // "$Junk" or "$NotJunk" respectively and store the modified flag + // name if the server doesn't support storing user defined flags + // and the server does allow storing the almost-standard flag names + // "$Junk" and "$NotJunk". Yahoo imap server is an example of this. + uint16_t userFlags = 0; + GetSupportedUserFlags(&userFlags); + bool userDefinedSettable = userFlags & kImapMsgSupportUserFlag; + bool stdJunkOk = GetServerStateParser().IsStdJunkNotJunkUseOk(); + + nsCString messageIdString; + nsCString addFlags; + nsCString subtractFlags; + + m_runningUrl->GetListOfMessageIds(messageIdString); + m_runningUrl->GetCustomAddFlags(addFlags); + m_runningUrl->GetCustomSubtractFlags(subtractFlags); + if (!addFlags.IsEmpty()) { + if (!userDefinedSettable) { + if (stdJunkOk) { + if (addFlags.EqualsIgnoreCase("junk")) + addFlags = "$Junk"; + else if (addFlags.EqualsIgnoreCase("nonjunk")) + addFlags = "$NotJunk"; + else + break; + } else + break; + } + nsAutoCString storeString("+FLAGS ("); + storeString.Append(addFlags); + storeString.Append(')'); + Store(messageIdString, storeString.get(), true); + } + if (!subtractFlags.IsEmpty()) { + if (!userDefinedSettable) { + if (stdJunkOk) { + if (subtractFlags.EqualsIgnoreCase("junk")) + subtractFlags = "$Junk"; + else if (subtractFlags.EqualsIgnoreCase("nonjunk")) + subtractFlags = "$NotJunk"; + else + break; + } else + break; + } + nsAutoCString storeString("-FLAGS ("); + storeString.Append(subtractFlags); + storeString.Append(')'); + Store(messageIdString, storeString.get(), true); + } + } break; + case nsIImapUrl::nsImapDeleteMsg: { + nsCString messageIdString; + m_runningUrl->GetListOfMessageIds(messageIdString); + + ProgressEventFunctionUsingName( + HandlingMultipleMessages(messageIdString) + ? "imapDeletingMessages" + : "imapDeletingMessage"); + + Store(messageIdString, "+FLAGS (\\Deleted)", bMessageIdsAreUids); + + if (GetServerStateParser().LastCommandSuccessful()) { + nsCString canonicalName; + const char* selectedMailboxName = + GetServerStateParser().GetSelectedMailboxName(); + if (selectedMailboxName) { + m_runningUrl->AllocateCanonicalPath( + selectedMailboxName, kOnlineHierarchySeparatorUnknown, + getter_Copies(canonicalName)); + } + + if (m_imapMessageSink) + m_imapMessageSink->NotifyMessageDeleted( + canonicalName.get(), false, messageIdString.get()); + // notice we don't wait for this to finish... + } else + HandleMemoryFailure(); + } break; + case nsIImapUrl::nsImapDeleteFolderAndMsgs: + DeleteFolderAndMsgs(mailboxName.get()); + break; + case nsIImapUrl::nsImapDeleteAllMsgs: { + uint32_t numberOfMessages = GetServerStateParser().NumberOfMessages(); + if (numberOfMessages) { + Store("1:*"_ns, "+FLAGS.SILENT (\\Deleted)", + false); // use sequence #'s + + if (GetServerStateParser().LastCommandSuccessful()) + Expunge(); // expunge messages with deleted flag + if (GetServerStateParser().LastCommandSuccessful()) { + nsCString canonicalName; + const char* selectedMailboxName = + GetServerStateParser().GetSelectedMailboxName(); + if (selectedMailboxName) { + m_runningUrl->AllocateCanonicalPath( + selectedMailboxName, kOnlineHierarchySeparatorUnknown, + getter_Copies(canonicalName)); + } + + if (m_imapMessageSink) + m_imapMessageSink->NotifyMessageDeleted(canonicalName.get(), + true, nullptr); + } + } + bool deleteSelf = false; + DeleteSubFolders(mailboxName.get(), deleteSelf); // don't delete self + } break; + case nsIImapUrl::nsImapAppendDraftFromFile: { + OnAppendMsgFromFile(); + } break; + case nsIImapUrl::nsImapAddMsgFlags: { + nsCString messageIdString; + m_runningUrl->GetListOfMessageIds(messageIdString); + + ProcessStoreFlags(messageIdString, bMessageIdsAreUids, msgFlags, + true); + } break; + case nsIImapUrl::nsImapSubtractMsgFlags: { + nsCString messageIdString; + m_runningUrl->GetListOfMessageIds(messageIdString); + + ProcessStoreFlags(messageIdString, bMessageIdsAreUids, msgFlags, + false); + } break; + case nsIImapUrl::nsImapSetMsgFlags: { + nsCString messageIdString; + m_runningUrl->GetListOfMessageIds(messageIdString); + + ProcessStoreFlags(messageIdString, bMessageIdsAreUids, msgFlags, + true); + ProcessStoreFlags(messageIdString, bMessageIdsAreUids, ~msgFlags, + false); + } break; + case nsIImapUrl::nsImapBiff: + PeriodicBiff(); + break; + case nsIImapUrl::nsImapOnlineCopy: + case nsIImapUrl::nsImapOnlineMove: { + nsCString messageIdString; + m_runningUrl->GetListOfMessageIds(messageIdString); + char* destinationMailbox = + OnCreateServerDestinationFolderPathString(); + + if (destinationMailbox) { + if (m_imapAction == nsIImapUrl::nsImapOnlineMove) { + if (HandlingMultipleMessages(messageIdString)) + ProgressEventFunctionUsingNameWithString("imapMovingMessages", + destinationMailbox); + else + ProgressEventFunctionUsingNameWithString("imapMovingMessage", + destinationMailbox); + } else { + if (HandlingMultipleMessages(messageIdString)) + ProgressEventFunctionUsingNameWithString("imapCopyingMessages", + destinationMailbox); + else + ProgressEventFunctionUsingNameWithString("imapCopyingMessage", + destinationMailbox); + } + Copy(messageIdString.get(), destinationMailbox, bMessageIdsAreUids); + PR_FREEIF(destinationMailbox); + ImapOnlineCopyState copyState; + if (DeathSignalReceived()) + copyState = ImapOnlineCopyStateType::kInterruptedState; + else + copyState = GetServerStateParser().LastCommandSuccessful() + ? (ImapOnlineCopyState) + ImapOnlineCopyStateType::kSuccessfulCopy + : (ImapOnlineCopyState) + ImapOnlineCopyStateType::kFailedCopy; + if (m_imapMailFolderSink) + m_imapMailFolderSink->OnlineCopyCompleted(this, copyState); + // Don't mark message 'Deleted' for AOL servers or standard imap + // servers that support MOVE since we already issued an 'xaol-move' + // or 'move' command. + if (GetServerStateParser().LastCommandSuccessful() && + (m_imapAction == nsIImapUrl::nsImapOnlineMove) && + !(GetServerStateParser().ServerIsAOLServer() || + GetServerStateParser().GetCapabilityFlag() & + kHasMoveCapability)) { + // Simulate MOVE for servers that don't support MOVE: do + // COPY-DELETE-EXPUNGE. + Store(messageIdString, "+FLAGS (\\Deleted \\Seen)", + bMessageIdsAreUids); + bool storeSuccessful = + GetServerStateParser().LastCommandSuccessful(); + if (storeSuccessful) { + if (gExpungeAfterDelete) { + // This will expunge all emails marked as deleted in mailbox, + // not just the ones marked as deleted above. + Expunge(); + } else { + // Check if UIDPLUS capable so we can just expunge emails we + // just copied and marked as deleted. This prevents expunging + // emails that other clients may have marked as deleted in the + // mailbox and don't want them to disappear. Only do + // UidExpunge() when user selected delete method is "Move it + // to this folder" or "Remove it immediately", not when the + // delete method is "Just mark it as deleted". + if (!GetShowDeletedMessages() && + (GetServerStateParser().GetCapabilityFlag() & + kUidplusCapability)) { + UidExpunge(messageIdString); + } + } + } + if (m_imapMailFolderSink) { + copyState = storeSuccessful + ? (ImapOnlineCopyState) + ImapOnlineCopyStateType::kSuccessfulDelete + : (ImapOnlineCopyState) + ImapOnlineCopyStateType::kFailedDelete; + m_imapMailFolderSink->OnlineCopyCompleted(this, copyState); + } + } + } else + HandleMemoryFailure(); + } break; + case nsIImapUrl::nsImapOnlineToOfflineCopy: + case nsIImapUrl::nsImapOnlineToOfflineMove: { + nsCString messageIdString; + nsresult rv = m_runningUrl->GetListOfMessageIds(messageIdString); + if (NS_SUCCEEDED(rv)) { + SetProgressString(IMAP_MESSAGES_STRING_INDEX); + m_progressCurrentNumber[m_stringIndex] = 0; + m_progressExpectedNumber = + CountMessagesInIdString(messageIdString.get()); + + FetchMessage(messageIdString, kEveryThingRFC822Peek); + + SetProgressString(IMAP_EMPTY_STRING_INDEX); + if (m_imapMailFolderSink) { + ImapOnlineCopyState copyStatus; + copyStatus = GetServerStateParser().LastCommandSuccessful() + ? ImapOnlineCopyStateType::kSuccessfulCopy + : ImapOnlineCopyStateType::kFailedCopy; + + m_imapMailFolderSink->OnlineCopyCompleted(this, copyStatus); + if (GetServerStateParser().LastCommandSuccessful() && + (m_imapAction == nsIImapUrl::nsImapOnlineToOfflineMove)) { + Store(messageIdString, "+FLAGS (\\Deleted \\Seen)", + bMessageIdsAreUids); + if (GetServerStateParser().LastCommandSuccessful()) { + copyStatus = ImapOnlineCopyStateType::kSuccessfulDelete; + if (gExpungeAfterDelete) Expunge(); + } else + copyStatus = ImapOnlineCopyStateType::kFailedDelete; + + m_imapMailFolderSink->OnlineCopyCompleted(this, copyStatus); + } + } + } else + HandleMemoryFailure(); + } break; + default: + if (GetServerStateParser().LastCommandSuccessful() && !uidValidityOk) + ProcessMailboxUpdate(false); // handle uidvalidity change + break; + } + } + } else if (!DeathSignalReceived()) + HandleMemoryFailure(); +} + +nsresult nsImapProtocol::BeginMessageDownLoad( + uint32_t total_message_size, // for user, headers and body + const char* content_type) { + nsresult rv = NS_OK; + char* sizeString = PR_smprintf("OPEN Size: %ld", total_message_size); + Log("STREAM", sizeString, "Begin Message Download Stream"); + PR_Free(sizeString); + // start counting how many bytes we see in this message after all + // transformations + m_bytesToChannel = 0; + + if (content_type) { + m_fromHeaderSeen = false; + if (GetServerStateParser().GetDownloadingHeaders()) { + // if we get multiple calls to BeginMessageDownload w/o intervening + // calls to NormalEndMessageDownload or Abort, then we're just + // going to fake a NormalMessageEndDownload. This will most likely + // cause an empty header to get written to the db, and the user + // will have to delete the empty header themselves, which + // should remove the message from the server as well. + if (m_curHdrInfo) NormalMessageEndDownload(); + if (!m_curHdrInfo) m_curHdrInfo = m_hdrDownloadCache->StartNewHdr(); + if (m_curHdrInfo) m_curHdrInfo->SetMsgSize(total_message_size); + return NS_OK; + } + // if we have a mock channel, that means we have a channel listener who + // wants the message. So set up a pipe. We'll write the message into one end + // of the pipe and they will read it out of the other end. + if (m_channelListener) { + // create a pipe to pump the message into...the output will go to whoever + // is consuming the message display + // we create an "infinite" pipe in case we get extremely long lines from + // the imap server, and the consumer is waiting for a whole line + nsCOMPtr<nsIPipe> pipe = do_CreateInstance("@mozilla.org/pipe;1"); + rv = pipe->Init(false, false, 4096, PR_UINT32_MAX); + NS_ENSURE_SUCCESS(rv, rv); + + // These always succeed because the pipe is initialized above. + MOZ_ALWAYS_SUCCEEDS( + pipe->GetInputStream(getter_AddRefs(m_channelInputStream))); + MOZ_ALWAYS_SUCCEEDS( + pipe->GetOutputStream(getter_AddRefs(m_channelOutputStream))); + } + // else, if we are saving the message to disk! + else if (m_imapMessageSink /* && m_imapAction == nsIImapUrl::nsImapSaveMessageToDisk */) + { + // we get here when download the inbox for offline use + nsCOMPtr<nsIFile> file; + bool addDummyEnvelope = true; + nsCOMPtr<nsIMsgMessageUrl> msgurl = do_QueryInterface(m_runningUrl); + msgurl->GetMessageFile(getter_AddRefs(file)); + msgurl->GetAddDummyEnvelope(&addDummyEnvelope); + if (file) + rv = m_imapMessageSink->SetupMsgWriteStream(file, addDummyEnvelope); + } + if (m_imapMailFolderSink && m_runningUrl) { + nsCOMPtr<nsISupports> copyState; + if (m_runningUrl) { + m_runningUrl->GetCopyState(getter_AddRefs(copyState)); + if (copyState) // only need this notification during copy + { + nsCOMPtr<nsIMsgMailNewsUrl> mailurl = do_QueryInterface(m_runningUrl); + m_imapMailFolderSink->StartMessage(mailurl); + } + } + } + + } else + HandleMemoryFailure(); + return rv; +} + +void nsImapProtocol::GetShouldDownloadAllHeaders(bool* aResult) { + if (m_imapMailFolderSink) + m_imapMailFolderSink->GetShouldDownloadAllHeaders(aResult); +} + +void nsImapProtocol::GetArbitraryHeadersToDownload(nsCString& aResult) { + if (m_imapServerSink) m_imapServerSink->GetArbitraryHeaders(aResult); +} + +void nsImapProtocol::AdjustChunkSize() { + int32_t deltaInSeconds; + + m_endTime = PR_Now(); + PRTime2Seconds(m_endTime - m_startTime, &deltaInSeconds); + m_trackingTime = false; + if (deltaInSeconds < 0) return; // bogus for some reason + + if (deltaInSeconds <= m_tooFastTime && m_curFetchSize >= m_chunkSize) { + m_chunkSize += m_chunkAddSize; + m_chunkThreshold = m_chunkSize + (m_chunkSize / 2); + // we used to have a max for the chunk size - I don't think that's needed. + } else if (deltaInSeconds <= m_idealTime) + return; + else { + if (m_chunkSize > m_chunkStartSize) + m_chunkSize = m_chunkStartSize; + else if (m_chunkSize > (m_chunkAddSize * 2)) + m_chunkSize -= m_chunkAddSize; + m_chunkThreshold = m_chunkSize + (m_chunkSize / 2); + } + // remember these new values globally so new connections + // can take advantage of them. + if (gChunkSize != m_chunkSize) { + // will cause chunk size pref to be written in CloseStream. + gChunkSizeDirty = true; + gChunkSize = m_chunkSize; + gChunkThreshold = m_chunkThreshold; + } +} + +// authenticated state commands + +// escape any backslashes or quotes. Backslashes are used a lot with our NT +// server +void nsImapProtocol::CreateEscapedMailboxName(const char* rawName, + nsCString& escapedName) { + escapedName.Assign(rawName); + + for (int32_t strIndex = 0; *rawName; strIndex++) { + char currentChar = *rawName++; + if ((currentChar == '\\') || (currentChar == '\"')) + escapedName.Insert('\\', strIndex++); + } +} +void nsImapProtocol::SelectMailbox(const char* mailboxName) { + ProgressEventFunctionUsingNameWithString("imapStatusSelectingMailbox", + mailboxName); + IncrementCommandTagNumber(); + + m_closeNeededBeforeSelect = false; // initial value + GetServerStateParser().ResetFlagInfo(); + nsCString escapedName; + CreateEscapedMailboxName(mailboxName, escapedName); + nsCString commandBuffer(GetServerCommandTag()); + commandBuffer.AppendLiteral(" select \""); + commandBuffer.Append(escapedName.get()); + commandBuffer.Append('"'); + if (UseCondStore()) commandBuffer.AppendLiteral(" (CONDSTORE)"); + commandBuffer.Append(CRLF); + + nsresult res; + res = SendData(commandBuffer.get()); + if (NS_FAILED(res)) return; + ParseIMAPandCheckForNewMail(); + + // Save the folder sink obtained in SetupSinkProxy() for whatever URL just + // caused this SELECT. Needed so idle and noop responses are using the correct + // folder when detecting changed flags or new messages. + m_imapMailFolderSinkSelected = m_imapMailFolderSink; + MOZ_ASSERT(m_imapMailFolderSinkSelected); + Log("SelectMailbox", nullptr, "got m_imapMailFolderSinkSelected"); + + int32_t numOfMessagesInFlagState = 0; + nsImapAction imapAction; + m_flagState->GetNumberOfMessages(&numOfMessagesInFlagState); + res = m_runningUrl->GetImapAction(&imapAction); + // if we've selected a mailbox, and we're not going to do an update because of + // the url type, but don't have the flags, go get them! + if (GetServerStateParser().LastCommandSuccessful() && NS_SUCCEEDED(res) && + imapAction != nsIImapUrl::nsImapSelectFolder && + imapAction != nsIImapUrl::nsImapExpungeFolder && + imapAction != nsIImapUrl::nsImapLiteSelectFolder && + imapAction != nsIImapUrl::nsImapDeleteAllMsgs && + ((GetServerStateParser().NumberOfMessages() != + numOfMessagesInFlagState) && + (numOfMessagesInFlagState == 0))) { + ProcessMailboxUpdate(false); + } +} + +void nsImapProtocol::FetchMsgAttribute(const nsCString& messageIds, + const nsCString& attribute) { + IncrementCommandTagNumber(); + + nsAutoCString commandString(GetServerCommandTag()); + commandString.AppendLiteral(" UID fetch "); + commandString.Append(messageIds); + commandString.AppendLiteral(" ("); + commandString.Append(attribute); + commandString.AppendLiteral(")" CRLF); + nsresult rv = SendData(commandString.get()); + + if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail(commandString.get()); + GetServerStateParser().SetFetchingFlags(false); + // Always clear this flag after every fetch. + m_fetchingWholeMessage = false; +} + +// this routine is used to fetch a message or messages, or headers for a +// message... + +void nsImapProtocol::FallbackToFetchWholeMsg(const nsCString& messageId, + uint32_t messageSize) { + if (m_imapMessageSink && m_runningUrl) { + bool shouldStoreMsgOffline; + m_runningUrl->GetStoreOfflineOnFallback(&shouldStoreMsgOffline); + m_runningUrl->SetStoreResultsOffline(shouldStoreMsgOffline); + } + FetchTryChunking(messageId, + m_imapAction == nsIImapUrl::nsImapMsgFetchPeek + ? kEveryThingRFC822Peek + : kEveryThingRFC822, + true, nullptr, messageSize, true); +} + +void nsImapProtocol::FetchMessage(const nsCString& messageIds, + nsIMAPeFetchFields whatToFetch, + const char* fetchModifier, uint32_t startByte, + uint32_t numBytes, char* part) { + IncrementCommandTagNumber(); + + nsCString commandString; + commandString = "%s UID fetch"; + + switch (whatToFetch) { + case kEveryThingRFC822: + m_flagChangeCount++; + m_fetchingWholeMessage = true; + if (m_trackingTime) AdjustChunkSize(); // we started another segment + m_startTime = PR_Now(); // save start of download time + m_trackingTime = true; + MOZ_LOG(IMAP, LogLevel::Debug, + ("FetchMessage everything: curFetchSize %u numBytes %u", + m_curFetchSize, numBytes)); + if (numBytes > 0) m_curFetchSize = numBytes; + + if (GetServerStateParser().ServerHasIMAP4Rev1Capability()) { + if (GetServerStateParser().GetCapabilityFlag() & kHasXSenderCapability) + commandString.AppendLiteral(" %s (XSENDER UID RFC822.SIZE BODY[]"); + else + commandString.AppendLiteral(" %s (UID RFC822.SIZE BODY[]"); + } else { + if (GetServerStateParser().GetCapabilityFlag() & kHasXSenderCapability) + commandString.AppendLiteral(" %s (XSENDER UID RFC822.SIZE RFC822"); + else + commandString.AppendLiteral(" %s (UID RFC822.SIZE RFC822"); + } + if (numBytes > 0) { + // if we are retrieving chunks + char* byterangeString = PR_smprintf("<%ld.%ld>", startByte, numBytes); + if (byterangeString) { + commandString.Append(byterangeString); + PR_Free(byterangeString); + } + } + commandString.Append(')'); + + break; + + case kEveryThingRFC822Peek: { + MOZ_LOG(IMAP, LogLevel::Debug, + ("FetchMessage peek: curFetchSize %u numBytes %u", m_curFetchSize, + numBytes)); + if (numBytes > 0) m_curFetchSize = numBytes; + const char* formatString = ""; + eIMAPCapabilityFlags server_capabilityFlags = + GetServerStateParser().GetCapabilityFlag(); + + m_fetchingWholeMessage = true; + if (server_capabilityFlags & kIMAP4rev1Capability) { + // use body[].peek since rfc822.peek is not in IMAP4rev1 + if (server_capabilityFlags & kHasXSenderCapability) + formatString = " %s (XSENDER UID RFC822.SIZE BODY.PEEK[]"; + else + formatString = " %s (UID RFC822.SIZE BODY.PEEK[]"; + } else { + if (server_capabilityFlags & kHasXSenderCapability) + formatString = " %s (XSENDER UID RFC822.SIZE RFC822.peek"; + else + formatString = " %s (UID RFC822.SIZE RFC822.peek"; + } + + commandString.Append(formatString); + if (numBytes > 0) { + // if we are retrieving chunks + char* byterangeString = PR_smprintf("<%ld.%ld>", startByte, numBytes); + if (byterangeString) { + commandString.Append(byterangeString); + PR_Free(byterangeString); + } + } + commandString.Append(')'); + } break; + case kHeadersRFC822andUid: + if (GetServerStateParser().ServerHasIMAP4Rev1Capability()) { + eIMAPCapabilityFlags server_capabilityFlags = + GetServerStateParser().GetCapabilityFlag(); + bool aolImapServer = + ((server_capabilityFlags & kAOLImapCapability) != 0); + bool downloadAllHeaders = false; + // checks if we're filtering on "any header" or running a spam filter + // requiring all headers + GetShouldDownloadAllHeaders(&downloadAllHeaders); + + if (!downloadAllHeaders) // if it's ok -- no filters on any header, + // etc. + { + char* headersToDL = nullptr; + char* what = nullptr; + const char* dbHeaders = + (gUseEnvelopeCmd) ? IMAP_DB_HEADERS : IMAP_ENV_AND_DB_HEADERS; + nsCString arbitraryHeaders; + GetArbitraryHeadersToDownload(arbitraryHeaders); + for (uint32_t i = 0; i < mCustomDBHeaders.Length(); i++) { + if (!FindInReadable(mCustomDBHeaders[i], arbitraryHeaders, + nsCaseInsensitiveCStringComparator)) { + if (!arbitraryHeaders.IsEmpty()) arbitraryHeaders.Append(' '); + arbitraryHeaders.Append(mCustomDBHeaders[i]); + } + } + for (uint32_t i = 0; i < mCustomHeaders.Length(); i++) { + if (!FindInReadable(mCustomHeaders[i], arbitraryHeaders, + nsCaseInsensitiveCStringComparator)) { + if (!arbitraryHeaders.IsEmpty()) arbitraryHeaders.Append(' '); + arbitraryHeaders.Append(mCustomHeaders[i]); + } + } + if (arbitraryHeaders.IsEmpty()) + headersToDL = strdup(dbHeaders); + else + headersToDL = + PR_smprintf("%s %s", dbHeaders, arbitraryHeaders.get()); + + if (gUseEnvelopeCmd) + what = PR_smprintf(" ENVELOPE BODY.PEEK[HEADER.FIELDS (%s)])", + headersToDL); + else + what = PR_smprintf(" BODY.PEEK[HEADER.FIELDS (%s)])", headersToDL); + free(headersToDL); + if (what) { + commandString.AppendLiteral(" %s (UID "); + if (m_isGmailServer) + commandString.AppendLiteral("X-GM-MSGID X-GM-THRID X-GM-LABELS "); + if (aolImapServer) + commandString.AppendLiteral(" XAOL.SIZE"); + else + commandString.AppendLiteral("RFC822.SIZE"); + commandString.AppendLiteral(" FLAGS"); + commandString.Append(what); + PR_Free(what); + } else { + commandString.AppendLiteral( + " %s (UID RFC822.SIZE BODY.PEEK[HEADER] FLAGS)"); + } + } else + commandString.AppendLiteral( + " %s (UID RFC822.SIZE BODY.PEEK[HEADER] FLAGS)"); + } else + commandString.AppendLiteral( + " %s (UID RFC822.SIZE RFC822.HEADER FLAGS)"); + break; + case kUid: + commandString.AppendLiteral(" %s (UID)"); + break; + case kFlags: + GetServerStateParser().SetFetchingFlags(true); + commandString.AppendLiteral(" %s (FLAGS)"); + break; + case kRFC822Size: + commandString.AppendLiteral(" %s (RFC822.SIZE)"); + break; + case kBodyStart: { + int32_t numBytesToFetch; + m_runningUrl->GetNumBytesToFetch(&numBytesToFetch); + + commandString.AppendLiteral( + " %s (UID BODY.PEEK[HEADER.FIELDS (Content-Type " + "Content-Transfer-Encoding)] BODY.PEEK[TEXT]<0."); + commandString.AppendInt(numBytesToFetch); + commandString.AppendLiteral(">)"); + } break; + case kRFC822HeadersOnly: + if (GetServerStateParser().ServerHasIMAP4Rev1Capability()) { + if (part) { + commandString.AppendLiteral(" %s (BODY["); + char* what = PR_smprintf("%s.HEADER])", part); + if (what) { + commandString.Append(what); + PR_Free(what); + } else + HandleMemoryFailure(); + } else { + // headers for the top-level message + commandString.AppendLiteral(" %s (BODY[HEADER])"); + } + } else + commandString.AppendLiteral(" %s (RFC822.HEADER)"); + break; + case kMIMEPart: + commandString.AppendLiteral(" %s (BODY.PEEK[%s]"); + if (numBytes > 0) { + // if we are retrieving chunks + char* byterangeString = PR_smprintf("<%ld.%ld>", startByte, numBytes); + if (byterangeString) { + commandString.Append(byterangeString); + PR_Free(byterangeString); + } + } + commandString.Append(')'); + break; + case kMIMEHeader: + commandString.AppendLiteral(" %s (BODY[%s.MIME])"); + break; + } + + if (fetchModifier) commandString.Append(fetchModifier); + + commandString.Append(CRLF); + + // since messageIds can be infinitely long, use a dynamic buffer rather than + // the fixed one + const char* commandTag = GetServerCommandTag(); + int protocolStringSize = commandString.Length() + messageIds.Length() + + PL_strlen(commandTag) + 1 + + (part ? PL_strlen(part) : 0); + char* protocolString = (char*)PR_CALLOC(protocolStringSize); + + if (protocolString) { + char* cCommandStr = ToNewCString(commandString); + if ((whatToFetch == kMIMEPart) || (whatToFetch == kMIMEHeader)) { + PR_snprintf(protocolString, // string to create + protocolStringSize, // max size + cCommandStr, // format string + commandTag, // command tag + messageIds.get(), part); + } else { + PR_snprintf(protocolString, // string to create + protocolStringSize, // max size + cCommandStr, // format string + commandTag, // command tag + messageIds.get()); + } + + nsresult rv = SendData(protocolString); + + free(cCommandStr); + if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail(protocolString); + PR_Free(protocolString); + GetServerStateParser().SetFetchingFlags(false); + // Always clear this flag after every fetch. + m_fetchingWholeMessage = false; + if (GetServerStateParser().LastCommandSuccessful() && CheckNeeded()) + Check(); + } else + HandleMemoryFailure(); +} + +void nsImapProtocol::FetchTryChunking(const nsCString& messageIds, + nsIMAPeFetchFields whatToFetch, + bool idIsUid, char* part, + uint32_t downloadSize, bool tryChunking) { + GetServerStateParser().SetTotalDownloadSize(downloadSize); + MOZ_LOG(IMAP, LogLevel::Debug, + ("FetchTryChunking: curFetchSize %u", downloadSize)); + MOZ_ASSERT(!part, "fetching a part should no longer occur"); + m_curFetchSize = downloadSize; // we'll change this if chunking. + if (m_fetchByChunks && tryChunking && + GetServerStateParser().ServerHasIMAP4Rev1Capability() && + (downloadSize > (uint32_t)m_chunkThreshold)) { + uint32_t startByte = 0; + m_curFetchSize = m_chunkSize; + GetServerStateParser().ClearLastFetchChunkReceived(); + while (!DeathSignalReceived() && !GetPseudoInterrupted() && + !GetServerStateParser().GetLastFetchChunkReceived() && + GetServerStateParser().ContinueParse()) { + GetServerStateParser().ClearNumBytesFetched(); + // This chunk is a fetch of m_chunkSize bytes. But m_chunkSize can be + // changed inside FetchMessage(). Save the original value of m_chunkSize + // to set the correct offset (startByte) for the next chunk. + int32_t bytesFetched = m_chunkSize; + FetchMessage(messageIds, whatToFetch, nullptr, startByte, bytesFetched, + part); + if (!GetServerStateParser().GetNumBytesFetched()) { + // Fetch returned zero bytes chunk from server. This occurs if the + // message was expunged during the fetch. + MOZ_LOG(IMAP, LogLevel::Info, + ("FetchTryChunking: Zero bytes chunk fetched; message probably " + "expunged")); + break; + } + startByte += bytesFetched; + } + + // Only abort the stream if this is a normal message download + // Otherwise, let the body shell abort the stream. + if ((whatToFetch == kEveryThingRFC822) && + ((startByte > 0 && (startByte < downloadSize) && + (DeathSignalReceived() || GetPseudoInterrupted())) || + !GetServerStateParser().ContinueParse())) { + AbortMessageDownLoad(); + PseudoInterrupt(false); + } + } else { + // small message, or (we're not chunking and not doing bodystructure), + // or the server is not rev1. + // Just fetch the whole thing. + FetchMessage(messageIds, whatToFetch, nullptr, 0, 0, part); + } +} + +void nsImapProtocol::PostLineDownLoadEvent(const char* line, + uint32_t uidOfMessage) { + if (!GetServerStateParser().GetDownloadingHeaders()) { + uint32_t byteCount = PL_strlen(line); + bool echoLineToMessageSink = false; + // if we have a channel listener, then just spool the message + // directly to the listener + if (m_channelListener) { + uint32_t count = 0; + if (m_channelOutputStream) { + nsresult rv = m_channelOutputStream->Write(line, byteCount, &count); + NS_ASSERTION(count == byteCount, + "IMAP channel pipe couldn't buffer entire write"); + if (NS_SUCCEEDED(rv)) { + m_channelListener->OnDataAvailable(m_mockChannel, + m_channelInputStream, 0, count); + } + // else some sort of explosion? + } + } + if (m_runningUrl) + m_runningUrl->GetStoreResultsOffline(&echoLineToMessageSink); + + m_bytesToChannel += byteCount; + if (m_imapMessageSink && line && echoLineToMessageSink && + !GetPseudoInterrupted()) + 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 nullptr 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 +// HandleMessageDownLoadLine("part 1 ", 1); +// HandleMessageDownLoadLine("part 2\r\n", 0); +// However, it is assumed that a CRLF or a CRCRLF is never split (i.e., this +// is ensured *before* invoking this method). +void nsImapProtocol::HandleMessageDownLoadLine(const char* line, + bool isPartialLine, + char* lineCopy) { + NS_ENSURE_TRUE_VOID(line); + NS_ASSERTION(lineCopy == nullptr || !PL_strcmp(line, lineCopy), + "line and lineCopy must contain the same string"); + const char* messageLine = line; + uint32_t lineLength = strlen(messageLine); + const char* cEndOfLine = messageLine + lineLength; + char* localMessageLine = nullptr; + + // If we obtain a partial line (due to fetching by chunks), we do not + // add/modify the end-of-line terminator. + if (!isPartialLine) { + // Change this line to native line termination, duplicate if necessary. + // Do not assume that the line really ends in CRLF + // to start with, even though it is supposed to be RFC822 + + // normalize line endings to CRLF unless we are saving the message to disk + bool canonicalLineEnding = true; + nsCOMPtr<nsIMsgMessageUrl> msgUrl = do_QueryInterface(m_runningUrl); + + if (m_imapAction == nsIImapUrl::nsImapSaveMessageToDisk && msgUrl) + msgUrl->GetCanonicalLineEnding(&canonicalLineEnding); + + NS_ASSERTION(MSG_LINEBREAK_LEN == 1 || (MSG_LINEBREAK_LEN == 2 && + !PL_strcmp(CRLF, MSG_LINEBREAK)), + "violated assumptions on MSG_LINEBREAK"); + if (MSG_LINEBREAK_LEN == 1 && !canonicalLineEnding) { + bool lineEndsWithCRorLF = + lineLength >= 1 && (cEndOfLine[-1] == '\r' || cEndOfLine[-1] == '\n'); + char* endOfLine; + if (lineCopy && lineEndsWithCRorLF) // true for most lines + { + endOfLine = lineCopy + lineLength; + messageLine = lineCopy; + } else { + // leave enough room for one more char, MSG_LINEBREAK[0] + localMessageLine = (char*)PR_MALLOC(lineLength + 2); + if (!localMessageLine) // memory failure + return; + PL_strcpy(localMessageLine, line); + endOfLine = localMessageLine + lineLength; + messageLine = localMessageLine; + } + + if (lineLength >= 2 && endOfLine[-2] == '\r' && endOfLine[-1] == '\n') { + if (lineLength >= 3 && endOfLine[-3] == '\r') // CRCRLF + { + endOfLine--; + lineLength--; + } + /* CRLF -> CR or LF */ + endOfLine[-2] = MSG_LINEBREAK[0]; + endOfLine[-1] = '\0'; + lineLength--; + } else if (lineLength >= 1 && + ((endOfLine[-1] == '\r') || (endOfLine[-1] == '\n'))) { + /* CR -> LF or LF -> CR */ + endOfLine[-1] = MSG_LINEBREAK[0]; + } else // no eol characters at all + { + endOfLine[0] = MSG_LINEBREAK[0]; // CR or LF + endOfLine[1] = '\0'; + lineLength++; + } + } else // enforce canonical CRLF linebreaks + { + if (lineLength == 0 || (lineLength == 1 && cEndOfLine[-1] == '\n')) { + messageLine = CRLF; + lineLength = 2; + } else if (cEndOfLine[-1] != '\n' || cEndOfLine[-2] != '\r' || + (lineLength >= 3 && cEndOfLine[-3] == '\r')) { + // The line does not end in CRLF (or it ends in CRCRLF). + // Copy line and leave enough room for two more chars (CR and LF). + localMessageLine = (char*)PR_MALLOC(lineLength + 3); + if (!localMessageLine) // memory failure + return; + PL_strcpy(localMessageLine, line); + char* endOfLine = localMessageLine + lineLength; + messageLine = localMessageLine; + + if (lineLength >= 3 && endOfLine[-1] == '\n' && endOfLine[-2] == '\r') { + // CRCRLF -> CRLF + endOfLine[-2] = '\n'; + endOfLine[-1] = '\0'; + lineLength--; + } else if ((endOfLine[-1] == '\r') || (endOfLine[-1] == '\n')) { + // LF -> CRLF or CR -> CRLF + endOfLine[-1] = '\r'; + endOfLine[0] = '\n'; + endOfLine[1] = '\0'; + lineLength++; + } else // no eol characters at all + { + endOfLine[0] = '\r'; + endOfLine[1] = '\n'; + endOfLine[2] = '\0'; + lineLength += 2; + } + } + } + } + NS_ASSERTION(lineLength == PL_strlen(messageLine), "lineLength not accurate"); + + // check if sender obtained via XSENDER server extension matches "From:" field + const char* xSenderInfo = GetServerStateParser().GetXSenderInfo(); + if (xSenderInfo && *xSenderInfo && !m_fromHeaderSeen) { + if (!PL_strncmp("From: ", messageLine, 6)) { + m_fromHeaderSeen = true; + if (PL_strstr(messageLine, xSenderInfo) != NULL) + // Adding a X-Mozilla-Status line here is not very elegant but it + // works. Another X-Mozilla-Status line is added to the message when + // downloading to a local folder; this new line will also contain the + // 'authed' flag we are adding here. (If the message is again + // uploaded to the server, this flag is lost.) + // 0x0200 == nsMsgMessageFlags::SenderAuthed + HandleMessageDownLoadLine("X-Mozilla-Status: 0200\r\n", false); + GetServerStateParser().FreeXSenderInfo(); + } + } + + if (GetServerStateParser().GetDownloadingHeaders()) { + if (!m_curHdrInfo) + BeginMessageDownLoad(GetServerStateParser().SizeOfMostRecentMessage(), + MESSAGE_RFC822); + if (m_curHdrInfo) { + if (NS_FAILED(m_curHdrInfo->CacheLine( + messageLine, GetServerStateParser().CurrentResponseUID()))) + NS_ERROR("CacheLine for a header failed"); + } + PR_Free(localMessageLine); + return; + } + // if this line is for a different message, or the incoming line is too big + if (((m_downloadLineCache->CurrentUID() != + GetServerStateParser().CurrentResponseUID()) && + !m_downloadLineCache->CacheEmpty()) || + (m_downloadLineCache->SpaceAvailable() < lineLength + 1)) + FlushDownloadCache(); + + // so now the cache is flushed, but this string might still be too big + if (m_downloadLineCache->SpaceAvailable() < lineLength + 1) + PostLineDownLoadEvent(messageLine, + GetServerStateParser().CurrentResponseUID()); + else { + NS_ASSERTION( + (PL_strlen(messageLine) + 1) <= m_downloadLineCache->SpaceAvailable(), + "Oops... line length greater than space available"); + if (NS_FAILED(m_downloadLineCache->CacheLine( + messageLine, GetServerStateParser().CurrentResponseUID()))) + NS_ERROR("CacheLine for message body failed"); + } + PR_Free(localMessageLine); +} + +void nsImapProtocol::FlushDownloadCache() { + if (!m_downloadLineCache->CacheEmpty()) { + msg_line_info* downloadLine = m_downloadLineCache->GetCurrentLineInfo(); + PostLineDownLoadEvent(downloadLine->adoptedMessageLine, + downloadLine->uidOfMessage); + m_downloadLineCache->ResetCache(); + } +} + +void nsImapProtocol::NormalMessageEndDownload() { + Log("STREAM", "CLOSE", "Normal Message End Download Stream"); + + if (m_trackingTime) AdjustChunkSize(); + if (m_imapMailFolderSink && m_curHdrInfo && + GetServerStateParser().GetDownloadingHeaders()) { + m_curHdrInfo->SetMsgSize(GetServerStateParser().SizeOfMostRecentMessage()); + m_curHdrInfo->SetMsgUid(GetServerStateParser().CurrentResponseUID()); + m_hdrDownloadCache->FinishCurrentHdr(); + int32_t numHdrsCached; + m_hdrDownloadCache->GetNumHeaders(&numHdrsCached); + if (numHdrsCached == kNumHdrsToXfer) { + m_imapMailFolderSink->ParseMsgHdrs(this, m_hdrDownloadCache); + m_hdrDownloadCache->ResetAll(); + } + } + FlushDownloadCache(); + + if (!GetServerStateParser().GetDownloadingHeaders()) { + int32_t updatedMessageSize = -1; + if (m_fetchingWholeMessage) { + updatedMessageSize = m_bytesToChannel; + if (m_bytesToChannel != + GetServerStateParser().SizeOfMostRecentMessage()) { + MOZ_LOG(IMAP, LogLevel::Debug, + ("STREAM:CLOSE Server's RFC822.SIZE %u, actual size %u", + GetServerStateParser().SizeOfMostRecentMessage(), + m_bytesToChannel)); + } + } + // need to know if we're downloading for display or not. We'll use action == + // nsImapMsgFetch for now + nsImapAction imapAction = + nsIImapUrl::nsImapSelectFolder; // just set it to some legal value + if (m_runningUrl) m_runningUrl->GetImapAction(&imapAction); + + if (m_imapMessageSink) { + if (m_mockChannel) { + // Have a mock channel, tell channel that write to cache is done. + m_mockChannel->SetWritingToCache(false); + MOZ_LOG(IMAP, LogLevel::Debug, ("%s: End cache write", __func__)); + } + m_imapMessageSink->NormalEndMsgWriteStream( + m_downloadLineCache->CurrentUID(), + imapAction == nsIImapUrl::nsImapMsgFetch, m_runningUrl, + updatedMessageSize); + } + + if (m_runningUrl && m_imapMailFolderSink) { + nsCOMPtr<nsISupports> copyState; + m_runningUrl->GetCopyState(getter_AddRefs(copyState)); + if (copyState) // only need this notification during copy + { + nsCOMPtr<nsIMsgMailNewsUrl> mailUrl(do_QueryInterface(m_runningUrl)); + m_imapMailFolderSink->EndMessage(mailUrl, + m_downloadLineCache->CurrentUID()); + } + } + } + m_curHdrInfo = nullptr; +} + +void nsImapProtocol::AbortMessageDownLoad() { + Log("STREAM", "CLOSE", "Abort Message Download Stream"); + + if (m_trackingTime) AdjustChunkSize(); + FlushDownloadCache(); + if (GetServerStateParser().GetDownloadingHeaders()) { + if (m_imapMailFolderSink) + m_imapMailFolderSink->AbortHeaderParseStream(this); + } else if (m_imapMessageSink) + m_imapMessageSink->AbortMsgWriteStream(); + + m_curHdrInfo = nullptr; +} + +void nsImapProtocol::ProcessMailboxUpdate(bool handlePossibleUndo) { + if (DeathSignalReceived()) return; + + // Update quota information + char* boxName; + GetSelectedMailboxName(&boxName); + GetQuotaDataIfSupported(boxName); + PR_Free(boxName); + + // fetch the flags and uids of all existing messages or new ones + if (!DeathSignalReceived() && GetServerStateParser().NumberOfMessages()) { + if (handlePossibleUndo) { + // undo any delete flags we may have asked to + nsCString undoIdsStr; + nsAutoCString undoIds; + + GetCurrentUrl()->GetListOfMessageIds(undoIdsStr); + undoIds.Assign(undoIdsStr); + if (!undoIds.IsEmpty()) { + char firstChar = (char)undoIds.CharAt(0); + undoIds.Cut(0, 1); // remove first character + // if this string started with a '-', then this is an undo of a delete + // if its a '+' its a redo + if (firstChar == '-') + Store(undoIds, "-FLAGS (\\Deleted)", + true); // most servers will fail silently on a failure, deal + // with it? + else if (firstChar == '+') + Store(undoIds, "+FLAGS (\\Deleted)", + true); // most servers will fail silently on a failure, deal + // with it? + else + NS_ASSERTION(false, "bogus undo Id's"); + } + } + + // make the parser record these flags + nsCString fetchStr; + int32_t added = 0, deleted = 0; + + m_flagState->GetNumberOfMessages(&added); + deleted = m_flagState->NumberOfDeletedMessages(); + bool flagStateEmpty = !added; + bool useCS = UseCondStore(); + + // Figure out if we need to do a full sync (UID Fetch Flags 1:*), + // a partial sync using CHANGEDSINCE, or a sync from the previous + // highwater mark. + + // If the folder doesn't know about the highest uid, or the flag state + // is empty, and we're not using CondStore, we definitely need a full sync. + // + // Print to log items affecting needFullFolderSync: + MOZ_LOG(IMAP_CS, LogLevel::Debug, + ("Do full sync?: mFolderHighestUID=%" PRIu32 ", added=%" PRId32 + ", useCS=%s", + mFolderHighestUID, added, useCS ? "true" : "false")); + bool needFullFolderSync = !mFolderHighestUID || (flagStateEmpty && !useCS); + bool needFolderSync = false; + + if (!needFullFolderSync) { + // Figure out if we need to do a non-highwater mark sync. + // Set needFolderSync true when at least 1 of these 3 cases is true: + // 1. Have no uids in flag array or all flag elements are marked deleted + // AND not using CONDSTORE. + // 2. Have no uids in flag array or all flag elements are marked deleted + // AND using "just mark as deleted" and EXISTS response count differs from + // stored message count for folder. + // 3. Using CONDSTORE and highest MODSEQ response is not equal to stored + // mod seq for folder. + + // Print to log items affecting needFolderSync: + // clang-format off + MOZ_LOG(IMAP_CS, LogLevel::Debug, + ("1. Do a sync?: added=%" PRId32 ", deleted=%" PRId32 ", useCS=%s", + added, deleted, useCS ? "true" : "false")); + MOZ_LOG(IMAP_CS, LogLevel::Debug, + ("2. Do a sync?: ShowDeletedMsgs=%s, exists=%" PRId32 + ", mFolderTotalMsgCount=%" PRId32, + GetShowDeletedMessages() ? "true" : "false", + GetServerStateParser().NumberOfMessages(), mFolderTotalMsgCount)); + // clang-format on + MOZ_LOG(IMAP_CS, LogLevel::Debug, + ("3. Do a sync?: fHighestModSeq=%" PRIu64 + ", mFolderLastModSeq=%" PRIu64, + GetServerStateParser().fHighestModSeq, mFolderLastModSeq)); + + needFolderSync = + ((flagStateEmpty || added == deleted) && + (!useCS || (GetShowDeletedMessages() && + GetServerStateParser().NumberOfMessages() != + mFolderTotalMsgCount))) || + (useCS && GetServerStateParser().fHighestModSeq != mFolderLastModSeq); + } + MOZ_LOG(IMAP_CS, LogLevel::Debug, + ("needFullFolderSync=%s, needFolderSync=%s", + needFullFolderSync ? "true" : "false", + needFolderSync ? "true" : "false")); + + if (needFullFolderSync || needFolderSync) { + nsCString idsToFetch("1:*"); + char fetchModifier[40] = ""; + if (!needFullFolderSync && !GetShowDeletedMessages() && useCS) { + m_flagState->StartCapture(); + MOZ_LOG(IMAP_CS, LogLevel::Debug, + ("Doing UID fetch 1:* (CHANGEDSINCE %" PRIu64 ")", + mFolderLastModSeq)); + PR_snprintf(fetchModifier, sizeof(fetchModifier), + " (CHANGEDSINCE %llu)", mFolderLastModSeq); + } else + m_flagState->SetPartialUIDFetch(false); + + FetchMessage(idsToFetch, kFlags, fetchModifier); + // lets see if we should expunge during a full sync of flags. + if (GetServerStateParser().LastCommandSuccessful()) { + // if we did a CHANGEDSINCE fetch, do a sanity check on the msg counts + // to see if some other client may have done an expunge. + if (m_flagState->GetPartialUIDFetch()) { + uint32_t numExists = GetServerStateParser().NumberOfMessages(); + uint32_t numPrevExists = mFolderTotalMsgCount; + MOZ_LOG(IMAP_CS, LogLevel::Debug, + ("Sanity, deleted=%" PRId32 ", numPrevExists=%" PRIu32 + ", numExists=%" PRIu32, + m_flagState->NumberOfDeletedMessages(), numPrevExists, + numExists)); + // Determine the number of new UIDs just fetched that are greater than + // the saved highest UID for the folder. numToCheck will contain the + // number of UIDs just fetched and, of course, not all are new. + uint32_t numNewUIDs = 0; + uint32_t numToCheck = m_flagState->GetNumAdded(); + bool flagChangeDetected = false; + bool expungeHappened = false; + MOZ_LOG(IMAP_CS, LogLevel::Debug, + ("numToCheck=%" PRIu32, numToCheck)); + if (numToCheck && mFolderHighestUID) { + uint32_t uid; + int32_t topIndex; + m_flagState->GetNumberOfMessages(&topIndex); + MOZ_LOG( + IMAP_CS, LogLevel::Debug, + ("Partial fetching. Number of UIDs stored=%" PRId32, topIndex)); + do { + topIndex--; + // Check for potential infinite loop here. This has happened but + // don't know why. If topIndex is negative at this point, set + // expungeHappened true to recover by doing a full flag fetch. + if (topIndex < 0) { + expungeHappened = true; + MOZ_LOG(IMAP_CS, LogLevel::Error, + ("Zero or negative number of UIDs stored, do full flag " + "fetch")); + break; + } + m_flagState->GetUidOfMessage(topIndex, &uid); + if (uid && uid != nsMsgKey_None) { + if (uid > mFolderHighestUID) { + numNewUIDs++; + MOZ_LOG(IMAP_CS, LogLevel::Debug, + ("numNewUIDs=%" PRIu32 ", Added new UID=%" PRIu32, + numNewUIDs, uid)); + numToCheck--; + } else { + // Just a flag change on an existing UID. No more new UIDs + // will be found. This does not detect an expunged message. + flagChangeDetected = true; + MOZ_LOG(IMAP_CS, LogLevel::Debug, + ("Not new uid=%" PRIu32, uid)); + break; + } + } else { + MOZ_LOG(IMAP_CS, LogLevel::Debug, + ("UID is 0 or a gap, uid=0x%" PRIx32, uid)); + break; + } + } while (numToCheck); + } + + // Another client expunged at least one message if the number of new + // UIDs is not equal to the observed change in the number of messages + // existing in the folder. + expungeHappened = + expungeHappened || numNewUIDs != (numExists - numPrevExists); + if (expungeHappened) { + // Sanity check failed - need full fetch to remove expunged msgs. + MOZ_LOG(IMAP_CS, LogLevel::Debug, + ("Other client expunged msgs, do full fetch to remove " + "expunged msgs")); + m_flagState->Reset(); + m_flagState->SetPartialUIDFetch(false); + FetchMessage("1:*"_ns, kFlags); + } else if (numNewUIDs == 0) { + // Nothing has been expunged and no new UIDs, so if just a flag + // change on existing message(s), avoid unneeded fetch of flags for + // messages with UIDs at and above uid (see var uid above) when + // "highwater mark" fetch occurs below. + if (mFolderHighestUID && flagChangeDetected) { + MOZ_LOG(IMAP_CS, LogLevel::Debug, + ("Avoid unneeded fetches after just flag changes")); + GetServerStateParser().ResetHighestRecordedUID(); + } + } + } + int32_t numDeleted = m_flagState->NumberOfDeletedMessages(); + // Don't do expunge when we are lite selecting folder (because we + // could be doing undo) or if gExpungeOption is kAutoExpungeNever. + // Expunge if we're always expunging, or the number of deleted messages + // is over the threshold, and we're either always respecting the + // threshold, or we're expunging based on the delete model, and the + // delete model is not "just mark it as deleted" (imap delete model). + if (m_imapAction != nsIImapUrl::nsImapLiteSelectFolder && + gExpungeOption != kAutoExpungeNever && + (gExpungeOption == kAutoExpungeAlways || + (numDeleted >= gExpungeThreshold && + (gExpungeOption == kAutoExpungeOnThreshold || + (gExpungeOption == kAutoExpungeDeleteModel && + !GetShowDeletedMessages()))))) + Expunge(); + } + } else { + // Obtain the highest (highwater mark) UID seen since the last UIDVALIDITY + // response occurred (associated with the most recent SELECT for the + // folder). + uint32_t highestRecordedUID = GetServerStateParser().HighestRecordedUID(); + // if we're using CONDSTORE, and the parser hasn't seen any UIDs, use + // the highest UID previously seen and saved for the folder instead. + if (useCS && !highestRecordedUID) highestRecordedUID = mFolderHighestUID; + // clang-format off + MOZ_LOG(IMAP_CS, LogLevel::Debug, + ("Check for new messages above UID=%" PRIu32, highestRecordedUID)); + // clang-format on + AppendUid(fetchStr, highestRecordedUID + 1); + fetchStr.AppendLiteral(":*"); + FetchMessage(fetchStr, kFlags); // only new messages please + } + } else if (GetServerStateParser().LastCommandSuccessful()) { + GetServerStateParser().ResetFlagInfo(); + // the flag state is empty, but not partial. + m_flagState->SetPartialUIDFetch(false); + } + + if (GetServerStateParser().LastCommandSuccessful()) { + nsImapAction imapAction; + nsresult res = m_runningUrl->GetImapAction(&imapAction); + if (NS_SUCCEEDED(res) && imapAction == nsIImapUrl::nsImapLiteSelectFolder) + return; + } + + nsTArray<nsMsgKey> msgIdList; + + if (GetServerStateParser().LastCommandSuccessful()) { + ReentrantMonitorAutoEnter mon(m_waitForBodyIdsMonitor); + RefPtr<nsImapMailboxSpec> new_spec = + GetServerStateParser().CreateCurrentMailboxSpec(); + nsImapAction imapAction; + nsresult res = m_runningUrl->GetImapAction(&imapAction); + if (NS_SUCCEEDED(res) && imapAction == nsIImapUrl::nsImapExpungeFolder) + new_spec->mBoxFlags |= kJustExpunged; + + if (m_imapMailFolderSink) { + bool more; + m_imapMailFolderSink->UpdateImapMailboxInfo(this, new_spec); + m_imapMailFolderSink->GetMsgHdrsToDownload( + &more, &m_progressExpectedNumber, msgIdList); + // Assert that either it's empty string OR it must be header string. + MOZ_ASSERT((m_stringIndex == IMAP_EMPTY_STRING_INDEX) || + (m_stringIndex == IMAP_HEADERS_STRING_INDEX)); + m_progressCurrentNumber[m_stringIndex] = 0; + m_runningUrl->SetMoreHeadersToDownload(more); + // We're going to be re-running this url if there are more headers. + if (more) m_runningUrl->SetRerunningUrl(true); + } + } + + if (GetServerStateParser().LastCommandSuccessful()) { + if (msgIdList.Length() > 0) { + FolderHeaderDump(msgIdList.Elements(), msgIdList.Length()); + } + HeaderFetchCompleted(); + // this might be bogus, how are we going to do pane notification and stuff + // when we fetch bodies without headers! + } + + // wait for a list of bodies to fetch. + if (GetServerStateParser().LastCommandSuccessful()) { + nsTArray<nsMsgKey> msgIds; + WaitForPotentialListOfBodysToFetch(msgIds); + if (msgIds.Length() > 0 && GetServerStateParser().LastCommandSuccessful()) { + // Tell the url that it should store the msg fetch results offline, + // while we're dumping the messages, and then restore the setting. + bool wasStoringOffline; + m_runningUrl->GetStoreResultsOffline(&wasStoringOffline); + m_runningUrl->SetStoreResultsOffline(true); + // Assert that either it's empty string OR it must be message string. + MOZ_ASSERT((m_stringIndex == IMAP_EMPTY_STRING_INDEX) || + (m_stringIndex == IMAP_MESSAGES_STRING_INDEX)); + m_progressCurrentNumber[m_stringIndex] = 0; + m_progressExpectedNumber = msgIds.Length(); + FolderMsgDump(msgIds.Elements(), msgIds.Length(), kEveryThingRFC822Peek); + m_runningUrl->SetStoreResultsOffline(wasStoringOffline); + } + } + if (!GetServerStateParser().LastCommandSuccessful()) + GetServerStateParser().ResetFlagInfo(); +} + +void nsImapProtocol::FolderHeaderDump(uint32_t* msgUids, uint32_t msgCount) { + FolderMsgDump(msgUids, msgCount, kHeadersRFC822andUid); +} + +void nsImapProtocol::FolderMsgDump(uint32_t* msgUids, uint32_t msgCount, + nsIMAPeFetchFields fields) { + // lets worry about this progress stuff later. + switch (fields) { + case kHeadersRFC822andUid: + SetProgressString(IMAP_HEADERS_STRING_INDEX); + break; + case kFlags: + SetProgressString(IMAP_FLAGS_STRING_INDEX); + break; + default: + SetProgressString(IMAP_MESSAGES_STRING_INDEX); + break; + } + + FolderMsgDumpLoop(msgUids, msgCount, fields); + + SetProgressString(IMAP_EMPTY_STRING_INDEX); +} + +void nsImapProtocol::WaitForPotentialListOfBodysToFetch( + nsTArray<nsMsgKey>& msgIdList) { + PRIntervalTime sleepTime = kImapSleepTime; + + ReentrantMonitorAutoEnter fetchListMon(m_fetchBodyListMonitor); + while (!m_fetchBodyListIsNew && !DeathSignalReceived()) + fetchListMon.Wait(sleepTime); + m_fetchBodyListIsNew = false; + + msgIdList = m_fetchBodyIdList.Clone(); +} + +// libmsg uses this to notify a running imap url about message bodies it should +// download. why not just have libmsg explicitly download the message bodies? +NS_IMETHODIMP nsImapProtocol::NotifyBodysToDownload( + const nsTArray<nsMsgKey>& keys) { + ReentrantMonitorAutoEnter fetchListMon(m_fetchBodyListMonitor); + m_fetchBodyIdList = keys.Clone(); + m_fetchBodyListIsNew = true; + fetchListMon.Notify(); + return NS_OK; +} + +NS_IMETHODIMP nsImapProtocol::GetFlagsForUID(uint32_t uid, bool* foundIt, + imapMessageFlagsType* resultFlags, + char** customFlags) { + int32_t i; + + imapMessageFlagsType flags = + m_flagState->GetMessageFlagsFromUID(uid, foundIt, &i); + if (*foundIt) { + *resultFlags = flags; + if ((flags & kImapMsgCustomKeywordFlag) && customFlags) + m_flagState->GetCustomFlags(uid, customFlags); + } + return NS_OK; +} + +NS_IMETHODIMP nsImapProtocol::GetFlagAndUidState( + nsIImapFlagAndUidState** aFlagState) { + NS_ENSURE_ARG_POINTER(aFlagState); + NS_IF_ADDREF(*aFlagState = m_flagState); + return NS_OK; +} + +NS_IMETHODIMP nsImapProtocol::GetSupportedUserFlags(uint16_t* supportedFlags) { + if (!supportedFlags) return NS_ERROR_NULL_POINTER; + + *supportedFlags = m_flagState->GetSupportedUserFlags(); + return NS_OK; +} +void nsImapProtocol::FolderMsgDumpLoop(uint32_t* msgUids, uint32_t msgCount, + nsIMAPeFetchFields fields) { + int32_t msgCountLeft = msgCount; + uint32_t msgsDownloaded = 0; + do { + nsCString idString; + uint32_t msgsToDownload = msgCountLeft; + AllocateImapUidString(msgUids + msgsDownloaded, msgsToDownload, m_flagState, + idString); // 20 * 200 + FetchMessage(idString, fields); + msgsDownloaded += msgsToDownload; + msgCountLeft -= msgsToDownload; + } while (msgCountLeft > 0 && !DeathSignalReceived()); +} + +void nsImapProtocol::HeaderFetchCompleted() { + if (m_imapMailFolderSink) + m_imapMailFolderSink->ParseMsgHdrs(this, m_hdrDownloadCache); + m_hdrDownloadCache->ReleaseAll(); + + if (m_imapMailFolderSink) m_imapMailFolderSink->HeaderFetchCompleted(this); +} + +// Use the noop to tell the server we are still here, and therefore we are +// willing to receive status updates. The recent or exists response from the +// server could tell us that there is more mail waiting for us, but we need to +// check the flags of the mail and the high water mark to make sure that we do +// not tell the user that there is new mail when perhaps they have already read +// it in another machine. + +void nsImapProtocol::PeriodicBiff() { + nsMsgBiffState startingState = m_currentBiffState; + + if (GetServerStateParser().GetIMAPstate() == + nsImapServerResponseParser::kFolderSelected) { + Noop(); // check the latest number of messages + int32_t numMessages = 0; + m_flagState->GetNumberOfMessages(&numMessages); + if (GetServerStateParser().NumberOfMessages() != numMessages) { + uint32_t id = GetServerStateParser().HighestRecordedUID() + 1; + nsCString fetchStr; // only update flags + uint32_t added = 0, deleted = 0; + + deleted = m_flagState->NumberOfDeletedMessages(); + added = numMessages; + if (!added || (added == deleted)) // empty keys, get them all + id = 1; + + // sprintf(fetchStr, "%ld:%ld", id, id + + // GetServerStateParser().NumberOfMessages() - + // fFlagState->GetNumberOfMessages()); + AppendUid(fetchStr, id); + fetchStr.AppendLiteral(":*"); + FetchMessage(fetchStr, kFlags); + if (((uint32_t)m_flagState->GetHighestNonDeletedUID() >= id) && + m_flagState->IsLastMessageUnseen()) + m_currentBiffState = nsIMsgFolder::nsMsgBiffState_NewMail; + else + m_currentBiffState = nsIMsgFolder::nsMsgBiffState_NoMail; + } else + m_currentBiffState = nsIMsgFolder::nsMsgBiffState_NoMail; + } else + m_currentBiffState = nsIMsgFolder::nsMsgBiffState_Unknown; + + if (startingState != m_currentBiffState) + SendSetBiffIndicatorEvent(m_currentBiffState); +} + +void nsImapProtocol::SendSetBiffIndicatorEvent(nsMsgBiffState newState) { + if (m_imapMailFolderSink) + m_imapMailFolderSink->SetBiffStateAndUpdate(newState); +} + +/* static */ void nsImapProtocol::LogImapUrl(const char* logMsg, + nsIImapUrl* imapUrl) { + if (MOZ_LOG_TEST(IMAP, LogLevel::Info)) { + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(imapUrl); + if (mailnewsUrl) { + nsAutoCString urlSpec, unescapedUrlSpec; + nsresult rv = mailnewsUrl->GetSpec(urlSpec); + if (NS_FAILED(rv)) return; + MsgUnescapeString(urlSpec, 0, unescapedUrlSpec); + MOZ_LOG(IMAP, LogLevel::Info, ("%s:%s", logMsg, unescapedUrlSpec.get())); + } + } +} + +// log info including current state... +void nsImapProtocol::Log(const char* logSubName, const char* extraInfo, + const char* logData) { + if (MOZ_LOG_TEST(IMAP, LogLevel::Info)) { + static const char nonAuthStateName[] = "NA"; + static const char authStateName[] = "A"; + static const char selectedStateName[] = "S"; + const nsCString& hostName = + GetImapHostName(); // initialize to empty string + + int32_t logDataLen = PL_strlen(logData); // PL_strlen checks for null + nsCString logDataLines; + const char* logDataToLog; + int32_t lastLineEnd; + + // nspr line length is 512, and we allow some space for the log preamble. + const int kLogDataChunkSize = 400; + + // break up buffers > 400 bytes on line boundaries. + if (logDataLen > kLogDataChunkSize) { + logDataLines.Assign(logData); + lastLineEnd = logDataLines.RFindChar('\n', kLogDataChunkSize); + // null terminate the last line + if (lastLineEnd == kNotFound) lastLineEnd = kLogDataChunkSize - 1; + + logDataLines.Insert('\0', lastLineEnd + 1); + logDataToLog = logDataLines.get(); + } else { + logDataToLog = logData; + lastLineEnd = logDataLen; + } + switch (GetServerStateParser().GetIMAPstate()) { + case nsImapServerResponseParser::kFolderSelected: + if (extraInfo) + MOZ_LOG(IMAP, LogLevel::Info, + ("%p:%s:%s-%s:%s:%s: %.400s", this, hostName.get(), + selectedStateName, + GetServerStateParser().GetSelectedMailboxName(), logSubName, + extraInfo, logDataToLog)); + else + MOZ_LOG(IMAP, LogLevel::Info, + ("%p:%s:%s-%s:%s: %.400s", this, hostName.get(), + selectedStateName, + GetServerStateParser().GetSelectedMailboxName(), logSubName, + logDataToLog)); + break; + case nsImapServerResponseParser::kNonAuthenticated: + case nsImapServerResponseParser::kAuthenticated: { + const char* stateName = (GetServerStateParser().GetIMAPstate() == + nsImapServerResponseParser::kNonAuthenticated) + ? nonAuthStateName + : authStateName; + if (extraInfo) + MOZ_LOG(IMAP, LogLevel::Info, + ("%p:%s:%s:%s:%s: %.400s", this, hostName.get(), stateName, + logSubName, extraInfo, logDataToLog)); + else + MOZ_LOG(IMAP, LogLevel::Info, + ("%p:%s:%s:%s: %.400s", this, hostName.get(), stateName, + logSubName, logDataToLog)); + } + } + + // dump the rest of the string in < 400 byte chunks + while (logDataLen > kLogDataChunkSize) { + logDataLines.Cut( + 0, + lastLineEnd + 2); // + 2 to account for the LF and the '\0' we added + logDataLen = logDataLines.Length(); + lastLineEnd = (logDataLen > kLogDataChunkSize) + ? logDataLines.RFindChar('\n', kLogDataChunkSize) + : kNotFound; + // null terminate the last line + if (lastLineEnd == kNotFound) lastLineEnd = kLogDataChunkSize - 1; + logDataLines.Insert('\0', lastLineEnd + 1); + logDataToLog = logDataLines.get(); + MOZ_LOG(IMAP, LogLevel::Info, ("%.400s", logDataToLog)); + } + } +} + +// In 4.5, this posted an event back to libmsg and blocked until it got a +// response. We may still have to do this.It would be nice if we could preflight +// this value, but we may not always know when we'll need it. +uint32_t nsImapProtocol::GetMessageSize(const nsACString& messageId) { + uint32_t size = 0; + if (m_imapMessageSink) + m_imapMessageSink->GetMessageSizeFromDB(PromiseFlatCString(messageId).get(), + &size); + if (DeathSignalReceived()) size = 0; + return size; +} + +// message id string utility functions +/* static */ bool nsImapProtocol::HandlingMultipleMessages( + const nsCString& messageIdString) { + return (MsgFindCharInSet(messageIdString, ",:") != kNotFound); +} + +uint32_t nsImapProtocol::CountMessagesInIdString(const char* idString) { + uint32_t numberOfMessages = 0; + char* uidString = PL_strdup(idString); + + if (uidString) { + // This is in the form <id>,<id>, or <id1>:<id2> + char curChar = *uidString; + bool isRange = false; + int32_t curToken; + int32_t saveStartToken = 0; + + for (char* curCharPtr = uidString; curChar && *curCharPtr;) { + char* currentKeyToken = curCharPtr; + curChar = *curCharPtr; + while (curChar != ':' && curChar != ',' && curChar != '\0') + curChar = *curCharPtr++; + *(curCharPtr - 1) = '\0'; + curToken = atol(currentKeyToken); + if (isRange) { + while (saveStartToken < curToken) { + numberOfMessages++; + saveStartToken++; + } + } + + numberOfMessages++; + isRange = (curChar == ':'); + if (isRange) saveStartToken = curToken + 1; + } + PR_Free(uidString); + } + return numberOfMessages; +} + +// It would be really nice not to have to use this method nearly as much as we +// did in 4.5 - we need to think about this some. Some of it may just go away in +// the new world order +bool nsImapProtocol::DeathSignalReceived() { + // ignore mock channel status if we've been pseudo interrupted + // ### need to make sure we clear pseudo interrupted status appropriately. + if (!GetPseudoInterrupted() && m_mockChannel) { + nsresult returnValue; + m_mockChannel->GetStatus(&returnValue); + if (NS_FAILED(returnValue)) return false; + } + + // Check the other way of cancelling. + ReentrantMonitorAutoEnter threadDeathMon(m_threadDeathMonitor); + return m_threadShouldDie; +} + +NS_IMETHODIMP nsImapProtocol::ResetToAuthenticatedState() { + GetServerStateParser().PreauthSetAuthenticatedState(); + return NS_OK; +} + +NS_IMETHODIMP nsImapProtocol::GetSelectedMailboxName(char** folderName) { + if (!folderName) return NS_ERROR_NULL_POINTER; + if (GetServerStateParser().GetSelectedMailboxName()) + *folderName = PL_strdup((GetServerStateParser().GetSelectedMailboxName())); + return NS_OK; +} + +bool nsImapProtocol::GetPseudoInterrupted() { + ReentrantMonitorAutoEnter pseudoInterruptMon(m_pseudoInterruptMonitor); + return m_pseudoInterrupted; +} + +NS_IMETHODIMP +nsImapProtocol::PseudoInterrupt(bool interrupt) { + ReentrantMonitorAutoEnter pseudoInterruptMon(m_pseudoInterruptMonitor); + m_pseudoInterrupted = interrupt; + if (interrupt) Log("CONTROL", NULL, "PSEUDO-Interrupted"); + return NS_OK; +} + +void nsImapProtocol::SetActive(bool active) { + ReentrantMonitorAutoEnter dataMemberMon(m_dataMemberMonitor); + m_active = active; +} + +bool nsImapProtocol::GetActive() { + ReentrantMonitorAutoEnter dataMemberMon(m_dataMemberMonitor); + return m_active; +} + +bool nsImapProtocol::GetShowAttachmentsInline() { + bool showAttachmentsInline = true; + if (m_imapServerSink) + m_imapServerSink->GetShowAttachmentsInline(&showAttachmentsInline); + return showAttachmentsInline; +} + +// Adds a set of rights for a given user on a given mailbox on the current host. +// if userName is NULL, it means "me," or MYRIGHTS. +void nsImapProtocol::AddFolderRightsForUser(const char* mailboxName, + const char* userName, + const char* rights) { + if (!userName) userName = ""; + if (m_imapServerSink) + m_imapServerSink->AddFolderRights(nsDependentCString(mailboxName), + nsDependentCString(userName), + nsDependentCString(rights)); +} + +void nsImapProtocol::SetCopyResponseUid(const char* msgIdString) { + if (m_imapMailFolderSink) + m_imapMailFolderSink->SetCopyResponseUid(msgIdString, m_runningUrl); +} + +void nsImapProtocol::CommitNamespacesForHostEvent() { + if (m_imapServerSink) m_imapServerSink->CommitNamespaces(); +} + +// notifies libmsg that we have new capability data for the current host +void nsImapProtocol::CommitCapability() { + if (m_imapServerSink) { + m_imapServerSink->SetCapability(GetServerStateParser().GetCapabilityFlag()); + } +} + +// rights is a single string of rights, as specified by RFC2086, the IMAP ACL +// extension. Clears all rights for a given folder, for all users. +void nsImapProtocol::ClearAllFolderRights() { + if (m_imapMailFolderSink) m_imapMailFolderSink->ClearFolderRights(); +} + +// Reads a line from the socket. +// Upon failure, the thread will be flagged for shutdown, and +// m_connectionStatus will be set to a failing code. +// Remember that some socket errors are deferred until the first read +// attempt, so this function could be the first place we hear about +// connection issues (e.g. bad certificates for SSL). +char* nsImapProtocol::CreateNewLineFromSocket() { + bool needMoreData = false; + char* newLine = nullptr; + uint32_t numBytesInLine = 0; + nsresult rv = NS_OK; + // we hold a ref to the input stream in case we get cancelled from the + // ui thread, which releases our ref to the input stream, and can + // cause the pipe to get deleted before the monitor the read is + // blocked on gets notified. When that happens, the imap thread + // will stay blocked. + nsCOMPtr<nsIInputStream> kungFuGrip = m_inputStream; + + if (m_mockChannel) { + nsImapMockChannel* imapChannel = + static_cast<nsImapMockChannel*>(m_mockChannel.get()); + + mozilla::MonitorAutoLock lock(imapChannel->mSuspendedMonitor); + + bool suspended = imapChannel->mSuspended; + if (suspended) + MOZ_LOG(IMAP, LogLevel::Debug, + ("Waiting until [imapChannel=%p] is resumed.", imapChannel)); + while (imapChannel->mSuspended) { + lock.Wait(); + } + if (suspended) + MOZ_LOG( + IMAP, LogLevel::Debug, + ("Done waiting, [imapChannel=%p] has been resumed.", imapChannel)); + } + + do { + newLine = m_inputStreamBuffer->ReadNextLine(m_inputStream, numBytesInLine, + needMoreData, &rv); + MOZ_LOG(IMAP, LogLevel::Verbose, + ("ReadNextLine [rv=0x%" PRIx32 " stream=%p nb=%u needmore=%u]", + static_cast<uint32_t>(rv), m_inputStream.get(), numBytesInLine, + needMoreData)); + + } while (!newLine && NS_SUCCEEDED(rv) && + !DeathSignalReceived()); // until we get the next line and haven't + // been interrupted + + kungFuGrip = nullptr; + + if (NS_FAILED(rv)) { + switch (rv) { + case NS_ERROR_UNKNOWN_HOST: + case NS_ERROR_UNKNOWN_PROXY_HOST: + AlertUserEventUsingName("imapUnknownHostError"); + break; + case NS_ERROR_CONNECTION_REFUSED: + case NS_ERROR_PROXY_CONNECTION_REFUSED: + AlertUserEventUsingName("imapConnectionRefusedError"); + break; + case NS_ERROR_NET_TIMEOUT: + case NS_ERROR_NET_RESET: + case NS_BASE_STREAM_CLOSED: + case NS_ERROR_NET_INTERRUPT: + // we should retry on RESET, especially for SSL... + if ((TestFlag(IMAP_RECEIVED_GREETING) || rv == NS_ERROR_NET_RESET) && + m_runningUrl && !m_retryUrlOnError) { + bool rerunningUrl; + nsImapAction imapAction; + m_runningUrl->GetRerunningUrl(&rerunningUrl); + m_runningUrl->GetImapAction(&imapAction); + // don't rerun if we already were rerunning. And don't rerun + // online move/copies that timeout. + if (!rerunningUrl && (rv != NS_ERROR_NET_TIMEOUT || + (imapAction != nsIImapUrl::nsImapOnlineCopy && + imapAction != nsIImapUrl::nsImapOnlineMove))) { + m_runningUrl->SetRerunningUrl(true); + m_retryUrlOnError = true; + break; + } + } + if (rv == NS_ERROR_NET_TIMEOUT) + AlertUserEventUsingName("imapNetTimeoutError"); + else + AlertUserEventUsingName(TestFlag(IMAP_RECEIVED_GREETING) + ? "imapServerDisconnected" + : "imapServerDroppedConnection"); + break; + default: + // This is probably a TLS error. Usually TLS errors won't show up until + // we do ReadNextLine() above. Since we're in the IMAP thread we can't + // call NSSErrorsService::GetErrorClass() to determine if the error + // should result in an non-fatal override dialog (usually certificate + // issues) or if it's a fatal protocol error that the user must be + // alerted to. Instead, we use some publicly-accessible macros and a + // function to determine this. + if (NS_ERROR_GET_MODULE(rv) == NS_ERROR_MODULE_SECURITY && + NS_ERROR_GET_SEVERITY(rv) == NS_ERROR_SEVERITY_ERROR) { + // It's an error of class 21 (SSL/TLS/Security), e.g., overridable + // SSL_ERROR_BAD_CERT_DOMAIN from security/nss/lib/ssl/sslerr.h + // rv = 0x80000000 + 0x00450000 + 0x00150000 + 0x00002ff4 = 0x805A2ff4 + int32_t sec_error = -1 * NS_ERROR_GET_CODE(rv); // = 0xFFFFD00C + if (!mozilla::psm::ErrorIsOverridable(sec_error)) { + AlertUserEventUsingName("imapTlsError"); + } + + // Stash the socket transport securityInfo on the URL so it will be + // available in nsIUrlListener OnStopRunningUrl() callbacks to trigger + // the override dialog or a security related error message. + // Currently this is only used to trigger the override dialog. + if (m_runningUrl) { + MOZ_ASSERT(!NS_IsMainThread()); + nsCOMPtr<nsIMsgMailNewsUrl> mailNewsUrl = + do_QueryInterface(m_runningUrl); + if (mailNewsUrl) { + nsCOMPtr<nsITransportSecurityInfo> securityInfo; + GetTransportSecurityInfo(getter_AddRefs(securityInfo)); + if (securityInfo) { + nsAutoCString logMsg("Security error - error code="); + nsAutoString errorCodeString; + securityInfo->GetErrorCodeString(errorCodeString); + logMsg.Append(NS_ConvertUTF16toUTF8(errorCodeString)); + Log("CreateNewLineFromSocket", nullptr, logMsg.get()); + + mailNewsUrl->SetFailedSecInfo(securityInfo); + } + } + } + } + break; + } + + nsAutoCString logMsg("clearing IMAP_CONNECTION_IS_OPEN - rv = "); + logMsg.AppendInt(static_cast<uint32_t>(rv), 16); + Log("CreateNewLineFromSocket", nullptr, logMsg.get()); + ClearFlag(IMAP_CONNECTION_IS_OPEN); + TellThreadToDie(); + } + Log("CreateNewLineFromSocket", nullptr, newLine); + SetConnectionStatus(newLine && numBytesInLine + ? NS_OK + : rv); // set > 0 if string is not null or empty + return newLine; +} + +nsresult nsImapProtocol::GetConnectionStatus() { return m_connectionStatus; } + +void nsImapProtocol::SetConnectionStatus(nsresult status) { + // Log failure at Debug level, otherwise use Verbose to avoid huge logs + MOZ_LOG( + IMAP, NS_SUCCEEDED(status) ? LogLevel::Verbose : LogLevel::Debug, + ("SetConnectionStatus(0x%" PRIx32 ")", static_cast<uint32_t>(status))); + m_connectionStatus = status; +} + +void nsImapProtocol::NotifyMessageFlags(imapMessageFlagsType flags, + const nsACString& keywords, + nsMsgKey key, uint64_t highestModSeq) { + if (m_imapMessageSink) { + // if we're selecting the folder, don't need to report the flags; we've + // already fetched them. + if (m_imapAction != nsIImapUrl::nsImapSelectFolder) + m_imapMessageSink->NotifyMessageFlags(flags, keywords, key, + highestModSeq); + } +} + +void nsImapProtocol::NotifySearchHit(const char* hitLine) { + nsresult rv; + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = + do_QueryInterface(m_runningUrl, &rv); + if (m_imapMailFolderSink) + m_imapMailFolderSink->NotifySearchHit(mailnewsUrl, hitLine); +} + +void nsImapProtocol::SetMailboxDiscoveryStatus(EMailboxDiscoverStatus status) { + ReentrantMonitorAutoEnter mon(m_dataMemberMonitor); + m_discoveryStatus = status; +} + +EMailboxDiscoverStatus nsImapProtocol::GetMailboxDiscoveryStatus() { + ReentrantMonitorAutoEnter mon(m_dataMemberMonitor); + return m_discoveryStatus; +} + +bool nsImapProtocol::GetSubscribingNow() { + // ***** code me ***** + return false; // ***** for now +} + +void nsImapProtocol::DiscoverMailboxSpec(nsImapMailboxSpec* adoptedBoxSpec) { + nsImapNamespace* ns = nullptr; + + NS_ASSERTION(m_hostSessionList, "fatal null host session list"); + if (!m_hostSessionList) return; + + m_hostSessionList->GetDefaultNamespaceOfTypeForHost(GetImapServerKey(), + kPersonalNamespace, ns); + const char* nsPrefix = ns ? ns->GetPrefix() : 0; + + if (m_specialXListMailboxes.Count() > 0) { + nsCString strHashKey(adoptedBoxSpec->mAllocatedPathName); + int32_t hashValue = m_specialXListMailboxes.Get(strHashKey); + adoptedBoxSpec->mBoxFlags |= hashValue; + } + + switch (m_hierarchyNameState) { + case kXListing: + if (adoptedBoxSpec->mBoxFlags & + (kImapXListTrash | kImapAllMail | kImapInbox | kImapSent | kImapSpam | + kImapDrafts)) { + nsCString mailboxName(adoptedBoxSpec->mAllocatedPathName); + m_specialXListMailboxes.InsertOrUpdate(mailboxName, + adoptedBoxSpec->mBoxFlags); + // Remember hierarchy delimiter in case this is the first time we've + // connected to the server and we need it to be correct for the + // two-level XLIST we send (INBOX is guaranteed to be in the first + // response). + if (adoptedBoxSpec->mBoxFlags & kImapInbox) + m_runningUrl->SetOnlineSubDirSeparator( + adoptedBoxSpec->mHierarchySeparator); + } + break; + case kListingForFolderFlags: { + // store mailbox flags from LIST for use by LSUB + nsCString mailboxName(adoptedBoxSpec->mAllocatedPathName); + m_standardListMailboxes.InsertOrUpdate(mailboxName, + adoptedBoxSpec->mBoxFlags); + } break; + case kListingForCreate: + case kNoOperationInProgress: + case kDiscoverTrashFolderInProgress: + case kListingForInfoAndDiscovery: { + // standard mailbox specs are stored in m_standardListMailboxes + // because LSUB does necessarily return all mailbox flags. + // count should be > 0 only when we are looking at response of LSUB + if (m_standardListMailboxes.Count() > 0) { + int32_t hashValue = 0; + nsCString strHashKey(adoptedBoxSpec->mAllocatedPathName); + if (m_standardListMailboxes.Get(strHashKey, &hashValue)) + adoptedBoxSpec->mBoxFlags |= hashValue; + else + // if mailbox is not in hash list, then it is subscribed but does not + // exist, so we make sure it can't be selected + adoptedBoxSpec->mBoxFlags |= kNoselect; + } + if (ns && + nsPrefix) // if no personal namespace, there can be no Trash folder + { + bool onlineTrashFolderExists = false; + if (m_hostSessionList) { + if (adoptedBoxSpec->mBoxFlags & (kImapTrash | kImapXListTrash)) { + m_hostSessionList->SetOnlineTrashFolderExistsForHost( + GetImapServerKey(), true); + onlineTrashFolderExists = true; + } else { + m_hostSessionList->GetOnlineTrashFolderExistsForHost( + GetImapServerKey(), onlineTrashFolderExists); + } + } + + // Don't set the Trash flag if not using the Trash model + if (GetDeleteIsMoveToTrash() && !onlineTrashFolderExists && + FindInReadable(m_trashFolderPath, + adoptedBoxSpec->mAllocatedPathName, + nsCaseInsensitiveCStringComparator)) { + bool trashExists = false; + if (StringBeginsWith(m_trashFolderPath, "INBOX/"_ns, + nsCaseInsensitiveCStringComparator)) { + nsAutoCString pathName(adoptedBoxSpec->mAllocatedPathName.get() + + 6); + trashExists = + StringBeginsWith( + adoptedBoxSpec->mAllocatedPathName, m_trashFolderPath, + nsCaseInsensitiveCStringComparator) && /* "INBOX/" */ + pathName.Equals(Substring(m_trashFolderPath, 6), + nsCaseInsensitiveCStringComparator); + } else + trashExists = adoptedBoxSpec->mAllocatedPathName.Equals( + m_trashFolderPath, nsCaseInsensitiveCStringComparator); + + if (m_hostSessionList) + m_hostSessionList->SetOnlineTrashFolderExistsForHost( + GetImapServerKey(), trashExists); + + if (trashExists) adoptedBoxSpec->mBoxFlags |= kImapTrash; + } + } + + // Discover the folder (shuttle over to libmsg, yay) + // Do this only if the folder name is not empty (i.e. the root) + if (!adoptedBoxSpec->mAllocatedPathName.IsEmpty()) { + if (m_hierarchyNameState == kListingForCreate) + adoptedBoxSpec->mBoxFlags |= kNewlyCreatedFolder; + + if (m_imapServerSink) { + bool newFolder; + + m_imapServerSink->PossibleImapMailbox( + adoptedBoxSpec->mAllocatedPathName, + adoptedBoxSpec->mHierarchySeparator, adoptedBoxSpec->mBoxFlags, + &newFolder); + // if it's a new folder to the server sink, setting discovery status + // to eContinueNew will cause us to get the ACL for the new folder. + if (newFolder) SetMailboxDiscoveryStatus(eContinueNew); + + bool useSubscription = false; + + if (m_hostSessionList) + m_hostSessionList->GetHostIsUsingSubscription(GetImapServerKey(), + useSubscription); + + if ((GetMailboxDiscoveryStatus() != eContinue) && + (GetMailboxDiscoveryStatus() != eContinueNew) && + (GetMailboxDiscoveryStatus() != eListMyChildren)) { + SetConnectionStatus(NS_ERROR_FAILURE); + } else if (!adoptedBoxSpec->mAllocatedPathName.IsEmpty() && + (GetMailboxDiscoveryStatus() == eListMyChildren) && + (!useSubscription || GetSubscribingNow())) { + NS_ASSERTION(false, "we should never get here anymore"); + SetMailboxDiscoveryStatus(eContinue); + } else if (GetMailboxDiscoveryStatus() == eContinueNew) { + if (m_hierarchyNameState == kListingForInfoAndDiscovery && + !adoptedBoxSpec->mAllocatedPathName.IsEmpty() && + !(adoptedBoxSpec->mBoxFlags & kNameSpace)) { + // remember the info here also + nsIMAPMailboxInfo* mb = + new nsIMAPMailboxInfo(adoptedBoxSpec->mAllocatedPathName, + adoptedBoxSpec->mHierarchySeparator); + m_listedMailboxList.AppendElement(mb); + } + SetMailboxDiscoveryStatus(eContinue); + } + } + } + } break; + case kDeleteSubFoldersInProgress: { + NS_ASSERTION(m_deletableChildren, "Oops .. null m_deletableChildren"); + m_deletableChildren->AppendElement(adoptedBoxSpec->mAllocatedPathName); + } break; + case kListingForInfoOnly: { + // UpdateProgressWindowForUpgrade(adoptedBoxSpec->allocatedPathName); + ProgressEventFunctionUsingNameWithString( + "imapDiscoveringMailbox", adoptedBoxSpec->mAllocatedPathName.get()); + nsIMAPMailboxInfo* mb = + new nsIMAPMailboxInfo(adoptedBoxSpec->mAllocatedPathName, + adoptedBoxSpec->mHierarchySeparator); + m_listedMailboxList.AppendElement(mb); + } break; + case kDiscoveringNamespacesOnly: { + } break; + default: + NS_ASSERTION(false, "we aren't supposed to be here"); + break; + } +} + +void nsImapProtocol::AlertUserEventUsingName(const char* aMessageName) { + if (m_imapServerSink) { + bool suppressErrorMsg = false; + + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(m_runningUrl); + if (mailnewsUrl) mailnewsUrl->GetSuppressErrorMsgs(&suppressErrorMsg); + + if (!suppressErrorMsg) + m_imapServerSink->FEAlertWithName(aMessageName, mailnewsUrl); + } +} + +void nsImapProtocol::AlertUserEvent(const char* message) { + if (m_imapServerSink) { + bool suppressErrorMsg = false; + + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(m_runningUrl); + if (mailnewsUrl) mailnewsUrl->GetSuppressErrorMsgs(&suppressErrorMsg); + + if (!suppressErrorMsg) + m_imapServerSink->FEAlert(NS_ConvertASCIItoUTF16(message), mailnewsUrl); + } +} + +void nsImapProtocol::AlertUserEventFromServer(const char* aServerEvent, + bool aForIdle) { + if (aServerEvent) { + // If called due to BAD/NO imap IDLE response, the server sink and running + // url are typically null when IDLE command is sent. So use the stored + // latest values for these so that the error alert notification occurs. + if (aForIdle && !m_imapServerSink && !m_runningUrl && + m_imapServerSinkLatest) { + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = + do_QueryInterface(m_runningUrlLatest); + m_imapServerSinkLatest->FEAlertFromServer( + nsDependentCString(aServerEvent), mailnewsUrl); + } else if (m_imapServerSink) { + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(m_runningUrl); + m_imapServerSink->FEAlertFromServer(nsDependentCString(aServerEvent), + mailnewsUrl); + } + } +} + +void nsImapProtocol::ResetProgressInfo() { + m_lastProgressTime = 0; + m_lastPercent = -1; + m_lastProgressStringName.Truncate(); +} + +void nsImapProtocol::SetProgressString(uint32_t aStringIndex) { + m_stringIndex = aStringIndex; + MOZ_ASSERT(m_stringIndex <= IMAP_EMPTY_STRING_INDEX); + switch (m_stringIndex) { + case IMAP_HEADERS_STRING_INDEX: + m_progressStringName = "imapReceivingMessageHeaders3"; + break; + case IMAP_MESSAGES_STRING_INDEX: + m_progressStringName = "imapFolderReceivingMessageOf3"; + break; + case IMAP_FLAGS_STRING_INDEX: + m_progressStringName = "imapReceivingMessageFlags3"; + break; + case IMAP_EMPTY_STRING_INDEX: + default: + break; + } +} + +void nsImapProtocol::ShowProgress() { + if (m_imapServerSink && (m_stringIndex != IMAP_EMPTY_STRING_INDEX)) { + nsString progressString; + const char* mailboxName = GetServerStateParser().GetSelectedMailboxName(); + nsString unicodeMailboxName; + nsresult rv = CopyFolderNameToUTF16(nsDependentCString(mailboxName), + unicodeMailboxName); + NS_ENSURE_SUCCESS_VOID(rv); + + int32_t progressCurrentNumber = ++m_progressCurrentNumber[m_stringIndex]; + + PercentProgressUpdateEvent(m_progressStringName, unicodeMailboxName, + progressCurrentNumber, m_progressExpectedNumber); + } +} + +void nsImapProtocol::ProgressEventFunctionUsingName(const char* aMsgName) { + if (m_imapMailFolderSink && !m_lastProgressStringName.Equals(aMsgName)) { + m_imapMailFolderSink->ProgressStatusString(this, aMsgName, nullptr); + m_lastProgressStringName.Assign(aMsgName); + // who's going to free this? Does ProgressStatusString complete + // synchronously? + } +} + +void nsImapProtocol::ProgressEventFunctionUsingNameWithString( + const char* aMsgName, const char* aExtraInfo) { + if (m_imapMailFolderSink) { + nsString unicodeStr; + nsresult rv = + CopyFolderNameToUTF16(nsDependentCString(aExtraInfo), unicodeStr); + if (NS_SUCCEEDED(rv)) + m_imapMailFolderSink->ProgressStatusString(this, aMsgName, + unicodeStr.get()); + } +} + +void nsImapProtocol::PercentProgressUpdateEvent(nsACString const& fmtStringName, + nsAString const& mailbox, + int64_t currentProgress, + int64_t maxProgress) { + int64_t nowMS = 0; + int32_t percent = (100 * currentProgress) / maxProgress; + if (percent == m_lastPercent) + return; // hasn't changed, right? So just return. Do we need to clear this + // anywhere? + + if (percent < 100) // always need to do 100% + { + nowMS = PR_IntervalToMilliseconds(PR_IntervalNow()); + if (nowMS - m_lastProgressTime < 750) return; + } + + m_lastPercent = percent; + m_lastProgressTime = nowMS; + + // set our max progress on the running URL + if (m_runningUrl) { + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl(do_QueryInterface(m_runningUrl)); + mailnewsUrl->SetMaxProgress(maxProgress); + } + + if (m_imapMailFolderSink) { + m_imapMailFolderSink->PercentProgress(this, fmtStringName, mailbox, + currentProgress, maxProgress); + } +} + +// imap commands issued by the parser +void nsImapProtocol::Store(const nsCString& messageList, + const char* messageData, bool idsAreUid) { + // turn messageList back into key array and then back into a message id list, + // but use the flag state to handle ranges correctly. + nsCString messageIdList; + nsTArray<nsMsgKey> msgKeys; + if (idsAreUid) ParseUidString(messageList.get(), msgKeys); + + int32_t msgCountLeft = msgKeys.Length(); + uint32_t msgsHandled = 0; + do { + nsCString idString; + + uint32_t msgsToHandle = msgCountLeft; + if (idsAreUid) + AllocateImapUidString(msgKeys.Elements() + msgsHandled, msgsToHandle, + m_flagState, idString); // 20 * 200 + else + idString.Assign(messageList); + + msgsHandled += msgsToHandle; + msgCountLeft -= msgsToHandle; + + IncrementCommandTagNumber(); + const char* formatString; + if (idsAreUid) + formatString = "%s uid store %s %s\015\012"; + else + formatString = "%s store %s %s\015\012"; + + // we might need to close this mailbox after this + m_closeNeededBeforeSelect = + GetDeleteIsMoveToTrash() && (PL_strcasestr(messageData, "\\Deleted")); + + const char* commandTag = GetServerCommandTag(); + int protocolStringSize = PL_strlen(formatString) + messageList.Length() + + PL_strlen(messageData) + PL_strlen(commandTag) + 1; + char* protocolString = (char*)PR_CALLOC(protocolStringSize); + + if (protocolString) { + PR_snprintf(protocolString, // string to create + protocolStringSize, // max size + formatString, // format string + commandTag, // command tag + idString.get(), messageData); + + nsresult rv = SendData(protocolString); + if (NS_SUCCEEDED(rv)) { + m_flagChangeCount++; + ParseIMAPandCheckForNewMail(protocolString); + if (GetServerStateParser().LastCommandSuccessful() && CheckNeeded()) + Check(); + } + PR_Free(protocolString); + } else + HandleMemoryFailure(); + } while (msgCountLeft > 0 && !DeathSignalReceived()); +} + +void nsImapProtocol::IssueUserDefinedMsgCommand(const char* command, + const char* messageList) { + IncrementCommandTagNumber(); + + const char* formatString; + formatString = "%s uid %s %s\015\012"; + + const char* commandTag = GetServerCommandTag(); + int protocolStringSize = PL_strlen(formatString) + PL_strlen(messageList) + + PL_strlen(command) + PL_strlen(commandTag) + 1; + char* protocolString = (char*)PR_CALLOC(protocolStringSize); + + if (protocolString) { + PR_snprintf(protocolString, // string to create + protocolStringSize, // max size + formatString, // format string + commandTag, // command tag + command, messageList); + + nsresult rv = SendData(protocolString); + if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail(protocolString); + PR_Free(protocolString); + } else + HandleMemoryFailure(); +} + +void nsImapProtocol::UidExpunge(const nsCString& messageSet) { + IncrementCommandTagNumber(); + nsCString command(GetServerCommandTag()); + command.AppendLiteral(" uid expunge "); + command.Append(messageSet); + command.Append(CRLF); + nsresult rv = SendData(command.get()); + if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail(); +} + +void nsImapProtocol::Expunge() { + uint32_t aclFlags = 0; + if (GetServerStateParser().ServerHasACLCapability() && m_imapMailFolderSink) + m_imapMailFolderSink->GetAclFlags(&aclFlags); + + if (aclFlags && !(aclFlags & IMAP_ACL_EXPUNGE_FLAG)) return; + ProgressEventFunctionUsingName("imapStatusExpungingMailbox"); + + if (gCheckDeletedBeforeExpunge) { + GetServerStateParser().ResetSearchResultSequence(); + Search("SEARCH DELETED", false, false); + if (GetServerStateParser().LastCommandSuccessful()) { + nsImapSearchResultIterator* search = + GetServerStateParser().CreateSearchResultIterator(); + nsMsgKey key = search->GetNextMessageNumber(); + delete search; + if (key == 0) return; // no deleted messages to expunge (bug 235004) + } + } + + IncrementCommandTagNumber(); + nsAutoCString command(GetServerCommandTag()); + command.AppendLiteral(" expunge" CRLF); + + nsresult rv = SendData(command.get()); + if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail(); +} + +void nsImapProtocol::HandleMemoryFailure() { + PR_CEnterMonitor(this); + // **** jefft fix me!!!!!! ****** + // m_imapThreadIsRunning = false; + // SetConnectionStatus(-1); + PR_CExitMonitor(this); +} + +void nsImapProtocol::HandleCurrentUrlError() { + // This is to handle a move/copy failing, especially because the user + // cancelled the password prompt. + (void)m_runningUrl->GetImapAction(&m_imapAction); + if (m_imapAction == nsIImapUrl::nsImapOfflineToOnlineMove || + m_imapAction == nsIImapUrl::nsImapAppendMsgFromFile || + m_imapAction == nsIImapUrl::nsImapAppendDraftFromFile) { + if (m_imapMailFolderSink) + m_imapMailFolderSink->OnlineCopyCompleted( + this, ImapOnlineCopyStateType::kFailedCopy); + } +} + +void nsImapProtocol::StartTLS() { + IncrementCommandTagNumber(); + nsCString tag(GetServerCommandTag()); + nsCString command(tag); + + command.AppendLiteral(" STARTTLS" CRLF); + nsresult rv = SendData(command.get()); + bool ok = false; + if (NS_SUCCEEDED(rv)) { + nsCString expectOkResponse = tag + " OK "_ns; + char* serverResponse = nullptr; + do { + // This reads and discards lines not starting with "<tag> OK " or + // "<tag> BAD " and exits when when either are found. Otherwise, this + // exits on timeout when all lines in the buffer are read causing + // serverResponse to be set null. Usually just "<tag> OK " is present. + serverResponse = CreateNewLineFromSocket(); + ok = serverResponse && + !PL_strncasecmp(serverResponse, expectOkResponse.get(), + expectOkResponse.Length()); + if (!ok && serverResponse) { + // Check for possible BAD response, e.g., server not STARTTLS capable. + nsCString expectBadResponse = tag + " BAD "_ns; + if (!PL_strncasecmp(serverResponse, expectBadResponse.get(), + expectBadResponse.Length())) { + PR_Free(serverResponse); + break; + } + } + PR_Free(serverResponse); + } while (serverResponse && !ok); + } + // ok == false implies a "<tag> BAD " response or time out on socket read. + // It could also be due to failure on SendData() above. + GetServerStateParser().SetCommandFailed(!ok); +} + +void nsImapProtocol::Capability() { + ProgressEventFunctionUsingName("imapStatusCheckCompat"); + IncrementCommandTagNumber(); + nsCString command(GetServerCommandTag()); + + command.AppendLiteral(" capability" CRLF); + + nsresult rv = SendData(command.get()); + if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail(); +} + +void nsImapProtocol::ID() { + if (!gAppName[0]) return; + IncrementCommandTagNumber(); + nsCString command(GetServerCommandTag()); + command.AppendLiteral(" ID (\"name\" \""); + command.Append(gAppName); + command.AppendLiteral("\" \"version\" \""); + command.Append(gAppVersion); + command.AppendLiteral("\")" CRLF); + + nsresult rv = SendData(command.get()); + if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail(); +} + +void nsImapProtocol::EnableUTF8Accept() { + IncrementCommandTagNumber(); + nsCString command(GetServerCommandTag()); + command.AppendLiteral(" ENABLE UTF8=ACCEPT" CRLF); + + nsresult rv = SendData(command.get()); + if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail(); +} + +void nsImapProtocol::EnableCondStore() { + IncrementCommandTagNumber(); + nsCString command(GetServerCommandTag()); + + command.AppendLiteral(" ENABLE CONDSTORE" CRLF); + + nsresult rv = SendData(command.get()); + if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail(); +} + +void nsImapProtocol::StartCompressDeflate() { + // only issue a compression request if we haven't already + if (!TestFlag(IMAP_ISSUED_COMPRESS_REQUEST)) { + SetFlag(IMAP_ISSUED_COMPRESS_REQUEST); + IncrementCommandTagNumber(); + nsCString command(GetServerCommandTag()); + + command.AppendLiteral(" COMPRESS DEFLATE" CRLF); + + nsresult rv = SendData(command.get()); + if (NS_SUCCEEDED(rv)) { + ParseIMAPandCheckForNewMail(); + if (GetServerStateParser().LastCommandSuccessful()) { + rv = BeginCompressing(); + if (NS_FAILED(rv)) { + Log("CompressDeflate", nullptr, "failed to enable compression"); + // we can't use this connection without compression any more, so die + ClearFlag(IMAP_CONNECTION_IS_OPEN); + TellThreadToDie(); + SetConnectionStatus(rv); + return; + } + } + } + } +} + +nsresult nsImapProtocol::BeginCompressing() { + // wrap the streams in compression layers that compress or decompress + // all traffic. + RefPtr<nsMsgCompressIStream> new_in = new nsMsgCompressIStream(); + if (!new_in) return NS_ERROR_OUT_OF_MEMORY; + + nsresult rv = new_in->InitInputStream(m_inputStream); + NS_ENSURE_SUCCESS(rv, rv); + + m_inputStream = new_in; + + RefPtr<nsMsgCompressOStream> new_out = new nsMsgCompressOStream(); + if (!new_out) return NS_ERROR_OUT_OF_MEMORY; + + rv = new_out->InitOutputStream(m_outputStream); + NS_ENSURE_SUCCESS(rv, rv); + + m_outputStream = new_out; + return rv; +} + +void nsImapProtocol::Language() { + // only issue the language request if we haven't done so already... + if (!TestFlag(IMAP_ISSUED_LANGUAGE_REQUEST)) { + SetFlag(IMAP_ISSUED_LANGUAGE_REQUEST); + ProgressEventFunctionUsingName("imapStatusCheckCompat"); + IncrementCommandTagNumber(); + nsCString command(GetServerCommandTag()); + + // extract the desired language attribute from prefs + nsresult rv = NS_OK; + + // we need to parse out the first language out of this comma separated + // list.... i.e if we have en,ja we only want to send en to the server. + if (mAcceptLanguages.get()) { + nsAutoCString extractedLanguage; + LossyCopyUTF16toASCII(mAcceptLanguages, extractedLanguage); + int32_t pos = extractedLanguage.FindChar(','); + if (pos > 0) // we have a comma separated list of languages... + extractedLanguage.SetLength(pos); // truncate everything after the + // first comma (including the comma) + + if (extractedLanguage.IsEmpty()) return; + + command.AppendLiteral(" LANGUAGE "); + command.Append(extractedLanguage); + command.Append(CRLF); + + rv = SendData(command.get()); + if (NS_SUCCEEDED(rv)) + ParseIMAPandCheckForNewMail(nullptr, true /* ignore bad or no result from the server for this command */); + } + } +} + +void nsImapProtocol::EscapeUserNamePasswordString(const char* strToEscape, + nsCString* resultStr) { + if (strToEscape) { + uint32_t i = 0; + uint32_t escapeStrlen = strlen(strToEscape); + for (i = 0; i < escapeStrlen; i++) { + if (strToEscape[i] == '\\' || strToEscape[i] == '\"') { + resultStr->Append('\\'); + } + resultStr->Append(strToEscape[i]); + } + } +} + +void nsImapProtocol::InitPrefAuthMethods(int32_t authMethodPrefValue, + nsIMsgIncomingServer* aServer) { + // for m_prefAuthMethods, using the same flags as server capabilities. + switch (authMethodPrefValue) { + case nsMsgAuthMethod::none: + m_prefAuthMethods = kHasAuthNoneCapability; + break; + case nsMsgAuthMethod::old: + m_prefAuthMethods = kHasAuthOldLoginCapability; + break; + case nsMsgAuthMethod::passwordCleartext: + m_prefAuthMethods = kHasAuthOldLoginCapability | kHasAuthLoginCapability | + kHasAuthPlainCapability; + break; + case nsMsgAuthMethod::passwordEncrypted: + m_prefAuthMethods = kHasCRAMCapability; + break; + case nsMsgAuthMethod::NTLM: + m_prefAuthMethods = kHasAuthNTLMCapability | kHasAuthMSNCapability; + break; + case nsMsgAuthMethod::GSSAPI: + m_prefAuthMethods = kHasAuthGssApiCapability; + break; + case nsMsgAuthMethod::External: + m_prefAuthMethods = kHasAuthExternalCapability; + break; + case nsMsgAuthMethod::secure: + m_prefAuthMethods = kHasCRAMCapability | kHasAuthGssApiCapability | + kHasAuthNTLMCapability | kHasAuthMSNCapability; + break; + case nsMsgAuthMethod::OAuth2: + m_prefAuthMethods = kHasXOAuth2Capability; + break; + default: + NS_ASSERTION(false, "IMAP: authMethod pref invalid"); + MOZ_LOG(IMAP, LogLevel::Error, + ("IMAP: bad pref authMethod = %d", authMethodPrefValue)); + // fall to any + [[fallthrough]]; + case nsMsgAuthMethod::anything: + m_prefAuthMethods = kHasAuthOldLoginCapability | kHasAuthLoginCapability | + kHasAuthPlainCapability | kHasCRAMCapability | + kHasAuthGssApiCapability | kHasAuthNTLMCapability | + kHasAuthMSNCapability | kHasAuthExternalCapability | + kHasXOAuth2Capability; + break; + } + + if (m_prefAuthMethods & kHasXOAuth2Capability) { + mOAuth2Support = new mozilla::mailnews::OAuth2ThreadHelper(aServer); + if (!mOAuth2Support || !mOAuth2Support->SupportsOAuth2()) { + // Disable OAuth2 support if we don't have the prefs installed. + m_prefAuthMethods &= ~kHasXOAuth2Capability; + mOAuth2Support = nullptr; + MOZ_LOG(IMAP, LogLevel::Warning, + ("IMAP: no OAuth2 support for this server.")); + } + } +} + +/** + * Changes m_currentAuthMethod to pick the best remaining one + * which is allowed by server and prefs and not marked failed. + * The order of preference and trying of auth methods is encoded here. + */ +nsresult nsImapProtocol::ChooseAuthMethod() { + eIMAPCapabilityFlags serverCaps = GetServerStateParser().GetCapabilityFlag(); + eIMAPCapabilityFlags availCaps = + serverCaps & m_prefAuthMethods & ~m_failedAuthMethods; + + MOZ_LOG(IMAP, LogLevel::Debug, + ("IMAP auth: server caps 0x%" PRIx64 ", pref 0x%" PRIx64 + ", failed 0x%" PRIx64 ", avail caps 0x%" PRIx64, + serverCaps, m_prefAuthMethods, m_failedAuthMethods, availCaps)); + // clang-format off + MOZ_LOG(IMAP, LogLevel::Debug, + ("(GSSAPI = 0x%" PRIx64 ", CRAM = 0x%" PRIx64 ", NTLM = 0x%" PRIx64 + ", MSN = 0x%" PRIx64 ", PLAIN = 0x%" PRIx64 ", LOGIN = 0x%" PRIx64 + ", old-style IMAP login = 0x%" PRIx64 + ", auth external IMAP login = 0x%" PRIx64 ", OAUTH2 = 0x%" PRIx64 ")", + kHasAuthGssApiCapability, kHasCRAMCapability, kHasAuthNTLMCapability, + kHasAuthMSNCapability, kHasAuthPlainCapability, kHasAuthLoginCapability, + kHasAuthOldLoginCapability, kHasAuthExternalCapability, + kHasXOAuth2Capability)); + // clang-format on + + if (kHasAuthExternalCapability & availCaps) + m_currentAuthMethod = kHasAuthExternalCapability; + else if (kHasAuthGssApiCapability & availCaps) + m_currentAuthMethod = kHasAuthGssApiCapability; + else if (kHasCRAMCapability & availCaps) + m_currentAuthMethod = kHasCRAMCapability; + else if (kHasAuthNTLMCapability & availCaps) + m_currentAuthMethod = kHasAuthNTLMCapability; + else if (kHasAuthMSNCapability & availCaps) + m_currentAuthMethod = kHasAuthMSNCapability; + else if (kHasXOAuth2Capability & availCaps) + m_currentAuthMethod = kHasXOAuth2Capability; + else if (kHasAuthPlainCapability & availCaps) + m_currentAuthMethod = kHasAuthPlainCapability; + else if (kHasAuthLoginCapability & availCaps) + m_currentAuthMethod = kHasAuthLoginCapability; + else if (kHasAuthOldLoginCapability & availCaps) + m_currentAuthMethod = kHasAuthOldLoginCapability; + else { + MOZ_LOG(IMAP, LogLevel::Debug, ("No remaining auth method")); + m_currentAuthMethod = kCapabilityUndefined; + return NS_ERROR_FAILURE; + } + MOZ_LOG(IMAP, LogLevel::Debug, + ("Trying auth method 0x%" PRIx64, m_currentAuthMethod)); + return NS_OK; +} + +void nsImapProtocol::MarkAuthMethodAsFailed( + eIMAPCapabilityFlags failedAuthMethod) { + MOZ_LOG(IMAP, LogLevel::Debug, + ("Marking auth method 0x%" PRIx64 " failed", failedAuthMethod)); + m_failedAuthMethods |= failedAuthMethod; +} + +/** + * Start over, trying all auth methods again + */ +void nsImapProtocol::ResetAuthMethods() { + MOZ_LOG(IMAP, LogLevel::Debug, ("resetting (failed) auth methods")); + m_currentAuthMethod = kCapabilityUndefined; + m_failedAuthMethods = 0; +} + +nsresult nsImapProtocol::SendDataParseIMAPandCheckForNewMail( + const char* aData, const char* aCommand) { + nsresult rv; + bool isResend = false; + while (true) { + // Send authentication string (true: suppress logging the string). + rv = SendData(aData, true); + if (NS_FAILED(rv)) break; + ParseIMAPandCheckForNewMail(aCommand); + if (!GetServerStateParser().WaitingForMoreClientInput()) break; + + // The server is asking for the authentication string again. So we send + // the same string again although we know that it might be rejected again. + // We do that to get a firm authentication failure instead of a resend + // request. That keeps things in order before failing authentication and + // trying another method if capable. + if (isResend) { + rv = NS_ERROR_FAILURE; + break; + } + isResend = true; + } + + return rv; +} + +nsresult nsImapProtocol::ClientID() { + IncrementCommandTagNumber(); + nsCString command(GetServerCommandTag()); + command += " CLIENTID UUID "; + command += m_clientId; + command += CRLF; + nsresult rv = SendDataParseIMAPandCheckForNewMail(command.get(), nullptr); + NS_ENSURE_SUCCESS(rv, rv); + if (!GetServerStateParser().LastCommandSuccessful()) { + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +nsresult nsImapProtocol::AuthLogin(const char* userName, + const nsString& aPassword, + eIMAPCapabilityFlag flag) { + ProgressEventFunctionUsingName("imapStatusSendingAuthLogin"); + IncrementCommandTagNumber(); + + char* currentCommand = nullptr; + nsresult rv; + NS_ConvertUTF16toUTF8 password(aPassword); + MOZ_LOG(IMAP, LogLevel::Debug, + ("IMAP: trying auth method 0x%" PRIx64, m_currentAuthMethod)); + + if (flag & kHasAuthExternalCapability) { + char* base64UserName = PL_Base64Encode(userName, strlen(userName), nullptr); + nsAutoCString command(GetServerCommandTag()); + command.AppendLiteral(" authenticate EXTERNAL "); + command.Append(base64UserName); + command.Append(CRLF); + PR_Free(base64UserName); + rv = SendData(command.get()); + ParseIMAPandCheckForNewMail(); + nsImapServerResponseParser& parser = GetServerStateParser(); + if (parser.LastCommandSuccessful()) return NS_OK; + parser.SetCapabilityFlag(parser.GetCapabilityFlag() & + ~kHasAuthExternalCapability); + } else if (flag & kHasCRAMCapability) { + NS_ENSURE_TRUE(m_imapServerSink, NS_ERROR_NULL_POINTER); + MOZ_LOG(IMAP, LogLevel::Debug, ("MD5 auth")); + // inform the server that we want to begin a CRAM authentication + // procedure... + nsAutoCString command(GetServerCommandTag()); + command.AppendLiteral(" authenticate CRAM-MD5" CRLF); + rv = SendData(command.get()); + NS_ENSURE_SUCCESS(rv, rv); + ParseIMAPandCheckForNewMail(); + if (GetServerStateParser().LastCommandSuccessful()) { + char* digest = nullptr; + char* cramDigest = GetServerStateParser().fAuthChallenge; + char* decodedChallenge = + PL_Base64Decode(cramDigest, strlen(cramDigest), nullptr); + rv = m_imapServerSink->CramMD5Hash(decodedChallenge, password.get(), + &digest); + PR_Free(decodedChallenge); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(digest, NS_ERROR_NULL_POINTER); + // The encoded digest is the hexadecimal representation of + // DIGEST_LENGTH characters, so it will be twice that length. + nsAutoCStringN<2 * DIGEST_LENGTH> encodedDigest; + + for (uint32_t j = 0; j < DIGEST_LENGTH; j++) { + char hexVal[3]; + PR_snprintf(hexVal, 3, "%.2x", 0x0ff & (unsigned short)(digest[j])); + encodedDigest.Append(hexVal); + } + + PR_snprintf(m_dataOutputBuf, OUTPUT_BUFFER_SIZE, "%.255s %s", userName, + encodedDigest.get()); + char* base64Str = + PL_Base64Encode(m_dataOutputBuf, strlen(m_dataOutputBuf), nullptr); + PR_snprintf(m_dataOutputBuf, OUTPUT_BUFFER_SIZE, "%s" CRLF, base64Str); + PR_Free(base64Str); + PR_Free(digest); + rv = SendData(m_dataOutputBuf); + NS_ENSURE_SUCCESS(rv, rv); + ParseIMAPandCheckForNewMail(command.get()); + } + } // if CRAM response was received + else if (flag & kHasAuthGssApiCapability) { + MOZ_LOG(IMAP, LogLevel::Debug, ("MD5 auth")); + + // Only try GSSAPI once - if it fails, its going to be because we don't + // have valid credentials + // MarkAuthMethodAsFailed(kHasAuthGssApiCapability); + + // We do step1 first, so we don't try GSSAPI against a server which + // we can't get credentials for. + nsAutoCString response; + + nsAutoCString service("imap@"); + service.Append(m_hostName); + rv = DoGSSAPIStep1(service, userName, response); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString command(GetServerCommandTag()); + command.AppendLiteral(" authenticate GSSAPI" CRLF); + rv = SendData(command.get()); + NS_ENSURE_SUCCESS(rv, rv); + + ParseIMAPandCheckForNewMail("AUTH GSSAPI"); + if (GetServerStateParser().LastCommandSuccessful()) { + response += CRLF; + rv = SendData(response.get()); + NS_ENSURE_SUCCESS(rv, rv); + ParseIMAPandCheckForNewMail(command.get()); + nsresult gssrv = NS_OK; + + while (GetServerStateParser().LastCommandSuccessful() && + NS_SUCCEEDED(gssrv) && gssrv != NS_SUCCESS_AUTH_FINISHED) { + nsCString challengeStr(GetServerStateParser().fAuthChallenge); + gssrv = DoGSSAPIStep2(challengeStr, response); + if (NS_SUCCEEDED(gssrv)) { + response += CRLF; + rv = SendData(response.get()); + } else + rv = SendData("*" CRLF); + + NS_ENSURE_SUCCESS(rv, rv); + ParseIMAPandCheckForNewMail(command.get()); + } + // TODO: whether it worked or not is shown by LastCommandSuccessful(), not + // gssrv, right? + } + } else if (flag & (kHasAuthNTLMCapability | kHasAuthMSNCapability)) { + MOZ_LOG(IMAP, LogLevel::Debug, ("NTLM auth")); + nsAutoCString command(GetServerCommandTag()); + command.Append((flag & kHasAuthNTLMCapability) ? " authenticate NTLM" CRLF + : " authenticate MSN" CRLF); + rv = SendData(command.get()); + ParseIMAPandCheckForNewMail( + "AUTH NTLM"); // this just waits for ntlm step 1 + if (GetServerStateParser().LastCommandSuccessful()) { + nsAutoCString cmd; + rv = DoNtlmStep1(nsDependentCString(userName), aPassword, cmd); + NS_ENSURE_SUCCESS(rv, rv); + cmd += CRLF; + rv = SendData(cmd.get()); + NS_ENSURE_SUCCESS(rv, rv); + ParseIMAPandCheckForNewMail(command.get()); + if (GetServerStateParser().LastCommandSuccessful()) { + nsCString challengeStr(GetServerStateParser().fAuthChallenge); + nsCString response; + rv = DoNtlmStep2(challengeStr, response); + NS_ENSURE_SUCCESS(rv, rv); + response += CRLF; + rv = SendData(response.get()); + ParseIMAPandCheckForNewMail(command.get()); + } + } + } else if (flag & kHasAuthPlainCapability) { + MOZ_LOG(IMAP, LogLevel::Debug, ("PLAIN auth")); + PR_snprintf(m_dataOutputBuf, OUTPUT_BUFFER_SIZE, + "%s authenticate PLAIN" CRLF, GetServerCommandTag()); + rv = SendData(m_dataOutputBuf); + NS_ENSURE_SUCCESS(rv, rv); + currentCommand = PL_strdup( + m_dataOutputBuf); /* StrAllocCopy(currentCommand, GetOutputBuffer()); */ + ParseIMAPandCheckForNewMail(); + if (GetServerStateParser().LastCommandSuccessful()) { + // RFC 4616 + char plain_string[513]; + memset(plain_string, 0, 513); + PR_snprintf(&plain_string[1], 256, "%.255s", userName); + uint32_t len = std::min<uint32_t>(PL_strlen(userName), 255u) + + 2; // We include two <NUL> characters. + PR_snprintf(&plain_string[len], 256, "%.255s", password.get()); + len += std::min<uint32_t>(password.Length(), 255u); + char* base64Str = PL_Base64Encode(plain_string, len, nullptr); + PR_snprintf(m_dataOutputBuf, OUTPUT_BUFFER_SIZE, "%s" CRLF, base64Str); + PR_Free(base64Str); + + rv = SendDataParseIMAPandCheckForNewMail(m_dataOutputBuf, currentCommand); + } // if the last command succeeded + } // if auth plain capability + else if (flag & kHasAuthLoginCapability) { + MOZ_LOG(IMAP, LogLevel::Debug, ("LOGIN auth")); + PR_snprintf(m_dataOutputBuf, OUTPUT_BUFFER_SIZE, + "%s authenticate LOGIN" CRLF, GetServerCommandTag()); + rv = SendData(m_dataOutputBuf); + NS_ENSURE_SUCCESS(rv, rv); + currentCommand = PL_strdup(m_dataOutputBuf); + ParseIMAPandCheckForNewMail(); + + if (GetServerStateParser().LastCommandSuccessful()) { + char* base64Str = PL_Base64Encode( + userName, std::min<uint32_t>(PL_strlen(userName), 255u), nullptr); + PR_snprintf(m_dataOutputBuf, OUTPUT_BUFFER_SIZE, "%s" CRLF, base64Str); + PR_Free(base64Str); + rv = SendData(m_dataOutputBuf, true /* suppress logging */); + if (NS_SUCCEEDED(rv)) { + ParseIMAPandCheckForNewMail(currentCommand); + if (GetServerStateParser().LastCommandSuccessful()) { + base64Str = PL_Base64Encode( + password.get(), std::min<uint32_t>(password.Length(), 255u), + nullptr); + PR_snprintf(m_dataOutputBuf, OUTPUT_BUFFER_SIZE, "%s" CRLF, + base64Str); + PR_Free(base64Str); + rv = SendData(m_dataOutputBuf, true /* suppress logging */); + if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail(currentCommand); + } // if last command successful + } // if last command successful + } // if last command successful + } // if has auth login capability + else if (flag & kHasAuthOldLoginCapability) { + MOZ_LOG(IMAP, LogLevel::Debug, ("old-style auth")); + ProgressEventFunctionUsingName("imapStatusSendingLogin"); + IncrementCommandTagNumber(); + nsCString command(GetServerCommandTag()); + nsAutoCString escapedUserName; + command.AppendLiteral(" login \""); + EscapeUserNamePasswordString(userName, &escapedUserName); + command.Append(escapedUserName); + command.AppendLiteral("\" \""); + + // if the password contains a \, login will fail + // turn foo\bar into foo\\bar + nsAutoCString correctedPassword; + // We're assuming old style login doesn't want UTF-8 + EscapeUserNamePasswordString(NS_LossyConvertUTF16toASCII(aPassword).get(), + &correctedPassword); + command.Append(correctedPassword); + command.AppendLiteral("\"" CRLF); + rv = SendData(command.get(), true /* suppress logging */); + NS_ENSURE_SUCCESS(rv, rv); + ParseIMAPandCheckForNewMail(); + } else if (flag & kHasXOAuth2Capability) { + MOZ_LOG(IMAP, LogLevel::Debug, ("XOAUTH2 auth")); + + // Get the XOAuth2 base64 string. + NS_ASSERTION(mOAuth2Support, + "What are we doing here without OAuth2 helper?"); + if (!mOAuth2Support) return NS_ERROR_UNEXPECTED; + nsAutoCString base64Str; + mOAuth2Support->GetXOAuth2String(base64Str); + mOAuth2Support = nullptr; // Its purpose has been served. + if (base64Str.IsEmpty()) { + MOZ_LOG(IMAP, LogLevel::Debug, ("OAuth2 failed")); + return NS_ERROR_FAILURE; + } + + // Send the data on the network. + nsAutoCString command(GetServerCommandTag()); + command += " AUTHENTICATE XOAUTH2 "; + command += base64Str; + command += CRLF; + + rv = SendDataParseIMAPandCheckForNewMail(command.get(), nullptr); + } else if (flag & kHasAuthNoneCapability) { + // TODO What to do? "login <username>" like POP? + return NS_ERROR_NOT_IMPLEMENTED; + } else { + MOZ_LOG(IMAP, LogLevel::Error, ("flags param has no auth scheme selected")); + return NS_ERROR_ILLEGAL_VALUE; + } + + PR_Free(currentCommand); + NS_ENSURE_SUCCESS(rv, rv); + return GetServerStateParser().LastCommandSuccessful() ? NS_OK + : NS_ERROR_FAILURE; +} + +void nsImapProtocol::OnLSubFolders() { + // **** use to find out whether Drafts, Sent, & Templates folder + // exists or not even the user didn't subscribe to it + char* mailboxName = OnCreateServerSourceFolderPathString(); + if (mailboxName) { + ProgressEventFunctionUsingName("imapStatusLookingForMailbox"); + IncrementCommandTagNumber(); + PR_snprintf(m_dataOutputBuf, OUTPUT_BUFFER_SIZE, "%s list \"\" \"%s\"" CRLF, + GetServerCommandTag(), mailboxName); + nsresult rv = SendData(m_dataOutputBuf); + if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail(); + PR_Free(mailboxName); + } else { + HandleMemoryFailure(); + } +} + +void nsImapProtocol::OnAppendMsgFromFile() { + nsCOMPtr<nsIFile> file; + nsresult rv = NS_OK; + rv = m_runningUrl->GetMsgFile(getter_AddRefs(file)); + if (NS_SUCCEEDED(rv) && file) { + char* mailboxName = OnCreateServerSourceFolderPathString(); + if (mailboxName) { + imapMessageFlagsType flagsToSet = 0; + uint32_t msgFlags = 0; + PRTime date = 0; + nsCString keywords; + if (m_imapMessageSink) + m_imapMessageSink->GetCurMoveCopyMessageInfo(m_runningUrl, &date, + keywords, &msgFlags); + + if (msgFlags & nsMsgMessageFlags::Read) flagsToSet |= kImapMsgSeenFlag; + if (msgFlags & nsMsgMessageFlags::MDNReportSent) + flagsToSet |= kImapMsgMDNSentFlag; + // convert msg flag label (0xE000000) to imap flag label (0x0E00) + if (msgFlags & nsMsgMessageFlags::Labels) + flagsToSet |= (msgFlags & nsMsgMessageFlags::Labels) >> 16; + if (msgFlags & nsMsgMessageFlags::Marked) + flagsToSet |= kImapMsgFlaggedFlag; + if (msgFlags & nsMsgMessageFlags::Replied) + flagsToSet |= kImapMsgAnsweredFlag; + if (msgFlags & nsMsgMessageFlags::Forwarded) + flagsToSet |= kImapMsgForwardedFlag; + + // If the message copied was a draft, flag it as such + nsImapAction imapAction; + rv = m_runningUrl->GetImapAction(&imapAction); + if (NS_SUCCEEDED(rv) && + (imapAction == nsIImapUrl::nsImapAppendDraftFromFile)) + flagsToSet |= kImapMsgDraftFlag; + UploadMessageFromFile(file, mailboxName, date, flagsToSet, keywords); + PR_Free(mailboxName); + } else { + HandleMemoryFailure(); + } + } +} + +void nsImapProtocol::UploadMessageFromFile(nsIFile* file, + const char* mailboxName, PRTime date, + imapMessageFlagsType flags, + nsCString& keywords) { + if (!file || !mailboxName) return; + IncrementCommandTagNumber(); + + int64_t fileSize = 0; + int64_t totalSize; + uint32_t readCount; + char* dataBuffer = nullptr; + nsCString command(GetServerCommandTag()); + nsCString escapedName; + CreateEscapedMailboxName(mailboxName, escapedName); + nsresult rv; + bool urlOk = false; + nsCString flagString; + + nsCOMPtr<nsIInputStream> fileInputStream; + + if (!escapedName.IsEmpty()) { + command.AppendLiteral(" append \""); + command.Append(escapedName); + command.Append('"'); + if (flags || keywords.Length()) { + command.AppendLiteral(" ("); + + if (flags) { + SetupMessageFlagsString(flagString, flags, + GetServerStateParser().SupportsUserFlags()); + command.Append(flagString); + } + if (keywords.Length()) { + if (flags) command.Append(' '); + command.Append(keywords); + } + command.Append(')'); + } + + // date should never be 0, but just in case... + if (date) { + /* Use PR_FormatTimeUSEnglish() to format the date in US English format, + then figure out what our local GMT offset is, and append it (since + PR_FormatTimeUSEnglish() can't do that.) Generate four digit years as + per RFC 1123 (superseding RFC 822.) + */ + char szDateTime[64]; + char dateStr[100]; + PRExplodedTime exploded; + PR_ExplodeTime(date, PR_LocalTimeParameters, &exploded); + PR_FormatTimeUSEnglish(szDateTime, sizeof(szDateTime), + "%d-%b-%Y %H:%M:%S", &exploded); + PRExplodedTime now; + PR_ExplodeTime(PR_Now(), PR_LocalTimeParameters, &now); + int gmtoffset = + (now.tm_params.tp_gmt_offset + now.tm_params.tp_dst_offset) / 60; + PR_snprintf(dateStr, sizeof(dateStr), " \"%s %c%02d%02d\"", szDateTime, + (gmtoffset >= 0 ? '+' : '-'), + ((gmtoffset >= 0 ? gmtoffset : -gmtoffset) / 60), + ((gmtoffset >= 0 ? gmtoffset : -gmtoffset) % 60)); + + command.Append(dateStr); + } + if (m_allowUTF8Accept) + command.AppendLiteral(" UTF8 (~{"); + else + command.AppendLiteral(" {"); + + dataBuffer = (char*)PR_CALLOC(COPY_BUFFER_SIZE + 1); + if (!dataBuffer) goto done; + rv = file->GetFileSize(&fileSize); + NS_ASSERTION(fileSize, "got empty file in UploadMessageFromFile"); + if (NS_FAILED(rv) || !fileSize) goto done; + rv = NS_NewLocalFileInputStream(getter_AddRefs(fileInputStream), file); + if (NS_FAILED(rv) || !fileInputStream) goto done; + command.AppendInt((int32_t)fileSize); + + // Set useLiteralPlus to true if server has capability LITERAL+ and + // LITERAL+ usage is enabled in the config editor, + // i.e., "mail.imap.use_literal_plus" = true. + bool useLiteralPlus = + (GetServerStateParser().GetCapabilityFlag() & kLiteralPlusCapability) && + gUseLiteralPlus; + if (useLiteralPlus) + command.AppendLiteral("+}" CRLF); + else + command.AppendLiteral("}" CRLF); + + rv = SendData(command.get()); + if (NS_FAILED(rv)) goto done; + + if (!useLiteralPlus) { + ParseIMAPandCheckForNewMail(); + if (!GetServerStateParser().LastCommandSuccessful()) goto done; + } + + totalSize = fileSize; + readCount = 0; + while (NS_SUCCEEDED(rv) && totalSize > 0) { + if (DeathSignalReceived()) goto done; + rv = fileInputStream->Read(dataBuffer, COPY_BUFFER_SIZE, &readCount); + if (NS_SUCCEEDED(rv) && !readCount) rv = NS_ERROR_FAILURE; + + if (NS_SUCCEEDED(rv)) { + NS_ASSERTION(readCount <= (uint32_t)totalSize, + "got more bytes than there should be"); + dataBuffer[readCount] = 0; + rv = SendData(dataBuffer); + totalSize -= readCount; + PercentProgressUpdateEvent(""_ns, u""_ns, fileSize - totalSize, + fileSize); + if (!totalSize) { + // The full message has been queued for sending, but the actual send + // is just now starting and can still potentially fail. From this + // point the progress cannot be determined, so just set the progress + // to "indeterminate" so that the user does not see an incorrect 100% + // complete on the progress bar while waiting for the retry dialog to + // appear if the send should fail. + m_lastProgressTime = 0; // Force progress bar update + m_lastPercent = -1; // Force progress bar update + PercentProgressUpdateEvent(""_ns, u""_ns, 0, -1); // Indeterminate + } + } + } // end while appending chunks + + if (NS_SUCCEEDED(rv)) { // complete the append + if (m_allowUTF8Accept) + rv = SendData(")" CRLF); + else + rv = SendData(CRLF); + if (NS_FAILED(rv)) goto done; + + ParseIMAPandCheckForNewMail(command.get()); + if (!GetServerStateParser().LastCommandSuccessful()) goto done; + + // If reached, the append completed without error. No more goto's! + // May still find problems in imap responses below so urlOk may still + // become false. + urlOk = true; + + nsImapAction imapAction; + m_runningUrl->GetImapAction(&imapAction); + + if (imapAction == nsIImapUrl::nsImapAppendDraftFromFile || + imapAction == nsIImapUrl::nsImapAppendMsgFromFile) { + if (GetServerStateParser().GetCapabilityFlag() & kUidplusCapability) { + nsMsgKey newKey = GetServerStateParser().CurrentResponseUID(); + if (m_imapMailFolderSink) + m_imapMailFolderSink->SetAppendMsgUid(newKey, m_runningUrl); + + // Courier imap server seems to have problems with recently + // appended messages. Noop seems to clear its confusion. + if (FolderIsSelected(mailboxName)) Noop(); + + nsCString oldMsgId; + rv = m_runningUrl->GetListOfMessageIds(oldMsgId); + if (NS_SUCCEEDED(rv) && !oldMsgId.IsEmpty()) { + bool idsAreUids = true; + m_runningUrl->MessageIdsAreUids(&idsAreUids); + Store(oldMsgId, "+FLAGS (\\Deleted)", idsAreUids); + UidExpunge(oldMsgId); + } + // Only checks the last imap command in sequence above. + if (!GetServerStateParser().LastCommandSuccessful()) urlOk = false; + } + // for non UIDPLUS servers this code used to check for + // imapAction==nsIImapUrl::nsImapAppendMsgFromFile, which meant we'd get + // into this code whenever sending a message, as well as when copying + // messages to an imap folder from local folders or an other imap + // server. This made sending a message slow when there was a large sent + // folder. I don't believe this code worked anyway. + // *** code me to search for the newly appended message + else if (m_imapMailFolderSink && + imapAction == nsIImapUrl::nsImapAppendDraftFromFile) { + // go to selected state + nsCString messageId; + rv = m_imapMailFolderSink->GetMessageId(m_runningUrl, messageId); + if (NS_SUCCEEDED(rv) && !messageId.IsEmpty()) { + // if the appended to folder isn't selected in the connection, + // select it. + if (!FolderIsSelected(mailboxName)) + SelectMailbox(mailboxName); + else + Noop(); // See if this makes SEARCH work on the newly appended + // msg. + + if (GetServerStateParser().LastCommandSuccessful()) { + command = "SEARCH UNDELETED HEADER Message-ID "; + command.Append(messageId); + + // Clean up result sequence before issuing the cmd. + GetServerStateParser().ResetSearchResultSequence(); + + Search(command.get(), true, false); + if (GetServerStateParser().LastCommandSuccessful()) { + nsMsgKey newkey = nsMsgKey_None; + nsImapSearchResultIterator* searchResult = + GetServerStateParser().CreateSearchResultIterator(); + newkey = searchResult->GetNextMessageNumber(); + delete searchResult; + if (newkey != nsMsgKey_None) + m_imapMailFolderSink->SetAppendMsgUid(newkey, m_runningUrl); + } else + urlOk = false; + } else + urlOk = false; + } + } + } + } + } +done: + // If imap command fails or network goes down, make sure URL sees the failure. + if (!urlOk) GetServerStateParser().SetCommandFailed(true); + + PR_Free(dataBuffer); + if (fileInputStream) fileInputStream->Close(); +} + +// caller must free using PR_Free +char* nsImapProtocol::OnCreateServerSourceFolderPathString() { + char* sourceMailbox = nullptr; + char hierarchyDelimiter = 0; + char onlineDelimiter = 0; + m_runningUrl->GetOnlineSubDirSeparator(&hierarchyDelimiter); + if (m_imapMailFolderSink) + m_imapMailFolderSink->GetOnlineDelimiter(&onlineDelimiter); + + if (onlineDelimiter != kOnlineHierarchySeparatorUnknown && + onlineDelimiter != hierarchyDelimiter) + m_runningUrl->SetOnlineSubDirSeparator(onlineDelimiter); + + m_runningUrl->CreateServerSourceFolderPathString(&sourceMailbox); + + return sourceMailbox; +} + +// caller must free using PR_Free, safe to call from ui thread +char* nsImapProtocol::GetFolderPathString() { + char* sourceMailbox = nullptr; + char onlineSubDirDelimiter = 0; + char hierarchyDelimiter = 0; + nsCOMPtr<nsIMsgFolder> msgFolder; + + m_runningUrl->GetOnlineSubDirSeparator(&onlineSubDirDelimiter); + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(m_runningUrl); + mailnewsUrl->GetFolder(getter_AddRefs(msgFolder)); + if (msgFolder) { + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(msgFolder); + if (imapFolder) { + imapFolder->GetHierarchyDelimiter(&hierarchyDelimiter); + if (hierarchyDelimiter != kOnlineHierarchySeparatorUnknown && + onlineSubDirDelimiter != hierarchyDelimiter) + m_runningUrl->SetOnlineSubDirSeparator(hierarchyDelimiter); + } + } + m_runningUrl->CreateServerSourceFolderPathString(&sourceMailbox); + + return sourceMailbox; +} + +nsresult nsImapProtocol::CreateServerSourceFolderPathString(char** result) { + NS_ENSURE_ARG(result); + *result = OnCreateServerSourceFolderPathString(); + return (*result) ? NS_OK : NS_ERROR_OUT_OF_MEMORY; +} + +// caller must free using PR_Free +char* nsImapProtocol::OnCreateServerDestinationFolderPathString() { + char* destinationMailbox = nullptr; + char hierarchyDelimiter = 0; + char onlineDelimiter = 0; + m_runningUrl->GetOnlineSubDirSeparator(&hierarchyDelimiter); + if (m_imapMailFolderSink) + m_imapMailFolderSink->GetOnlineDelimiter(&onlineDelimiter); + if (onlineDelimiter != kOnlineHierarchySeparatorUnknown && + onlineDelimiter != hierarchyDelimiter) + m_runningUrl->SetOnlineSubDirSeparator(onlineDelimiter); + + m_runningUrl->CreateServerDestinationFolderPathString(&destinationMailbox); + + return destinationMailbox; +} + +void nsImapProtocol::OnCreateFolder(const char* aSourceMailbox) { + bool created = CreateMailboxRespectingSubscriptions(aSourceMailbox); + if (created) { + m_hierarchyNameState = kListingForCreate; + nsCString mailboxWODelim(aSourceMailbox); + RemoveHierarchyDelimiter(mailboxWODelim); + List(mailboxWODelim.get(), false); + m_hierarchyNameState = kNoOperationInProgress; + } else + FolderNotCreated(aSourceMailbox); +} + +void nsImapProtocol::OnEnsureExistsFolder(const char* aSourceMailbox) { + // We need to handle the following edge case where the destination server + // wasn't authenticated when the folder name was encoded in + // `EnsureFolderExists()'. In this case we always get MUTF-7. Here we are + // authenticated and can rely on `m_allowUTF8Accept'. If the folder appears + // to be MUTF-7 and we need UTF-8, we re-encode it. If it's not ASCII, it + // must be already correct in UTF-8. And if it was ASCII to start with, it + // doesn't matter that we MUTF-7 decode and UTF-8 re-encode. + + // `aSourceMailbox' is a path with hierarchy delimiters possibly. To determine + // if the edge case is in effect, we only want to check the leaf node for + // ASCII and this is only necessary when `m_allowUTF8Accept' is true. + + // `fullPath' is modified below if leaf re-encoding is necessary and it must + // be defined here at top level so it stays in scope. + nsAutoCString fullPath(aSourceMailbox); + + if (m_allowUTF8Accept) { + char onlineDirSeparator = kOnlineHierarchySeparatorUnknown; + m_runningUrl->GetOnlineSubDirSeparator(&onlineDirSeparator); + + int32_t leafStart = fullPath.RFindChar(onlineDirSeparator); + nsAutoCString leafName; + if (leafStart == kNotFound) { + // This is a root level mailbox + leafName = fullPath; + fullPath.SetLength(0); + } else { + leafName = Substring(fullPath, leafStart + 1); + fullPath.SetLength(leafStart + 1); + } + + if (NS_IsAscii(leafName.get())) { + MOZ_LOG(IMAP, LogLevel::Debug, + ("re-encode leaf of mailbox %s to UTF-8", aSourceMailbox)); + nsAutoString utf16LeafName; + CopyMUTF7toUTF16(leafName, utf16LeafName); + + // Convert UTF-16 to UTF-8 to create the folder. + nsAutoCString utf8LeafName; + CopyUTF16toUTF8(utf16LeafName, utf8LeafName); + fullPath.Append(utf8LeafName); + aSourceMailbox = fullPath.get(); + MOZ_LOG(IMAP, LogLevel::Debug, + ("re-encoded leaf of mailbox %s to UTF-8", aSourceMailbox)); + } + } + List(aSourceMailbox, false); // how to tell if that succeeded? + // If List() produces OK tagged response and an untagged "* LIST" response + // then the folder exists on the server. For simplicity, just look for + // a general untagged response. + bool folderExists = false; + if (GetServerStateParser().LastCommandSuccessful()) + folderExists = GetServerStateParser().UntaggedResponse(); + + // try converting aSourceMailbox to canonical format + nsImapNamespace* nsForMailbox = nullptr; + m_hostSessionList->GetNamespaceForMailboxForHost( + GetImapServerKey(), aSourceMailbox, nsForMailbox); + // NS_ASSERTION (nsForMailbox, "Oops .. null nsForMailbox"); + + nsCString name; + + if (nsForMailbox) + m_runningUrl->AllocateCanonicalPath( + aSourceMailbox, nsForMailbox->GetDelimiter(), getter_Copies(name)); + else + m_runningUrl->AllocateCanonicalPath( + aSourceMailbox, kOnlineHierarchySeparatorUnknown, getter_Copies(name)); + + // Also check that the folder has been verified to exist in server sink. + bool verifiedExists = false; + if (folderExists && m_imapServerSink) + m_imapServerSink->FolderVerifiedOnline(name, &verifiedExists); + + // If folder exists on server and is verified and known to exists in server + // sink, just subscribe the folder. Otherwise, create a new folder and + // then subscribe and do list again to make sure it's created. + if (folderExists && verifiedExists) { + Subscribe(aSourceMailbox); + } else { + bool created = CreateMailboxRespectingSubscriptions(aSourceMailbox); + if (created) { + List(aSourceMailbox, false); + // Check that we see an untagged response indicating folder now exists. + folderExists = GetServerStateParser().UntaggedResponse(); + } + } + if (!GetServerStateParser().LastCommandSuccessful() || !folderExists) + FolderNotCreated(aSourceMailbox); +} + +void nsImapProtocol::OnSubscribe(const char* sourceMailbox) { + Subscribe(sourceMailbox); +} + +void nsImapProtocol::OnUnsubscribe(const char* sourceMailbox) { + // When we try to auto-unsubscribe from \Noselect folders, + // some servers report errors if we were already unsubscribed + // from them. + bool lastReportingErrors = GetServerStateParser().GetReportingErrors(); + GetServerStateParser().SetReportingErrors(false); + Unsubscribe(sourceMailbox); + GetServerStateParser().SetReportingErrors(lastReportingErrors); +} + +void nsImapProtocol::RefreshACLForFolderIfNecessary(const char* mailboxName) { + if (GetServerStateParser().ServerHasACLCapability()) { + if (!m_folderNeedsACLRefreshed && m_imapMailFolderSink) + m_imapMailFolderSink->GetFolderNeedsACLListed(&m_folderNeedsACLRefreshed); + if (m_folderNeedsACLRefreshed) { + RefreshACLForFolder(mailboxName); + m_folderNeedsACLRefreshed = false; + } + } +} + +void nsImapProtocol::RefreshACLForFolder(const char* mailboxName) { + nsImapNamespace* ns = nullptr; + m_hostSessionList->GetNamespaceForMailboxForHost(GetImapServerKey(), + mailboxName, ns); + if (ns) { + switch (ns->GetType()) { + case kPersonalNamespace: + // It's a personal folder, most likely. + // I find it hard to imagine a server that supports ACL that doesn't + // support NAMESPACE, so most likely we KNOW that this is a personal, + // rather than the default, namespace. + + // First, clear what we have. + ClearAllFolderRights(); + // Now, get the new one. + GetMyRightsForFolder(mailboxName); + if (m_imapMailFolderSink) { + uint32_t aclFlags = 0; + if (NS_SUCCEEDED(m_imapMailFolderSink->GetAclFlags(&aclFlags)) && + aclFlags & IMAP_ACL_ADMINISTER_FLAG) + GetACLForFolder(mailboxName); + } + + // We're all done, refresh the icon/flags for this folder + RefreshFolderACLView(mailboxName, ns); + break; + default: + // We know it's a public folder or other user's folder. + // We only want our own rights + + // First, clear what we have + ClearAllFolderRights(); + // Now, get the new one. + GetMyRightsForFolder(mailboxName); + // We're all done, refresh the icon/flags for this folder + RefreshFolderACLView(mailboxName, ns); + break; + } + } else { + // no namespace, not even default... can this happen? + NS_ASSERTION(false, "couldn't get namespace"); + } +} + +void nsImapProtocol::RefreshFolderACLView(const char* mailboxName, + nsImapNamespace* nsForMailbox) { + nsCString canonicalMailboxName; + + if (nsForMailbox) + m_runningUrl->AllocateCanonicalPath(mailboxName, + nsForMailbox->GetDelimiter(), + getter_Copies(canonicalMailboxName)); + else + m_runningUrl->AllocateCanonicalPath(mailboxName, + kOnlineHierarchySeparatorUnknown, + getter_Copies(canonicalMailboxName)); + + if (m_imapServerSink) + m_imapServerSink->RefreshFolderRights(canonicalMailboxName); +} + +void nsImapProtocol::GetACLForFolder(const char* mailboxName) { + IncrementCommandTagNumber(); + + nsCString command(GetServerCommandTag()); + nsCString escapedName; + CreateEscapedMailboxName(mailboxName, escapedName); + command.AppendLiteral(" getacl \""); + command.Append(escapedName); + command.AppendLiteral("\"" CRLF); + + nsresult rv = SendData(command.get()); + if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail(); +} + +void nsImapProtocol::OnRefreshAllACLs() { + m_hierarchyNameState = kListingForInfoOnly; + nsIMAPMailboxInfo* mb = NULL; + + // This will fill in the list + List("*", true); + + int32_t total = m_listedMailboxList.Length(), count = 0; + GetServerStateParser().SetReportingErrors(false); + for (int32_t i = 0; i < total; i++) { + mb = m_listedMailboxList.ElementAt(i); + if (mb) // paranoia + { + char* onlineName = nullptr; + m_runningUrl->AllocateServerPath( + PromiseFlatCString(mb->GetMailboxName()).get(), mb->GetDelimiter(), + &onlineName); + if (onlineName) { + RefreshACLForFolder(onlineName); + free(onlineName); + } + PercentProgressUpdateEvent(""_ns, u""_ns, count, total); + delete mb; + count++; + } + } + m_listedMailboxList.Clear(); + + PercentProgressUpdateEvent(""_ns, u""_ns, 100, 100); + GetServerStateParser().SetReportingErrors(true); + m_hierarchyNameState = kNoOperationInProgress; +} + +// any state commands +void nsImapProtocol::Logout(bool shuttingDown /* = false */, + bool waitForResponse /* = true */) { + if (!shuttingDown) ProgressEventFunctionUsingName("imapStatusLoggingOut"); + + /****************************************************************** + * due to the undo functionality we cannot issue ImapClose when logout; there + * is no way to do an undo if the message has been permanently expunge + * jt - 07/12/1999 + + bool closeNeeded = GetServerStateParser().GetIMAPstate() == + nsImapServerResponseParser::kFolderSelected; + + if (closeNeeded && GetDeleteIsMoveToTrash()) + ImapClose(); + ********************/ + + IncrementCommandTagNumber(); + + nsCString command(GetServerCommandTag()); + + command.AppendLiteral(" logout" CRLF); + + nsresult rv = SendData(command.get()); + if (m_transport && shuttingDown) + m_transport->SetTimeout(nsISocketTransport::TIMEOUT_READ_WRITE, 5); + // the socket may be dead before we read the response, so drop it. + if (NS_SUCCEEDED(rv) && waitForResponse) ParseIMAPandCheckForNewMail(); +} + +void nsImapProtocol::Noop() { + // ProgressUpdateEvent("noop..."); + IncrementCommandTagNumber(); + nsCString command(GetServerCommandTag()); + + command.AppendLiteral(" noop" CRLF); + + nsresult rv = SendData(command.get()); + if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail(); +} + +void nsImapProtocol::XServerInfo() { + ProgressEventFunctionUsingName("imapGettingServerInfo"); + IncrementCommandTagNumber(); + nsCString command(GetServerCommandTag()); + + command.AppendLiteral( + " XSERVERINFO MANAGEACCOUNTURL MANAGELISTSURL MANAGEFILTERSURL" CRLF); + + nsresult rv = SendData(command.get()); + if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail(); +} + +void nsImapProtocol::Netscape() { + ProgressEventFunctionUsingName("imapGettingServerInfo"); + IncrementCommandTagNumber(); + + nsCString command(GetServerCommandTag()); + + command.AppendLiteral(" netscape" CRLF); + + nsresult rv = SendData(command.get()); + if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail(); +} + +void nsImapProtocol::XMailboxInfo(const char* mailboxName) { + ProgressEventFunctionUsingName("imapGettingMailboxInfo"); + IncrementCommandTagNumber(); + nsCString command(GetServerCommandTag()); + + command.AppendLiteral(" XMAILBOXINFO \""); + command.Append(mailboxName); + command.AppendLiteral("\" MANAGEURL POSTURL" CRLF); + + nsresult rv = SendData(command.get()); + if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail(); +} + +void nsImapProtocol::Namespace() { + IncrementCommandTagNumber(); + + nsCString command(GetServerCommandTag()); + command.AppendLiteral(" namespace" CRLF); + + nsresult rv = SendData(command.get()); + if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail(); +} + +void nsImapProtocol::MailboxData() { + IncrementCommandTagNumber(); + + nsCString command(GetServerCommandTag()); + command.AppendLiteral(" mailboxdata" CRLF); + + nsresult rv = SendData(command.get()); + if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail(); +} + +void nsImapProtocol::GetMyRightsForFolder(const char* mailboxName) { + IncrementCommandTagNumber(); + + nsCString command(GetServerCommandTag()); + nsCString escapedName; + CreateEscapedMailboxName(mailboxName, escapedName); + + if (MailboxIsNoSelectMailbox(escapedName.get())) + return; // Don't issue myrights on Noselect folder + + command.AppendLiteral(" myrights \""); + command.Append(escapedName); + command.AppendLiteral("\"" CRLF); + + nsresult rv = SendData(command.get()); + if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail(); +} + +bool nsImapProtocol::FolderIsSelected(const char* mailboxName) { + return (GetServerStateParser().GetIMAPstate() == + nsImapServerResponseParser::kFolderSelected && + GetServerStateParser().GetSelectedMailboxName() && + PL_strcmp(GetServerStateParser().GetSelectedMailboxName(), + mailboxName) == 0); +} + +void nsImapProtocol::OnStatusForFolder(const char* mailboxName) { + bool untaggedResponse; + // RFC 3501 says: + // "the STATUS command SHOULD NOT be used on the currently selected mailbox", + // so use NOOP instead if mailboxName is the selected folder on this + // connection. + if (FolderIsSelected(mailboxName)) { + Noop(); + // Did untagged responses occur during the NOOP response? If so, this + // indicates new mail or other changes in the mailbox. Handle this like an + // IDLE response which will cause a folder update. + if ((untaggedResponse = GetServerStateParser().UntaggedResponse()) && + m_imapMailFolderSinkSelected) { + Log("OnStatusForFolder", nullptr, + "mailbox change on selected folder during noop"); + m_imapMailFolderSinkSelected->OnNewIdleMessages(); + } + mailboxName = nullptr; // for new_spec below. Obtain SELECTed mailbox data. + } else { + // Imap connection is not in selected state or imap connection is selected + // on a mailbox other than than the mailbox folderstatus URL is requesting + // status for. + untaggedResponse = true; // STATUS always produces an untagged response + IncrementCommandTagNumber(); + + nsAutoCString command(GetServerCommandTag()); + nsCString escapedName; + CreateEscapedMailboxName(mailboxName, escapedName); + + command.AppendLiteral(" STATUS \""); + command.Append(escapedName); + command.AppendLiteral("\" (UIDNEXT MESSAGES UNSEEN RECENT)" CRLF); + + int32_t prevNumMessages = GetServerStateParser().NumberOfMessages(); + nsresult rv = SendData(command.get()); + if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail(); + + // Respond to possible untagged responses EXISTS and RECENT for the SELECTed + // folder. Handle as though this were an IDLE response. Can't check for any + // untagged as for Noop() above since STATUS always produces an untagged + // response for the target mailbox and possibly also for the SELECTed box. + // Of cource, this won't occur if imap connection is not in selected state. + if (GetServerStateParser().GetIMAPstate() == + nsImapServerResponseParser::kFolderSelected && + m_imapMailFolderSinkSelected && + (GetServerStateParser().NumberOfRecentMessages() || + prevNumMessages != GetServerStateParser().NumberOfMessages())) { + Log("OnStatusForFolder", nullptr, + "new mail on selected folder during status"); + m_imapMailFolderSinkSelected->OnNewIdleMessages(); + } + MOZ_ASSERT(m_imapMailFolderSink != m_imapMailFolderSinkSelected); + } + + // Do this to ensure autosync detects changes in server counts and thus + // triggers a full body fetch for when NOOP or STATUS is sent above. + // But if NOOP didn't produce an untagged response, no need to do this. + // Note: For SELECTed noop() above, "folder sink" and "folder sink selected" + // both reference the same folder but are not always equal. So OK to use + // m_imapMailFolderSink below since it is correct for NOOP and STATUS cases. + if (untaggedResponse && GetServerStateParser().LastCommandSuccessful()) { + RefPtr<nsImapMailboxSpec> new_spec = + GetServerStateParser().CreateCurrentMailboxSpec(mailboxName); + if (new_spec && m_imapMailFolderSink) + m_imapMailFolderSink->UpdateImapMailboxStatus(this, new_spec); + } +} + +void nsImapProtocol::OnListFolder(const char* aSourceMailbox, bool aBool) { + List(aSourceMailbox, aBool); +} + +// Returns true if the mailbox is a NoSelect mailbox. +// If we don't know about it, returns false. +bool nsImapProtocol::MailboxIsNoSelectMailbox(const char* mailboxName) { + bool rv = false; + + nsImapNamespace* nsForMailbox = nullptr; + m_hostSessionList->GetNamespaceForMailboxForHost(GetImapServerKey(), + mailboxName, nsForMailbox); + // NS_ASSERTION (nsForMailbox, "Oops .. null nsForMailbox"); + + nsCString name; + + if (nsForMailbox) + m_runningUrl->AllocateCanonicalPath( + mailboxName, nsForMailbox->GetDelimiter(), getter_Copies(name)); + else + m_runningUrl->AllocateCanonicalPath( + mailboxName, kOnlineHierarchySeparatorUnknown, getter_Copies(name)); + + if (name.IsEmpty()) return false; + + NS_ASSERTION(m_imapServerSink, + "unexpected, no imap server sink, see bug #194335"); + if (m_imapServerSink) m_imapServerSink->FolderIsNoSelect(name, &rv); + return rv; +} + +nsresult nsImapProtocol::SetFolderAdminUrl(const char* mailboxName) { + nsresult rv = + NS_ERROR_NULL_POINTER; // if m_imapServerSink is null, rv will be this. + + nsImapNamespace* nsForMailbox = nullptr; + m_hostSessionList->GetNamespaceForMailboxForHost(GetImapServerKey(), + mailboxName, nsForMailbox); + + nsCString name; + + if (nsForMailbox) + m_runningUrl->AllocateCanonicalPath( + mailboxName, nsForMailbox->GetDelimiter(), getter_Copies(name)); + else + m_runningUrl->AllocateCanonicalPath( + mailboxName, kOnlineHierarchySeparatorUnknown, getter_Copies(name)); + + if (m_imapServerSink) + rv = m_imapServerSink->SetFolderAdminURL( + name, nsDependentCString(GetServerStateParser().GetManageFolderUrl())); + return rv; +} +// returns true is the delete succeeded (regardless of subscription changes) +bool nsImapProtocol::DeleteMailboxRespectingSubscriptions( + const char* mailboxName) { + bool rv = true; + if (!MailboxIsNoSelectMailbox(mailboxName)) { + // Only try to delete it if it really exists + DeleteMailbox(mailboxName); + rv = GetServerStateParser().LastCommandSuccessful(); + } + + // We can unsubscribe even if the mailbox doesn't exist. + if (rv && m_autoUnsubscribe) // auto-unsubscribe is on + { + bool reportingErrors = GetServerStateParser().GetReportingErrors(); + GetServerStateParser().SetReportingErrors(false); + Unsubscribe(mailboxName); + GetServerStateParser().SetReportingErrors(reportingErrors); + } + return (rv); +} + +// returns true is the rename succeeded (regardless of subscription changes) +// reallyRename tells us if we should really do the rename (true) or if we +// should just move subscriptions (false) +bool nsImapProtocol::RenameMailboxRespectingSubscriptions( + const char* existingName, const char* newName, bool reallyRename) { + bool rv = true; + if (reallyRename && !MailboxIsNoSelectMailbox(existingName)) { + RenameMailbox(existingName, newName); + rv = GetServerStateParser().LastCommandSuccessful(); + } + + if (rv) { + if (m_autoSubscribe) // if auto-subscribe is on + { + bool reportingErrors = GetServerStateParser().GetReportingErrors(); + GetServerStateParser().SetReportingErrors(false); + Subscribe(newName); + GetServerStateParser().SetReportingErrors(reportingErrors); + } + if (m_autoUnsubscribe) // if auto-unsubscribe is on + { + bool reportingErrors = GetServerStateParser().GetReportingErrors(); + GetServerStateParser().SetReportingErrors(false); + Unsubscribe(existingName); + GetServerStateParser().SetReportingErrors(reportingErrors); + } + } + return (rv); +} + +bool nsImapProtocol::RenameHierarchyByHand(const char* oldParentMailboxName, + const char* newParentMailboxName) { + bool renameSucceeded = true; + char onlineDirSeparator = kOnlineHierarchySeparatorUnknown; + m_deletableChildren = new nsTArray<nsCString>(); + + bool nonHierarchicalRename = + ((GetServerStateParser().GetCapabilityFlag() & kNoHierarchyRename) || + MailboxIsNoSelectMailbox(oldParentMailboxName)); + + m_hierarchyNameState = kDeleteSubFoldersInProgress; + nsImapNamespace* ns = nullptr; + m_hostSessionList->GetNamespaceForMailboxForHost(GetImapServerKey(), + oldParentMailboxName, + ns); // for delimiter + if (!ns) { + if (!PL_strcasecmp(oldParentMailboxName, "INBOX")) + m_hostSessionList->GetDefaultNamespaceOfTypeForHost( + GetImapServerKey(), kPersonalNamespace, ns); + } + if (ns) { + nsCString pattern(oldParentMailboxName); + pattern += ns->GetDelimiter(); + pattern += "*"; + bool isUsingSubscription = false; + m_hostSessionList->GetHostIsUsingSubscription(GetImapServerKey(), + isUsingSubscription); + + if (isUsingSubscription) + Lsub(pattern.get(), false); + else + List(pattern.get(), false); + } + m_hierarchyNameState = kNoOperationInProgress; + + if (GetServerStateParser().LastCommandSuccessful()) + renameSucceeded = // rename this, and move subscriptions + RenameMailboxRespectingSubscriptions(oldParentMailboxName, + newParentMailboxName, true); + + size_t numberToDelete = m_deletableChildren->Length(); + + for (size_t childIndex = 0; (childIndex < numberToDelete) && renameSucceeded; + childIndex++) { + nsCString name = m_deletableChildren->ElementAt(childIndex); + char* serverName = nullptr; + m_runningUrl->AllocateServerPath(name.get(), onlineDirSeparator, + &serverName); + if (!serverName) { + renameSucceeded = false; + break; + } + char* currentName = serverName; + + // calculate the new name and do the rename + nsCString newChildName(newParentMailboxName); + newChildName += (currentName + PL_strlen(oldParentMailboxName)); + // Pass in 'nonHierarchicalRename' to determine if we should really + // rename, or just move subscriptions. + renameSucceeded = RenameMailboxRespectingSubscriptions( + currentName, newChildName.get(), nonHierarchicalRename); + PR_FREEIF(currentName); + } + + delete m_deletableChildren; + m_deletableChildren = nullptr; + + return renameSucceeded; +} + +bool nsImapProtocol::DeleteSubFolders(const char* selectedMailbox, + bool& aDeleteSelf) { + bool deleteSucceeded = true; + m_deletableChildren = new nsTArray<nsCString>; + + bool folderDeleted = false; + + m_hierarchyNameState = kDeleteSubFoldersInProgress; + nsCString pattern(selectedMailbox); + char onlineDirSeparator = kOnlineHierarchySeparatorUnknown; + m_runningUrl->GetOnlineSubDirSeparator(&onlineDirSeparator); + pattern.Append(onlineDirSeparator); + pattern.Append('*'); + + if (!pattern.IsEmpty()) { + List(pattern.get(), false); + } + m_hierarchyNameState = kNoOperationInProgress; + + // this should be a short list so perform a sequential search for the + // longest name mailbox. Deleting the longest first will hopefully + // prevent the server from having problems about deleting parents + // ** jt - why? I don't understand this. + size_t numberToDelete = m_deletableChildren->Length(); + size_t outerIndex, innerIndex; + + // intelligently decide if myself(either plain format or following the + // dir-separator) is in the sub-folder list + bool folderInSubfolderList = false; // For Performance + char* selectedMailboxDir = nullptr; + { + int32_t length = strlen(selectedMailbox); + selectedMailboxDir = (char*)PR_MALLOC(length + 2); + if (selectedMailboxDir) // only do the intelligent test if there is + // enough memory + { + strcpy(selectedMailboxDir, selectedMailbox); + selectedMailboxDir[length] = onlineDirSeparator; + selectedMailboxDir[length + 1] = '\0'; + size_t i; + for (i = 0; i < numberToDelete && !folderInSubfolderList; i++) { + const char* currentName = m_deletableChildren->ElementAt(i).get(); + if (!strcmp(currentName, selectedMailbox) || + !strcmp(currentName, selectedMailboxDir)) + folderInSubfolderList = true; + } + } + } + + deleteSucceeded = GetServerStateParser().LastCommandSuccessful(); + for (outerIndex = 0; (outerIndex < numberToDelete) && deleteSucceeded; + outerIndex++) { + char* longestName = nullptr; + size_t longestIndex = 0; // fix bogus warning by initializing + for (innerIndex = 0; innerIndex < m_deletableChildren->Length(); + innerIndex++) { + const char* currentName = + m_deletableChildren->ElementAt(innerIndex).get(); + if (!longestName || strlen(longestName) < strlen(currentName)) { + longestName = (char*)currentName; + longestIndex = innerIndex; + } + } + if (longestName) { + char* serverName = nullptr; + m_runningUrl->AllocateServerPath(longestName, onlineDirSeparator, + &serverName); + m_deletableChildren->RemoveElementAt(longestIndex); + longestName = serverName; + } + + // some imap servers include the selectedMailbox in the list of + // subfolders of the selectedMailbox. Check for this so we don't + // delete the selectedMailbox (usually the trash and doing an + // empty trash) + // The Cyrus imap server ignores the "INBOX.Trash" constraining + // string passed to the list command. Be defensive and make sure + // we only delete children of the trash + if (longestName && strcmp(selectedMailbox, longestName) && + !strncmp(selectedMailbox, longestName, strlen(selectedMailbox))) { + if (selectedMailboxDir && + !strcmp(selectedMailboxDir, longestName)) // just myself + { + if (aDeleteSelf) { + bool deleted = DeleteMailboxRespectingSubscriptions(longestName); + if (deleted) FolderDeleted(longestName); + folderDeleted = deleted; + deleteSucceeded = deleted; + } + } else { + if (m_imapServerSink) + m_imapServerSink->ResetServerConnection( + nsDependentCString(longestName)); + bool deleted = false; + if (folderInSubfolderList) // for performance + { + nsTArray<nsCString>* pDeletableChildren = m_deletableChildren; + m_deletableChildren = nullptr; + bool folderDeleted = true; + deleted = DeleteSubFolders(longestName, folderDeleted); + // longestName may have subfolder list including itself + if (!folderDeleted) { + if (deleted) + deleted = DeleteMailboxRespectingSubscriptions(longestName); + if (deleted) FolderDeleted(longestName); + } + m_deletableChildren = pDeletableChildren; + } else { + deleted = DeleteMailboxRespectingSubscriptions(longestName); + if (deleted) FolderDeleted(longestName); + } + deleteSucceeded = deleted; + } + } + PR_FREEIF(longestName); + } + + aDeleteSelf = folderDeleted; // feedback if myself is deleted + PR_Free(selectedMailboxDir); + + delete m_deletableChildren; + m_deletableChildren = nullptr; + + return deleteSucceeded; +} + +void nsImapProtocol::FolderDeleted(const char* mailboxName) { + char onlineDelimiter = kOnlineHierarchySeparatorUnknown; + nsCString orphanedMailboxName; + + if (mailboxName) { + m_runningUrl->AllocateCanonicalPath(mailboxName, onlineDelimiter, + getter_Copies(orphanedMailboxName)); + if (m_imapServerSink) + m_imapServerSink->OnlineFolderDelete(orphanedMailboxName); + } +} + +void nsImapProtocol::FolderNotCreated(const char* folderName) { + if (folderName && m_imapServerSink) + m_imapServerSink->OnlineFolderCreateFailed(nsDependentCString(folderName)); +} + +void nsImapProtocol::FolderRenamed(const char* oldName, const char* newName) { + char onlineDelimiter = kOnlineHierarchySeparatorUnknown; + + if ((m_hierarchyNameState == kNoOperationInProgress) || + (m_hierarchyNameState == kListingForInfoAndDiscovery)) + + { + nsCString canonicalOldName, canonicalNewName; + m_runningUrl->AllocateCanonicalPath(oldName, onlineDelimiter, + getter_Copies(canonicalOldName)); + m_runningUrl->AllocateCanonicalPath(newName, onlineDelimiter, + getter_Copies(canonicalNewName)); + AutoProxyReleaseMsgWindow msgWindow; + GetMsgWindow(getter_AddRefs(msgWindow)); + m_imapServerSink->OnlineFolderRename(msgWindow, canonicalOldName, + canonicalNewName); + } +} + +void nsImapProtocol::OnDeleteFolder(const char* sourceMailbox) { + // intelligently delete the folder + bool folderDeleted = true; + bool deleted = DeleteSubFolders(sourceMailbox, folderDeleted); + if (!folderDeleted) { + if (deleted) deleted = DeleteMailboxRespectingSubscriptions(sourceMailbox); + if (deleted) FolderDeleted(sourceMailbox); + } +} + +void nsImapProtocol::RemoveMsgsAndExpunge() { + uint32_t numberOfMessages = GetServerStateParser().NumberOfMessages(); + if (numberOfMessages) { + // Remove all msgs and expunge the folder (ie, compact it). + Store("1:*"_ns, "+FLAGS.SILENT (\\Deleted)", + false); // use sequence #'s + if (GetServerStateParser().LastCommandSuccessful()) Expunge(); + } +} + +void nsImapProtocol::DeleteFolderAndMsgs(const char* sourceMailbox) { + RemoveMsgsAndExpunge(); + if (GetServerStateParser().LastCommandSuccessful()) { + // All msgs are deleted successfully - let's remove the folder itself. + bool reportingErrors = GetServerStateParser().GetReportingErrors(); + GetServerStateParser().SetReportingErrors(false); + OnDeleteFolder(sourceMailbox); + GetServerStateParser().SetReportingErrors(reportingErrors); + } +} + +void nsImapProtocol::OnRenameFolder(const char* sourceMailbox) { + char* destinationMailbox = OnCreateServerDestinationFolderPathString(); + + if (destinationMailbox) { + bool renamed = RenameHierarchyByHand(sourceMailbox, destinationMailbox); + if (renamed) FolderRenamed(sourceMailbox, destinationMailbox); + + // Cause a LIST and re-discovery when slash and/or ^ are escaped. Also + // needed when folder renamed to non-ASCII UTF-8 when UTF8=ACCEPT in + // effect. + m_hierarchyNameState = kListingForCreate; + nsCString mailboxWODelim(destinationMailbox); + RemoveHierarchyDelimiter(mailboxWODelim); + List(mailboxWODelim.get(), false); + m_hierarchyNameState = kNoOperationInProgress; + + PR_Free(destinationMailbox); + } else + HandleMemoryFailure(); +} + +void nsImapProtocol::OnMoveFolderHierarchy(const char* sourceMailbox) { + char* destinationMailbox = OnCreateServerDestinationFolderPathString(); + + if (destinationMailbox) { + nsCString newBoxName; + newBoxName.Adopt(destinationMailbox); + + char onlineDirSeparator = kOnlineHierarchySeparatorUnknown; + m_runningUrl->GetOnlineSubDirSeparator(&onlineDirSeparator); + + nsCString oldBoxName(sourceMailbox); + int32_t leafStart = oldBoxName.RFindChar(onlineDirSeparator); + nsCString leafName; + + if (-1 == leafStart) + leafName = oldBoxName; // this is a root level box + else + leafName = Substring(oldBoxName, leafStart + 1); + + if (!newBoxName.IsEmpty()) newBoxName.Append(onlineDirSeparator); + newBoxName.Append(leafName); + bool renamed = RenameHierarchyByHand(sourceMailbox, newBoxName.get()); + if (renamed) FolderRenamed(sourceMailbox, newBoxName.get()); + } else + HandleMemoryFailure(); +} + +// This is called to do mailbox discovery if discovery not already complete +// for the "host" (i.e., server or account). Discovery still only occurs if +// the imap action is appropriate and if discovery is not in progress due to +// a running "discoverallboxes" URL. +void nsImapProtocol::FindMailboxesIfNecessary() { + // biff should not discover mailboxes + nsImapAction imapAction; + (void)m_runningUrl->GetImapAction(&imapAction); + if ((imapAction != nsIImapUrl::nsImapBiff) && + (imapAction != nsIImapUrl::nsImapVerifylogon) && + (imapAction != nsIImapUrl::nsImapDiscoverAllBoxesUrl) && + (imapAction != nsIImapUrl::nsImapUpgradeToSubscription) && + !GetSubscribingNow()) { + // If discovery in progress, don't kick-off another discovery. + bool discoveryInProgress = false; + m_hostSessionList->GetDiscoveryForHostInProgress(GetImapServerKey(), + discoveryInProgress); + if (!discoveryInProgress) { + m_hostSessionList->SetDiscoveryForHostInProgress(GetImapServerKey(), + true); + DiscoverMailboxList(); + } + } +} + +void nsImapProtocol::DiscoverAllAndSubscribedBoxes() { + // used for subscribe pane + // iterate through all namespaces + uint32_t count = 0; + m_hostSessionList->GetNumberOfNamespacesForHost(GetImapServerKey(), count); + + for (uint32_t i = 0; i < count; i++) { + nsImapNamespace* ns = nullptr; + m_hostSessionList->GetNamespaceNumberForHost(GetImapServerKey(), i, ns); + if (!ns) { + continue; + } + if ((gHideOtherUsersFromList && (ns->GetType() != kOtherUsersNamespace)) || + !gHideOtherUsersFromList) { + const char* prefix = ns->GetPrefix(); + if (prefix) { + nsAutoCString inboxNameWithDelim("INBOX"); + inboxNameWithDelim.Append(ns->GetDelimiter()); + + // Only do it for non-empty namespace prefixes. + if (!gHideUnusedNamespaces && *prefix && + PL_strcasecmp(prefix, inboxNameWithDelim.get())) { + // Explicitly discover each Namespace, just so they're + // there in the subscribe UI + RefPtr<nsImapMailboxSpec> boxSpec = new nsImapMailboxSpec; + boxSpec->mFolderSelected = false; + boxSpec->mHostName.Assign(GetImapHostName()); + boxSpec->mConnection = this; + boxSpec->mFlagState = nullptr; + boxSpec->mDiscoveredFromLsub = true; + boxSpec->mOnlineVerified = true; + boxSpec->mBoxFlags = kNoselect; + boxSpec->mHierarchySeparator = ns->GetDelimiter(); + + m_runningUrl->AllocateCanonicalPath( + ns->GetPrefix(), ns->GetDelimiter(), + getter_Copies(boxSpec->mAllocatedPathName)); + boxSpec->mNamespaceForFolder = ns; + boxSpec->mBoxFlags |= kNameSpace; + + switch (ns->GetType()) { + case kPersonalNamespace: + boxSpec->mBoxFlags |= kPersonalMailbox; + break; + case kPublicNamespace: + boxSpec->mBoxFlags |= kPublicMailbox; + break; + case kOtherUsersNamespace: + boxSpec->mBoxFlags |= kOtherUsersMailbox; + break; + default: // (kUnknownNamespace) + break; + } + + DiscoverMailboxSpec(boxSpec); + } + + nsAutoCString allPattern(prefix); + allPattern += '*'; + + if (!m_imapServerSink) return; + + m_imapServerSink->SetServerDoingLsub(true); + Lsub(allPattern.get(), true); // LSUB all the subscribed + + m_imapServerSink->SetServerDoingLsub(false); + List(allPattern.get(), true); // LIST all folders + } + } + } +} + +// DiscoverMailboxList() is used to actually do the discovery of folders +// for a host. This is used both when we initially start up (and re-sync) +// and also when the user manually requests a re-sync, by collapsing and +// expanding a host in the folder pane. This is not used for the subscribe +// pane. +// DiscoverMailboxList() also gets the ACLs for each newly discovered folder +void nsImapProtocol::DiscoverMailboxList() { + bool usingSubscription = false; + + m_hostSessionList->GetHostIsUsingSubscription(GetImapServerKey(), + usingSubscription); + // Pretend that the Trash folder doesn't exist, so we will rediscover it if we + // need to. + m_hostSessionList->SetOnlineTrashFolderExistsForHost(GetImapServerKey(), + false); + + // should we check a pref here, to be able to turn off XList? + bool hasXLIST = + GetServerStateParser().GetCapabilityFlag() & kHasXListCapability; + if (hasXLIST && usingSubscription) { + m_hierarchyNameState = kXListing; + nsAutoCString pattern("%"); + List("%", true, true); + // We list the first and second levels since special folders are unlikely + // to be more than 2 levels deep. + char separator = 0; + m_runningUrl->GetOnlineSubDirSeparator(&separator); + pattern.Append(separator); + pattern += '%'; + List(pattern.get(), true, true); + } + + SetMailboxDiscoveryStatus(eContinue); + if (GetServerStateParser().ServerHasACLCapability()) + m_hierarchyNameState = kListingForInfoAndDiscovery; + else + m_hierarchyNameState = kNoOperationInProgress; + + // iterate through all namespaces and LSUB them. + uint32_t count = 0; + m_hostSessionList->GetNumberOfNamespacesForHost(GetImapServerKey(), count); + for (uint32_t i = 0; i < count; i++) { + nsImapNamespace* ns = nullptr; + m_hostSessionList->GetNamespaceNumberForHost(GetImapServerKey(), i, ns); + if (ns) { + const char* prefix = ns->GetPrefix(); + if (prefix) { + nsAutoCString inboxNameWithDelim("INBOX"); + inboxNameWithDelim.Append(ns->GetDelimiter()); + + // static bool gHideUnusedNamespaces = true; + // mscott -> WARNING!!! i where are we going to get this + // global variable for unused name spaces from??? + // dmb - we should get this from a per-host preference, + // I'd say. But for now, just make it true. + // Only do it for non-empty namespace prefixes, and for non-INBOX prefix + if (!gHideUnusedNamespaces && *prefix && + PL_strcasecmp(prefix, inboxNameWithDelim.get())) { + // Explicitly discover each Namespace, so that we can + // create subfolders of them, + RefPtr<nsImapMailboxSpec> boxSpec = new nsImapMailboxSpec; + boxSpec->mFolderSelected = false; + boxSpec->mHostName = GetImapHostName(); + boxSpec->mConnection = this; + boxSpec->mFlagState = nullptr; + boxSpec->mDiscoveredFromLsub = true; + boxSpec->mOnlineVerified = true; + boxSpec->mBoxFlags = kNoselect; + boxSpec->mHierarchySeparator = ns->GetDelimiter(); + // Until |AllocateCanonicalPath()| gets updated: + m_runningUrl->AllocateCanonicalPath( + ns->GetPrefix(), ns->GetDelimiter(), + getter_Copies(boxSpec->mAllocatedPathName)); + boxSpec->mNamespaceForFolder = ns; + boxSpec->mBoxFlags |= kNameSpace; + + switch (ns->GetType()) { + case kPersonalNamespace: + boxSpec->mBoxFlags |= kPersonalMailbox; + break; + case kPublicNamespace: + boxSpec->mBoxFlags |= kPublicMailbox; + break; + case kOtherUsersNamespace: + boxSpec->mBoxFlags |= kOtherUsersMailbox; + break; + default: // (kUnknownNamespace) + break; + } + + DiscoverMailboxSpec(boxSpec); + } + + // now do the folders within this namespace + nsCString pattern; + nsCString pattern2; + if (usingSubscription) { + pattern.Append(prefix); + pattern.Append('*'); + } else { + pattern.Append(prefix); + pattern.Append('%'); // mscott just need one percent right? + // pattern = PR_smprintf("%s%%", prefix); + char delimiter = ns->GetDelimiter(); + if (delimiter) { + // delimiter might be NIL, in which case there's no hierarchy anyway + pattern2 = prefix; + pattern2 += "%"; + pattern2 += delimiter; + pattern2 += "%"; + // pattern2 = PR_smprintf("%s%%%c%%", prefix, delimiter); + } + } + // Note: It is important to make sure we are respecting the + // server_sub_directory preference when calling List and Lsub (2nd arg = + // true), otherwise we end up with performance issues or even crashes + // when connecting to servers that expose the users entire home + // directory (like UW-IMAP). + if (usingSubscription) { // && !GetSubscribingNow()) should never get + // here from subscribe pane + if (GetServerStateParser().GetCapabilityFlag() & + kHasListExtendedCapability) + Lsub(pattern.get(), true); // do LIST (SUBSCRIBED) + else { + // store mailbox flags from LIST + EMailboxHierarchyNameState currentState = m_hierarchyNameState; + m_hierarchyNameState = kListingForFolderFlags; + List(pattern.get(), true); + m_hierarchyNameState = currentState; + // then do LSUB using stored flags + Lsub(pattern.get(), true); + m_standardListMailboxes.Clear(); + } + } else { + List(pattern.get(), true, hasXLIST); + List(pattern2.get(), true, hasXLIST); + } + } + } + } + + // explicitly LIST the INBOX if (a) we're not using subscription, or (b) we + // are using subscription and the user wants us to always show the INBOX. + bool listInboxForHost = false; + m_hostSessionList->GetShouldAlwaysListInboxForHost(GetImapServerKey(), + listInboxForHost); + if (!usingSubscription || listInboxForHost) List("INBOX", true); + + m_hierarchyNameState = kNoOperationInProgress; + + MailboxDiscoveryFinished(); + + // Get the ACLs for newly discovered folders + if (GetServerStateParser().ServerHasACLCapability()) { + int32_t total = m_listedMailboxList.Length(), cnt = 0; + // Let's not turn this off here, since we don't turn it on after + // GetServerStateParser().SetReportingErrors(false); + if (total) { + ProgressEventFunctionUsingName("imapGettingACLForFolder"); + nsIMAPMailboxInfo* mb = nullptr; + do { + if (m_listedMailboxList.Length() == 0) break; + + mb = m_listedMailboxList[0]; // get top element + m_listedMailboxList.RemoveElementAt( + 0); // XP_ListRemoveTopObject(fListedMailboxList); + if (mb) { + if (FolderNeedsACLInitialized( + PromiseFlatCString(mb->GetMailboxName()).get())) { + char* onlineName = nullptr; + m_runningUrl->AllocateServerPath( + PromiseFlatCString(mb->GetMailboxName()).get(), + mb->GetDelimiter(), &onlineName); + if (onlineName) { + RefreshACLForFolder(onlineName); + PR_Free(onlineName); + } + } + PercentProgressUpdateEvent(""_ns, u""_ns, cnt, total); + delete mb; // this is the last time we're using the list, so delete + // the entries here + cnt++; + } + } while (mb && !DeathSignalReceived()); + } + } +} + +bool nsImapProtocol::FolderNeedsACLInitialized(const char* folderName) { + bool rv = false; + m_imapServerSink->FolderNeedsACLInitialized(nsDependentCString(folderName), + &rv); + return rv; +} + +void nsImapProtocol::MailboxDiscoveryFinished() { + if (!DeathSignalReceived() && !GetSubscribingNow() && + ((m_hierarchyNameState == kNoOperationInProgress) || + (m_hierarchyNameState == kListingForInfoAndDiscovery))) { + nsImapNamespace* ns = nullptr; + m_hostSessionList->GetDefaultNamespaceOfTypeForHost(GetImapServerKey(), + kPersonalNamespace, ns); + const char* personalDir = ns ? ns->GetPrefix() : 0; + + bool trashFolderExists = false; + bool usingSubscription = false; + m_hostSessionList->GetOnlineTrashFolderExistsForHost(GetImapServerKey(), + trashFolderExists); + m_hostSessionList->GetHostIsUsingSubscription(GetImapServerKey(), + usingSubscription); + if (!trashFolderExists && GetDeleteIsMoveToTrash() && usingSubscription) { + // maybe we're not subscribed to the Trash folder + if (personalDir) { + m_hierarchyNameState = kDiscoverTrashFolderInProgress; + List(m_trashFolderPath.get(), true); + m_hierarchyNameState = kNoOperationInProgress; + } + } + + // There is no Trash folder (either LIST'd or LSUB'd), and we're using the + // Delete-is-move-to-Trash model, and there is a personal namespace + if (!trashFolderExists && GetDeleteIsMoveToTrash() && ns) { + nsCString onlineTrashName; + m_runningUrl->AllocateServerPath(m_trashFolderPath.get(), + ns->GetDelimiter(), + getter_Copies(onlineTrashName)); + + GetServerStateParser().SetReportingErrors(false); + bool created = + CreateMailboxRespectingSubscriptions(onlineTrashName.get()); + GetServerStateParser().SetReportingErrors(true); + + // force discovery of new trash folder. + if (created) { + m_hierarchyNameState = kDiscoverTrashFolderInProgress; + List(onlineTrashName.get(), false); + m_hierarchyNameState = kNoOperationInProgress; + } else + m_hostSessionList->SetOnlineTrashFolderExistsForHost(GetImapServerKey(), + true); + } // if trash folder doesn't exist + m_hostSessionList->SetHaveWeEverDiscoveredFoldersForHost(GetImapServerKey(), + true); + // notify front end that folder discovery is complete.... + if (m_imapServerSink) m_imapServerSink->DiscoveryDone(); + + // Clear the discovery in progress flag. + m_hostSessionList->SetDiscoveryForHostInProgress(GetImapServerKey(), false); + } +} + +// returns the mailboxName with the IMAP delimiter removed from the tail end +void nsImapProtocol::RemoveHierarchyDelimiter(nsCString& mailboxName) { + char onlineDelimiter[2] = {0, 0}; + if (m_imapMailFolderSink) + m_imapMailFolderSink->GetOnlineDelimiter(&onlineDelimiter[0]); + // take the hierarchy delimiter off the end, if any. + if (onlineDelimiter[0]) mailboxName.Trim(onlineDelimiter, false, true); +} + +// returns true is the create succeeded (regardless of subscription changes) +bool nsImapProtocol::CreateMailboxRespectingSubscriptions( + const char* mailboxName) { + CreateMailbox(mailboxName); + bool rv = GetServerStateParser().LastCommandSuccessful(); + if (rv && m_autoSubscribe) // auto-subscribe is on + { + // create succeeded - let's subscribe to it + bool reportingErrors = GetServerStateParser().GetReportingErrors(); + GetServerStateParser().SetReportingErrors(false); + nsCString mailboxWODelim(mailboxName); + RemoveHierarchyDelimiter(mailboxWODelim); + OnSubscribe(mailboxWODelim.get()); + GetServerStateParser().SetReportingErrors(reportingErrors); + } + return rv; +} + +void nsImapProtocol::CreateMailbox(const char* mailboxName) { + ProgressEventFunctionUsingName("imapStatusCreatingMailbox"); + + IncrementCommandTagNumber(); + + nsCString escapedName; + CreateEscapedMailboxName(mailboxName, escapedName); + nsCString command(GetServerCommandTag()); + command += " create \""; + command += escapedName; + command += "\"" CRLF; + + nsresult rv = SendData(command.get()); + if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail(); + // If that failed, let's list the parent folder to see if + // it allows inferiors, so we won't try to create sub-folders + // of the parent folder again in the current session. + if (GetServerStateParser().CommandFailed()) { + // Figure out parent folder name. + nsCString parentName(mailboxName); + char hierarchyDelimiter; + m_runningUrl->GetOnlineSubDirSeparator(&hierarchyDelimiter); + int32_t leafPos = parentName.RFindChar(hierarchyDelimiter); + if (leafPos > 0) { + parentName.SetLength(leafPos); + List(parentName.get(), false); + // We still want the caller to know the create failed, so restore that. + GetServerStateParser().SetCommandFailed(true); + } + } +} + +void nsImapProtocol::DeleteMailbox(const char* mailboxName) { + // check if this connection currently has the folder to be deleted selected. + // If so, we should close it because at least some UW servers don't like you + // deleting a folder you have open. + if (FolderIsSelected(mailboxName)) ImapClose(); + + ProgressEventFunctionUsingNameWithString("imapStatusDeletingMailbox", + mailboxName); + + IncrementCommandTagNumber(); + + nsCString escapedName; + CreateEscapedMailboxName(mailboxName, escapedName); + nsCString command(GetServerCommandTag()); + command += " delete \""; + command += escapedName; + command += "\"" CRLF; + + nsresult rv = SendData(command.get()); + if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail(); +} + +void nsImapProtocol::RenameMailbox(const char* existingName, + const char* newName) { + // just like DeleteMailbox; Some UW servers don't like it. + if (FolderIsSelected(existingName)) ImapClose(); + + ProgressEventFunctionUsingNameWithString("imapStatusRenamingMailbox", + existingName); + + IncrementCommandTagNumber(); + + nsCString escapedExistingName; + nsCString escapedNewName; + CreateEscapedMailboxName(existingName, escapedExistingName); + CreateEscapedMailboxName(newName, escapedNewName); + nsCString command(GetServerCommandTag()); + command += " rename \""; + command += escapedExistingName; + command += "\" \""; + command += escapedNewName; + command += "\"" CRLF; + + nsresult rv = SendData(command.get()); + if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail(); +} + +bool nsImapProtocol::GetListSubscribedIsBrokenOnServer() { + // This is a workaround for an issue with LIST(SUBSCRIBED) crashing older + // versions of Zimbra + if (FindInReadable("\"NAME\" \"Zimbra\""_ns, + GetServerStateParser().GetServerID(), + nsCaseInsensitiveCStringComparator)) { + nsCString serverID(GetServerStateParser().GetServerID()); + int start = serverID.LowerCaseFindASCII("\"version\" \"") + 11; + int length = serverID.LowerCaseFindASCII("\" ", start); + const nsDependentCSubstring serverVersionSubstring = + Substring(serverID, start, length); + nsCString serverVersionStr(serverVersionSubstring); + Version serverVersion(serverVersionStr.get()); + Version sevenTwoThree("7.2.3_"); + Version eightZeroZero("8.0.0_"); + Version eightZeroThree("8.0.3_"); + if ((serverVersion < sevenTwoThree) || + ((serverVersion >= eightZeroZero) && (serverVersion < eightZeroThree))) + return true; + } + return false; +} + +void nsImapProtocol::Lsub(const char* mailboxPattern, + bool addDirectoryIfNecessary) { + ProgressEventFunctionUsingName("imapStatusLookingForMailbox"); + + IncrementCommandTagNumber(); + + char* boxnameWithOnlineDirectory = nullptr; + if (addDirectoryIfNecessary) + m_runningUrl->AddOnlineDirectoryIfNecessary(mailboxPattern, + &boxnameWithOnlineDirectory); + + nsCString escapedPattern; + CreateEscapedMailboxName( + boxnameWithOnlineDirectory ? boxnameWithOnlineDirectory : mailboxPattern, + escapedPattern); + + nsCString command(GetServerCommandTag()); + eIMAPCapabilityFlags flag = GetServerStateParser().GetCapabilityFlag(); + bool useListSubscribed = (flag & kHasListExtendedCapability) && + !GetListSubscribedIsBrokenOnServer(); + if (useListSubscribed) + command += " list (subscribed)"; + else + command += " lsub"; + command += " \"\" \""; + command += escapedPattern; + if (useListSubscribed && (flag & kHasSpecialUseCapability)) + command += "\" return (special-use)" CRLF; + else + command += "\"" CRLF; + + PR_Free(boxnameWithOnlineDirectory); + + nsresult rv = SendData(command.get()); + if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail(command.get(), true); +} + +void nsImapProtocol::List(const char* mailboxPattern, + bool addDirectoryIfNecessary, bool useXLIST) { + ProgressEventFunctionUsingName("imapStatusLookingForMailbox"); + + IncrementCommandTagNumber(); + + char* boxnameWithOnlineDirectory = nullptr; + if (addDirectoryIfNecessary) + m_runningUrl->AddOnlineDirectoryIfNecessary(mailboxPattern, + &boxnameWithOnlineDirectory); + + nsCString escapedPattern; + CreateEscapedMailboxName( + boxnameWithOnlineDirectory ? boxnameWithOnlineDirectory : mailboxPattern, + escapedPattern); + + nsCString command(GetServerCommandTag()); + command += useXLIST ? " xlist \"\" \"" : " list \"\" \""; + command += escapedPattern; + command += "\"" CRLF; + + PR_Free(boxnameWithOnlineDirectory); + + nsresult rv = SendData(command.get()); + if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail(command.get(), true); +} + +void nsImapProtocol::Subscribe(const char* mailboxName) { + ProgressEventFunctionUsingNameWithString("imapStatusSubscribeToMailbox", + mailboxName); + + IncrementCommandTagNumber(); + + nsCString escapedName; + CreateEscapedMailboxName(mailboxName, escapedName); + + nsCString command(GetServerCommandTag()); + command += " subscribe \""; + command += escapedName; + command += "\"" CRLF; + + nsresult rv = SendData(command.get()); + if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail(); +} + +void nsImapProtocol::Unsubscribe(const char* mailboxName) { + ProgressEventFunctionUsingNameWithString("imapStatusUnsubscribeMailbox", + mailboxName); + IncrementCommandTagNumber(); + + nsCString escapedName; + CreateEscapedMailboxName(mailboxName, escapedName); + + nsCString command(GetServerCommandTag()); + command += " unsubscribe \""; + command += escapedName; + command += "\"" CRLF; + + nsresult rv = SendData(command.get()); + if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail(); +} + +void nsImapProtocol::Idle() { + IncrementCommandTagNumber(); + + if (m_urlInProgress) return; + nsAutoCString command(GetServerCommandTag()); + command += " IDLE" CRLF; + nsresult rv = SendData(command.get()); + if (NS_SUCCEEDED(rv)) { + // Typically, we'll just get back only a continuation char on IDLE response, + // "+ idling". However, it is possible untagged responses will occur before + // and/or after the '+' which we treat the same as later untagged responses + // signaled by the socket thread. If untagged responses occur on IDLE, + // HandleIdleResponses() will trigger a select URL which will exit idle mode + // and update the selected folder. Finally, if IDLE responds with tagged BAD + // or NO, HandleIdleResponses() will return false. + m_idle = HandleIdleResponses(); + } +} + +// until we can fix the hang on shutdown waiting for server +// responses, we need to not wait for the server response +// on shutdown. +void nsImapProtocol::EndIdle(bool waitForResponse /* = true */) { + // clear the async wait - otherwise, we have trouble doing a blocking read + // below. + nsCOMPtr<nsIAsyncInputStream> asyncInputStream = + do_QueryInterface(m_inputStream); + if (asyncInputStream) asyncInputStream->AsyncWait(nullptr, 0, 0, nullptr); + nsresult rv = SendData("DONE" CRLF); + // set a short timeout if we don't want to wait for a response + if (m_transport && !waitForResponse) + m_transport->SetTimeout(nsISocketTransport::TIMEOUT_READ_WRITE, 5); + if (NS_SUCCEEDED(rv)) { + m_idle = false; + ParseIMAPandCheckForNewMail(); + // If waiting for response (i.e., not shutting down), check for IDLE + // untagged response(s) occurring after DONE is sent, which can occur and is + // mentioned in the IDLE rfc as a possibility. This is similar to the checks + // done in OnStatusForFolder(). + if (waitForResponse && m_imapMailFolderSinkSelected && + GetServerStateParser().UntaggedResponse()) { + Log("EndIdle", nullptr, "idle response after idle DONE"); + m_imapMailFolderSinkSelected->OnNewIdleMessages(); + } + } + // Set m_imapMailFolderSink null only if shutting down or if DONE succeeds. + // We need to keep m_imapMailFolderSink if DONE fails or times out when not + // shutting down so the URL that is attempting to run on this connection can + // retry or signal a failed status when SetUrlState is called in + // ProcessCurrentUrl to invoke nsIUrlListener.onStopRunningUrl. + if (!waitForResponse || GetServerStateParser().LastCommandSuccessful()) + m_imapMailFolderSink = nullptr; +} + +void nsImapProtocol::Search(const char* searchCriteria, bool useUID, + bool notifyHit /* true */) { + m_notifySearchHit = notifyHit; + ProgressEventFunctionUsingName("imapStatusSearchMailbox"); + IncrementCommandTagNumber(); + + nsCString protocolString(GetServerCommandTag()); + // the searchCriteria string contains the 'search ....' string + if (useUID) protocolString.AppendLiteral(" uid"); + protocolString.Append(' '); + protocolString.Append(searchCriteria); + // the search criteria can contain string literals, which means we + // need to break up the protocol string by CRLF's, and after sending CRLF, + // wait for the server to respond OK before sending more data + nsresult rv; + int32_t crlfIndex; + while ((crlfIndex = protocolString.Find(CRLF)) != kNotFound && + !DeathSignalReceived()) { + nsAutoCString tempProtocolString; + tempProtocolString = StringHead(protocolString, crlfIndex + 2); + rv = SendData(tempProtocolString.get()); + if (NS_FAILED(rv)) return; + ParseIMAPandCheckForNewMail(); + protocolString.Cut(0, crlfIndex + 2); + } + protocolString.Append(CRLF); + + rv = SendData(protocolString.get()); + if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail(); +} + +void nsImapProtocol::Copy(const char* messageList, + const char* destinationMailbox, bool idsAreUid) { + IncrementCommandTagNumber(); + + nsCString escapedDestination; + CreateEscapedMailboxName(destinationMailbox, escapedDestination); + + // turn messageList back into key array and then back into a message id list, + // but use the flag state to handle ranges correctly. + nsCString messageIdList; + nsTArray<nsMsgKey> msgKeys; + if (idsAreUid) ParseUidString(messageList, msgKeys); + + int32_t msgCountLeft = msgKeys.Length(); + uint32_t msgsHandled = 0; + + do { + nsCString idString; + + uint32_t msgsToHandle = msgCountLeft; + if (idsAreUid) + AllocateImapUidString(msgKeys.Elements() + msgsHandled, msgsToHandle, + m_flagState, idString); + else + idString.Assign(messageList); + + msgsHandled += msgsToHandle; + msgCountLeft -= msgsToHandle; + + IncrementCommandTagNumber(); + nsAutoCString protocolString(GetServerCommandTag()); + if (idsAreUid) protocolString.AppendLiteral(" uid"); + // If it's a MOVE operation on aol servers then use 'xaol-move' cmd. + if ((m_imapAction == nsIImapUrl::nsImapOnlineMove) && + GetServerStateParser().ServerIsAOLServer()) + protocolString.AppendLiteral(" xaol-move "); + else if ((m_imapAction == nsIImapUrl::nsImapOnlineMove) && + GetServerStateParser().GetCapabilityFlag() & kHasMoveCapability) + protocolString.AppendLiteral(" move "); + else + protocolString.AppendLiteral(" copy "); + + protocolString.Append(idString); + protocolString.AppendLiteral(" \""); + protocolString.Append(escapedDestination); + protocolString.AppendLiteral("\"" CRLF); + + nsresult rv = SendData(protocolString.get()); + if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail(protocolString.get()); + } while (msgCountLeft > 0 && !DeathSignalReceived()); +} + +void nsImapProtocol::NthLevelChildList(const char* onlineMailboxPrefix, + int32_t depth) { + NS_ASSERTION(depth >= 0, "Oops ... depth must be equal or greater than 0"); + if (depth < 0) return; + + nsCString truncatedPrefix(onlineMailboxPrefix); + char16_t slash = '/'; + if (truncatedPrefix.Last() == slash) + truncatedPrefix.SetLength(truncatedPrefix.Length() - 1); + + nsAutoCString pattern(truncatedPrefix); + nsAutoCString suffix; + int count = 0; + char separator = 0; + m_runningUrl->GetOnlineSubDirSeparator(&separator); + suffix.Assign(separator); + suffix += '%'; + + while (count < depth) { + pattern += suffix; + count++; + List(pattern.get(), false); + } +} + +/** + * ProcessAuthenticatedStateURL() is a helper for ProcessCurrentURL() which + * handles running URLs which require the connection to be in the + * Authenticated state. + */ +void nsImapProtocol::ProcessAuthenticatedStateURL() { + nsImapAction imapAction; + char* sourceMailbox = nullptr; + m_runningUrl->GetImapAction(&imapAction); + + // switch off of the imap url action and take an appropriate action + switch (imapAction) { + case nsIImapUrl::nsImapLsubFolders: + OnLSubFolders(); + break; + case nsIImapUrl::nsImapAppendMsgFromFile: + OnAppendMsgFromFile(); + break; + case nsIImapUrl::nsImapDiscoverAllBoxesUrl: + NS_ASSERTION(!GetSubscribingNow(), + "Oops ... should not get here from subscribe UI"); + DiscoverMailboxList(); + break; + case nsIImapUrl::nsImapDiscoverAllAndSubscribedBoxesUrl: + DiscoverAllAndSubscribedBoxes(); + break; + case nsIImapUrl::nsImapCreateFolder: + sourceMailbox = OnCreateServerSourceFolderPathString(); + OnCreateFolder(sourceMailbox); + break; + case nsIImapUrl::nsImapEnsureExistsFolder: + sourceMailbox = OnCreateServerSourceFolderPathString(); + OnEnsureExistsFolder(sourceMailbox); + break; + case nsIImapUrl::nsImapDiscoverChildrenUrl: { + char* canonicalParent = nullptr; + m_runningUrl->CreateServerSourceFolderPathString(&canonicalParent); + if (canonicalParent) { + NthLevelChildList(canonicalParent, 2); + PR_Free(canonicalParent); + } + break; + } + case nsIImapUrl::nsImapSubscribe: + sourceMailbox = OnCreateServerSourceFolderPathString(); + OnSubscribe(sourceMailbox); // used to be called subscribe + + if (GetServerStateParser().LastCommandSuccessful()) { + bool shouldList; + // if url is an external click url, then we should list the folder + // after subscribing to it, so we can select it. + m_runningUrl->GetExternalLinkUrl(&shouldList); + if (shouldList) OnListFolder(sourceMailbox, true); + } + break; + case nsIImapUrl::nsImapUnsubscribe: + sourceMailbox = OnCreateServerSourceFolderPathString(); + OnUnsubscribe(sourceMailbox); + break; + case nsIImapUrl::nsImapRefreshACL: + sourceMailbox = OnCreateServerSourceFolderPathString(); + RefreshACLForFolder(sourceMailbox); + break; + case nsIImapUrl::nsImapRefreshAllACLs: + OnRefreshAllACLs(); + break; + case nsIImapUrl::nsImapListFolder: + sourceMailbox = OnCreateServerSourceFolderPathString(); + OnListFolder(sourceMailbox, false); + break; + case nsIImapUrl::nsImapFolderStatus: + sourceMailbox = OnCreateServerSourceFolderPathString(); + OnStatusForFolder(sourceMailbox); + break; + case nsIImapUrl::nsImapRefreshFolderUrls: + sourceMailbox = OnCreateServerSourceFolderPathString(); + XMailboxInfo(sourceMailbox); + if (GetServerStateParser().LastCommandSuccessful()) + SetFolderAdminUrl(sourceMailbox); + break; + case nsIImapUrl::nsImapDeleteFolder: + sourceMailbox = OnCreateServerSourceFolderPathString(); + OnDeleteFolder(sourceMailbox); + break; + case nsIImapUrl::nsImapRenameFolder: + sourceMailbox = OnCreateServerSourceFolderPathString(); + OnRenameFolder(sourceMailbox); + break; + case nsIImapUrl::nsImapMoveFolderHierarchy: + sourceMailbox = OnCreateServerSourceFolderPathString(); + OnMoveFolderHierarchy(sourceMailbox); + break; + case nsIImapUrl::nsImapVerifylogon: + break; + default: + break; + } + PR_Free(sourceMailbox); +} + +void nsImapProtocol::ProcessAfterAuthenticated() { + // if we're a netscape server, and we haven't got the admin url, get it + bool hasAdminUrl = true; + + // If a capability response didn't occur during authentication, request + // the capabilities again to ensure the full capability set is known. + if (!m_capabilityResponseOccurred) Capability(); + + if (NS_SUCCEEDED(m_hostSessionList->GetHostHasAdminURL(GetImapServerKey(), + hasAdminUrl)) && + !hasAdminUrl) { + if (GetServerStateParser().ServerHasServerInfo()) { + XServerInfo(); + if (GetServerStateParser().LastCommandSuccessful() && m_imapServerSink) { + m_imapServerSink->SetMailServerUrls( + GetServerStateParser().GetMailAccountUrl(), + GetServerStateParser().GetManageListsUrl(), + GetServerStateParser().GetManageFiltersUrl()); + // we've tried to ask for it, so don't try again this session. + m_hostSessionList->SetHostHasAdminURL(GetImapServerKey(), true); + } + } else if (GetServerStateParser().ServerIsNetscape3xServer()) { + Netscape(); + if (GetServerStateParser().LastCommandSuccessful() && m_imapServerSink) + m_imapServerSink->SetMailServerUrls( + GetServerStateParser().GetMailAccountUrl(), EmptyCString(), + EmptyCString()); + } + } + + if (GetServerStateParser().ServerHasNamespaceCapability()) { + bool nameSpacesOverridable = false; + bool haveNameSpacesForHost = false; + m_hostSessionList->GetNamespacesOverridableForHost(GetImapServerKey(), + nameSpacesOverridable); + m_hostSessionList->GetGotNamespacesForHost(GetImapServerKey(), + haveNameSpacesForHost); + + // mscott: VERIFY THIS CLAUSE!!!!!!! + if (nameSpacesOverridable && !haveNameSpacesForHost) Namespace(); + } + + // If the server supports compression, turn it on now. + // Choosing this spot (after login has finished) because + // many proxies (e.g. perdition, nginx) talk IMAP to the + // client until login is finished, then hand off to the + // backend. If we enable compression early the proxy + // will be confused. + if (UseCompressDeflate()) StartCompressDeflate(); + + if ((GetServerStateParser().GetCapabilityFlag() & kHasEnableCapability) && + UseCondStore()) + EnableCondStore(); + + if ((GetServerStateParser().GetCapabilityFlag() & kHasIDCapability) && + m_sendID) { + ID(); + if (m_imapServerSink && !GetServerStateParser().GetServerID().IsEmpty()) + m_imapServerSink->SetServerID(GetServerStateParser().GetServerID()); + } + + bool utf8AcceptAllowed = m_allowUTF8Accept; + m_allowUTF8Accept = false; + if (utf8AcceptAllowed && + ((GetServerStateParser().GetCapabilityFlag() & + (kHasEnableCapability | kHasUTF8AcceptCapability)) == + (kHasEnableCapability | kHasUTF8AcceptCapability))) { + if (m_imapServerSink) { + EnableUTF8Accept(); + m_allowUTF8Accept = GetServerStateParser().fUtf8AcceptEnabled; + // m_allowUTF8Accept affects imap append handling. See + // UploadMessageFromFile(). + m_imapServerSink->SetServerUtf8AcceptEnabled(m_allowUTF8Accept); + GetServerStateParser().fUtf8AcceptEnabled = false; + } else { + NS_WARNING("UTF8=ACCEPT not enabled due to null m_imapServerSink"); + } + } +} + +void nsImapProtocol::SetupMessageFlagsString(nsCString& flagString, + imapMessageFlagsType flags, + uint16_t userFlags) { + if (flags & kImapMsgSeenFlag) flagString.AppendLiteral("\\Seen "); + if (flags & kImapMsgAnsweredFlag) flagString.AppendLiteral("\\Answered "); + if (flags & kImapMsgFlaggedFlag) flagString.AppendLiteral("\\Flagged "); + if (flags & kImapMsgDeletedFlag) flagString.AppendLiteral("\\Deleted "); + if (flags & kImapMsgDraftFlag) flagString.AppendLiteral("\\Draft "); + if (flags & kImapMsgRecentFlag) flagString.AppendLiteral("\\Recent "); + if ((flags & kImapMsgForwardedFlag) && + (userFlags & kImapMsgSupportForwardedFlag)) + flagString.AppendLiteral("$Forwarded "); // Not always available + if ((flags & kImapMsgMDNSentFlag) && (userFlags & kImapMsgSupportMDNSentFlag)) + flagString.AppendLiteral("$MDNSent "); // Not always available + + // eat the last space + if (!flagString.IsEmpty()) flagString.SetLength(flagString.Length() - 1); +} + +void nsImapProtocol::ProcessStoreFlags(const nsCString& messageIdsString, + bool idsAreUids, + imapMessageFlagsType flags, + bool addFlags) { + nsCString flagString; + + uint16_t userFlags = GetServerStateParser().SupportsUserFlags(); + uint16_t settableFlags = GetServerStateParser().SettablePermanentFlags(); + + if (!addFlags && (flags & userFlags) && !(flags & settableFlags)) { + if (m_runningUrl) + m_runningUrl->SetExtraStatus(nsIImapUrl::ImapStatusFlagsNotSettable); + return; // if cannot set any of the flags bail out + } + + if (addFlags) + flagString = "+Flags ("; + else + flagString = "-Flags ("; + + if (flags & kImapMsgSeenFlag && kImapMsgSeenFlag & settableFlags) + flagString.AppendLiteral("\\Seen "); + if (flags & kImapMsgAnsweredFlag && kImapMsgAnsweredFlag & settableFlags) + flagString.AppendLiteral("\\Answered "); + if (flags & kImapMsgFlaggedFlag && kImapMsgFlaggedFlag & settableFlags) + flagString.AppendLiteral("\\Flagged "); + if (flags & kImapMsgDeletedFlag && kImapMsgDeletedFlag & settableFlags) + flagString.AppendLiteral("\\Deleted "); + if (flags & kImapMsgDraftFlag && kImapMsgDraftFlag & settableFlags) + flagString.AppendLiteral("\\Draft "); + if (flags & kImapMsgForwardedFlag && kImapMsgSupportForwardedFlag & userFlags) + flagString.AppendLiteral("$Forwarded "); // if supported + if (flags & kImapMsgMDNSentFlag && kImapMsgSupportMDNSentFlag & userFlags) + flagString.AppendLiteral("$MDNSent "); // if supported + + if (flagString.Length() > 8) // if more than "+Flags (" + { + // replace the final space with ')' + flagString.SetCharAt(')', flagString.Length() - 1); + + Store(messageIdsString, flagString.get(), idsAreUids); + if (m_runningUrl && idsAreUids) { + nsCString messageIdString; + m_runningUrl->GetListOfMessageIds(messageIdString); + nsTArray<nsMsgKey> msgKeys; + ParseUidString(messageIdString.get(), msgKeys); + + int32_t msgCount = msgKeys.Length(); + for (int32_t i = 0; i < msgCount; i++) { + bool found; + imapMessageFlagsType resultFlags; + // check if the flags were added/removed, and if the uid really exists. + nsresult rv = GetFlagsForUID(msgKeys[i], &found, &resultFlags, nullptr); + if (NS_FAILED(rv) || !found || + (addFlags && ((flags & resultFlags) != flags)) || + (!addFlags && ((flags & resultFlags) != 0))) { + m_runningUrl->SetExtraStatus(nsIImapUrl::ImapStatusFlagChangeFailed); + break; + } + } + } + } +} + +/** + * This will cause all messages marked deleted to be expunged with no untagged + * response so it can cause unexpected data loss if used improperly. + */ +void nsImapProtocol::ImapClose(bool shuttingDown /* = false */, + bool waitForResponse /* = true */) { + IncrementCommandTagNumber(); + + nsCString command(GetServerCommandTag()); + command.AppendLiteral(" close" CRLF); + + if (!shuttingDown) ProgressEventFunctionUsingName("imapStatusCloseMailbox"); + + GetServerStateParser().ResetFlagInfo(); + + nsresult rv = SendData(command.get()); + if (m_transport && shuttingDown) + m_transport->SetTimeout(nsISocketTransport::TIMEOUT_READ_WRITE, 5); + + if (NS_SUCCEEDED(rv) && waitForResponse) ParseIMAPandCheckForNewMail(); +} + +void nsImapProtocol::XAOL_Option(const char* option) { + IncrementCommandTagNumber(); + + nsCString command(GetServerCommandTag()); + command.AppendLiteral(" XAOL-OPTION "); + command.Append(option); + command.Append(CRLF); + + nsresult rv = SendData(command.get()); + if (NS_SUCCEEDED(rv)) ParseIMAPandCheckForNewMail(); +} + +void nsImapProtocol::Check() { + // ProgressUpdateEvent("Checking mailbox..."); + + IncrementCommandTagNumber(); + + nsCString command(GetServerCommandTag()); + command.AppendLiteral(" check" CRLF); + + nsresult rv = SendData(command.get()); + if (NS_SUCCEEDED(rv)) { + m_flagChangeCount = 0; + m_lastCheckTime = PR_Now(); + ParseIMAPandCheckForNewMail(); + } +} + +nsresult nsImapProtocol::GetMsgWindow(nsIMsgWindow** aMsgWindow) { + nsresult rv; + *aMsgWindow = nullptr; + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = + do_QueryInterface(m_runningUrl, &rv); + NS_ENSURE_SUCCESS(rv, rv); + if (!m_imapProtocolSink) return NS_ERROR_FAILURE; + return m_imapProtocolSink->GetUrlWindow(mailnewsUrl, aMsgWindow); +} + +/** + * Get password from RAM, disk (password manager) or user (dialog) + * @return NS_MSG_PASSWORD_PROMPT_CANCELLED + * (which is NS_SUCCEEDED!) when user cancelled + * NS_FAILED(rv) for other errors + */ +nsresult nsImapProtocol::GetPassword(nsString& password, + bool newPasswordRequested) { + // we are in the imap thread so *NEVER* try to extract the password with UI + NS_ENSURE_TRUE(m_imapServerSink, NS_ERROR_NULL_POINTER); + NS_ENSURE_TRUE(m_server, NS_ERROR_NULL_POINTER); + nsresult rv; + + password = nsString(); + // Get the password already stored in mem + rv = m_imapServerSink->GetServerPassword(password); + if (NS_FAILED(rv) || password.IsEmpty()) { + // First see if there's an associated window. We don't want to produce a + // password prompt if there is no window, e.g., during biff. + AutoProxyReleaseMsgWindow msgWindow; + GetMsgWindow(getter_AddRefs(msgWindow)); + if (msgWindow) { + m_passwordStatus = NS_OK; + m_passwordObtained = false; + + // Get the password from pw manager (harddisk) or user (dialog) + rv = m_imapServerSink->AsyncGetPassword(this, newPasswordRequested, + password); + + if (NS_SUCCEEDED(rv)) { + while (password.IsEmpty()) { + bool shuttingDown = false; + (void)m_imapServerSink->GetServerShuttingDown(&shuttingDown); + if (shuttingDown) { + // Note: If we fix bug 1783573 this check could be ditched. + rv = NS_ERROR_FAILURE; + break; + } + + ReentrantMonitorAutoEnter mon(m_passwordReadyMonitor); + if (!m_passwordObtained && !NS_FAILED(m_passwordStatus) && + m_passwordStatus != NS_MSG_PASSWORD_PROMPT_CANCELLED && + !DeathSignalReceived()) { + mon.Wait(PR_MillisecondsToInterval(1000)); + } + + if (NS_FAILED(m_passwordStatus) || + m_passwordStatus == NS_MSG_PASSWORD_PROMPT_CANCELLED) { + rv = m_passwordStatus; + break; + } + + if (DeathSignalReceived()) { + rv = NS_ERROR_FAILURE; + break; + } + + if (m_passwordObtained) { + rv = m_passwordStatus; + password = m_password; + break; + } + } // end while + } + } else { + // If no msgWindow (i.e., unattended operation like biff, filtering or + // autosync) try to get the password directly from login mgr. If it's not + // there, will return NS_ERROR_NOT_AVAILABLE and the connection will fail + // with only the IMAP log message: `password prompt failed or user + // canceled it'. No password prompt occurs. + rv = m_imapServerSink->SyncGetPassword(password); + } + } + if (!password.IsEmpty()) m_lastPasswordSent = password; + return rv; +} + +NS_IMETHODIMP nsImapProtocol::OnPromptStartAsync( + nsIMsgAsyncPromptCallback* aCallback) { + bool result = false; + OnPromptStart(&result); + return aCallback->OnAuthResult(result); +} + +// This is called from the UI thread. +NS_IMETHODIMP +nsImapProtocol::OnPromptStart(bool* aResult) { + nsresult rv; + nsCOMPtr<nsIImapIncomingServer> imapServer = do_QueryReferent(m_server, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgWindow> msgWindow; + + *aResult = false; + GetMsgWindow(getter_AddRefs(msgWindow)); + nsString password = m_lastPasswordSent; + rv = imapServer->PromptPassword(msgWindow, password); + + ReentrantMonitorAutoEnter passwordMon(m_passwordReadyMonitor); + + m_password = password; + m_passwordStatus = rv; + if (!m_password.IsEmpty()) *aResult = true; + + // Notify the imap thread that we have a password. + m_passwordObtained = true; + passwordMon.Notify(); + return rv; +} + +NS_IMETHODIMP +nsImapProtocol::OnPromptAuthAvailable() { + nsresult rv; + nsCOMPtr<nsIMsgIncomingServer> imapServer = do_QueryReferent(m_server, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsresult status = imapServer->GetPassword(m_password); + + ReentrantMonitorAutoEnter passwordMon(m_passwordReadyMonitor); + + m_passwordStatus = status; + // Notify the imap thread that we have a password. + m_passwordObtained = true; + passwordMon.Notify(); + return m_passwordStatus; +} + +NS_IMETHODIMP +nsImapProtocol::OnPromptCanceled() { + // A prompt was cancelled, so notify the imap thread. + ReentrantMonitorAutoEnter passwordMon(m_passwordReadyMonitor); + m_passwordStatus = NS_MSG_PASSWORD_PROMPT_CANCELLED; + passwordMon.Notify(); + return NS_OK; +} + +// Called when capability response is parsed. +void nsImapProtocol::SetCapabilityResponseOccurred() { + m_capabilityResponseOccurred = true; +} + +bool nsImapProtocol::TryToLogon() { + MOZ_LOG(IMAP, LogLevel::Debug, ("Try to log in")); + NS_ENSURE_TRUE(m_imapServerSink, false); + bool loginSucceeded = false; + bool skipLoop = false; + nsAutoString password; + nsAutoCString userName; + + // If remains false when authentication is complete it means that a + // capability response didn't occur within the authentication response so + // capabilities will be requested explicitly. + m_capabilityResponseOccurred = false; + + nsresult rv = ChooseAuthMethod(); + if (NS_FAILED(rv)) // all methods failed + { + // are there any matching login schemes at all? + if (!(GetServerStateParser().GetCapabilityFlag() & m_prefAuthMethods)) { + // Pref doesn't match server. Now, find an appropriate error msg. + + // pref has plaintext pw & server claims to support encrypted pw + if (m_prefAuthMethods == + (kHasAuthOldLoginCapability | kHasAuthLoginCapability | + kHasAuthPlainCapability) && + GetServerStateParser().GetCapabilityFlag() & kHasCRAMCapability) + // tell user to change to encrypted pw + AlertUserEventUsingName("imapAuthChangePlainToEncrypt"); + // pref has encrypted pw & server claims to support plaintext pw + else if (m_prefAuthMethods == kHasCRAMCapability && + GetServerStateParser().GetCapabilityFlag() & + (kHasAuthOldLoginCapability | kHasAuthLoginCapability | + kHasAuthPlainCapability)) { + // have SSL + if (m_socketType == nsMsgSocketType::SSL || + m_socketType == nsMsgSocketType::alwaysSTARTTLS) + // tell user to change to plaintext pw + AlertUserEventUsingName("imapAuthChangeEncryptToPlainSSL"); + else + // tell user to change to plaintext pw, with big warning + AlertUserEventUsingName("imapAuthChangeEncryptToPlainNoSSL"); + } else + // just "change auth method" + AlertUserEventUsingName("imapAuthMechNotSupported"); + + skipLoop = true; + } else { + // try to reset failed methods and try them again + ResetAuthMethods(); + rv = ChooseAuthMethod(); + if (NS_FAILED(rv)) // all methods failed + { + MOZ_LOG(IMAP, LogLevel::Error, + ("huch? there are auth methods, and we reset failed ones, but " + "ChooseAuthMethod still fails.")); + return false; + } + } + } + + // Check the uri host for localhost indicators to see if we + // should bypass the SSL check for clientid. + // Unfortunately we cannot call IsOriginPotentiallyTrustworthy + // here because it can only be called from the main thread. + bool isLocalhostConnection = false; + if (m_mockChannel) { + nsCOMPtr<nsIURI> uri; + m_mockChannel->GetURI(getter_AddRefs(uri)); + if (uri) { + nsCString uriHost; + uri->GetHost(uriHost); + if (uriHost.Equals("127.0.0.1") || uriHost.Equals("::1") || + uriHost.Equals("localhost")) { + isLocalhostConnection = true; + } + } + } + + // Whether our connection can be considered 'secure' and whether + // we should allow the CLIENTID to be sent over this channel. + bool isSecureConnection = + (m_connectionType.EqualsLiteral("starttls") || + m_connectionType.EqualsLiteral("ssl") || isLocalhostConnection); + + // Before running the ClientID command we check for clientid + // support by checking the server capability flags for the + // flag kHasClientIDCapability. + // We check that the m_clientId string is not empty, and + // we ensure the connection can be considered secure. + if ((GetServerStateParser().GetCapabilityFlag() & kHasClientIDCapability) && + !m_clientId.IsEmpty() && isSecureConnection) { + rv = ClientID(); + if (NS_FAILED(rv)) { + MOZ_LOG(IMAP, LogLevel::Error, + ("TryToLogon: Could not issue CLIENTID command")); + skipLoop = true; + } + } + + // Get username, either the stored one or from user + rv = m_imapServerSink->GetLoginUsername(userName); + if (NS_FAILED(rv) || userName.IsEmpty()) { + // The user hit "Cancel" on the dialog box + skipLoop = true; + } + + // clang-format off + /* + * Login can fail for various reasons: + * 1. Server claims to support GSSAPI, but it really doesn't. + * Or the client doesn't support GSSAPI, or is not logged in yet. + * (GSSAPI is a mechanism without password in apps). + * 2. Server claims to support CRAM-MD5, but it's broken and will fail despite correct password. + * 2.1. Some servers say they support CRAM but are so badly broken that trying it causes + * all subsequent login attempts to fail during this connection (bug 231303). + * So we use CRAM/NTLM/MSN only if enabled in prefs. + * Update: if it affects only some ISPs, we can maybe use the ISP DB + * and disable CRAM specifically for these. + * 3. Prefs are set to require auth methods which the server doesn't support + * (per CAPS or we tried and they failed). + * 4. User provided wrong password. + * 5. We tried too often and the server shut us down, so even a correct attempt + * will now (currently) fail. + * The above problems may overlap, e.g. 3. with 1. and 2., and we can't differentiate + * between 2. and 4., which is really unfortunate. + */ + // clang-format on + + bool newPasswordRequested = false; + // remember the msgWindow before we start trying to logon, because if the + // server drops the connection on errors, TellThreadToDie will null out the + // protocolsink and we won't be able to get the msgWindow. + AutoProxyReleaseMsgWindow msgWindow; + GetMsgWindow(getter_AddRefs(msgWindow)); + + // This loops over 1) auth methods (only one per loop) and 2) password tries + // (with UI) + while (!loginSucceeded && !skipLoop && !DeathSignalReceived()) { + // Get password + if (m_currentAuthMethod != + kHasAuthGssApiCapability && // GSSAPI uses no pw in apps + m_currentAuthMethod != kHasAuthExternalCapability && + m_currentAuthMethod != kHasXOAuth2Capability && + m_currentAuthMethod != kHasAuthNoneCapability) { + rv = GetPassword(password, newPasswordRequested); + newPasswordRequested = false; + if (rv == NS_MSG_PASSWORD_PROMPT_CANCELLED || NS_FAILED(rv)) { + MOZ_LOG(IMAP, LogLevel::Error, + ("IMAP: password prompt failed or user canceled it")); + break; + } + MOZ_LOG(IMAP, LogLevel::Debug, ("got new password")); + } + + bool lastReportingErrors = GetServerStateParser().GetReportingErrors(); + GetServerStateParser().SetReportingErrors( + false); // turn off errors - we'll put up our own. + + rv = AuthLogin(userName.get(), password, m_currentAuthMethod); + + GetServerStateParser().SetReportingErrors( + lastReportingErrors); // restore error reports + loginSucceeded = NS_SUCCEEDED(rv); + + if (!loginSucceeded) { + MOZ_LOG(IMAP, LogLevel::Debug, ("authlogin failed")); + MarkAuthMethodAsFailed(m_currentAuthMethod); + rv = ChooseAuthMethod(); // change m_currentAuthMethod to try other one + // next round + + if (NS_FAILED(rv)) // all methods failed + { + if (m_prefAuthMethods == kHasAuthGssApiCapability) { + // GSSAPI failed, and it's the only available method, + // and it's password-less, so nothing left to do. + AlertUserEventUsingName("imapAuthGssapiFailed"); + break; + } + + if (m_prefAuthMethods & kHasXOAuth2Capability) { + // OAuth2 failed. Entering password does not help. + AlertUserEventUsingName("imapOAuth2Error"); + break; + } + + // The reason that we failed might be a wrong password, so + // ask user what to do + MOZ_LOG(IMAP, LogLevel::Warning, + ("IMAP: ask user what to do (after login failed): new " + "passwort, retry, cancel")); + if (!m_imapServerSink) break; + // if there's no msg window, don't forget the password + if (!msgWindow) break; + int32_t buttonPressed = 1; + rv = m_imapServerSink->PromptLoginFailed(msgWindow, &buttonPressed); + if (NS_FAILED(rv)) break; + if (buttonPressed == 2) // 'New password' button + { + MOZ_LOG(IMAP, LogLevel::Warning, ("new password button pressed.")); + // Forget the current password + password.Truncate(); + m_hostSessionList->SetPasswordForHost(GetImapServerKey(), + EmptyString()); + m_imapServerSink->ForgetPassword(); + m_password.Truncate(); + MOZ_LOG(IMAP, LogLevel::Warning, ("password reset (nulled)")); + newPasswordRequested = true; + // Will call GetPassword() in beginning of next loop + + // Try all possible auth methods again with the new password. + ResetAuthMethods(); + } else if (buttonPressed == 0) // Retry button + { + MOZ_LOG(IMAP, LogLevel::Warning, ("retry button pressed")); + // Try all possible auth methods again + ResetAuthMethods(); + } else if (buttonPressed == 1) // Cancel button + { + MOZ_LOG(IMAP, LogLevel::Warning, ("cancel button pressed")); + break; // Abort quickly + } + + // TODO what is this for? When does it get set to != unknown again? + m_currentBiffState = nsIMsgFolder::nsMsgBiffState_Unknown; + SendSetBiffIndicatorEvent(m_currentBiffState); + } // all methods failed + } // login failed + } // while + + if (loginSucceeded) { + MOZ_LOG(IMAP, LogLevel::Debug, ("login succeeded")); + bool passwordAlreadyVerified; + m_hostSessionList->SetPasswordForHost(GetImapServerKey(), password); + rv = m_hostSessionList->GetPasswordVerifiedOnline(GetImapServerKey(), + passwordAlreadyVerified); + if (NS_SUCCEEDED(rv) && !passwordAlreadyVerified) { + // First successful login for this server/host during this session. + m_hostSessionList->SetPasswordVerifiedOnline(GetImapServerKey()); + } + + bool imapPasswordIsNew = !passwordAlreadyVerified; + if (imapPasswordIsNew) { + if (m_currentBiffState == nsIMsgFolder::nsMsgBiffState_Unknown) { + m_currentBiffState = nsIMsgFolder::nsMsgBiffState_NoMail; + SendSetBiffIndicatorEvent(m_currentBiffState); + } + m_imapServerSink->SetUserAuthenticated(true); + } + + nsImapAction imapAction; + m_runningUrl->GetImapAction(&imapAction); + // We don't want to do any more processing if we're just + // verifying the ability to logon because it can leave us in + // a half-constructed state. + if (imapAction != nsIImapUrl::nsImapVerifylogon) + ProcessAfterAuthenticated(); + } else // login failed + { + MOZ_LOG(IMAP, LogLevel::Error, ("login failed entirely")); + m_currentBiffState = nsIMsgFolder::nsMsgBiffState_Unknown; + SendSetBiffIndicatorEvent(m_currentBiffState); + HandleCurrentUrlError(); + SetConnectionStatus(NS_ERROR_FAILURE); // stop netlib + } + + return loginSucceeded; +} + +void nsImapProtocol::UpdateFolderQuotaData(nsImapQuotaAction aAction, + nsCString& aQuotaRoot, + uint64_t aUsed, uint64_t aMax) { + NS_ASSERTION(m_imapMailFolderSink, "m_imapMailFolderSink is null!"); + + m_imapMailFolderSink->SetFolderQuotaData(aAction, aQuotaRoot, aUsed, aMax); +} + +void nsImapProtocol::GetQuotaDataIfSupported(const char* aBoxName) { + // If server doesn't have quota support, don't do anything + if (!(GetServerStateParser().GetCapabilityFlag() & kQuotaCapability)) return; + + nsCString escapedName; + CreateEscapedMailboxName(aBoxName, escapedName); + + IncrementCommandTagNumber(); + + nsAutoCString quotacommand(GetServerCommandTag()); + quotacommand.AppendLiteral(" getquotaroot \""); + quotacommand.Append(escapedName); + quotacommand.AppendLiteral("\"" CRLF); + + NS_ASSERTION(m_imapMailFolderSink, "m_imapMailFolderSink is null!"); + if (m_imapMailFolderSink) + m_imapMailFolderSink->SetFolderQuotaCommandIssued(true); + + nsresult quotarv = SendData(quotacommand.get()); + if (NS_SUCCEEDED(quotarv)) + ParseIMAPandCheckForNewMail(nullptr, true); // don't display errors. +} + +bool nsImapProtocol::GetDeleteIsMoveToTrash() { + bool rv = false; + NS_ASSERTION(m_hostSessionList, "fatal... null host session list"); + if (m_hostSessionList) + m_hostSessionList->GetDeleteIsMoveToTrashForHost(GetImapServerKey(), rv); + return rv; +} + +bool nsImapProtocol::GetShowDeletedMessages() { + bool rv = false; + if (m_hostSessionList) + m_hostSessionList->GetShowDeletedMessagesForHost(GetImapServerKey(), rv); + return rv; +} + +bool nsImapProtocol::CheckNeeded() { + if (m_flagChangeCount >= kFlagChangesBeforeCheck) return true; + + int32_t deltaInSeconds; + + PRTime2Seconds(PR_Now() - m_lastCheckTime, &deltaInSeconds); + + return (deltaInSeconds >= kMaxSecondsBeforeCheck); +} + +bool nsImapProtocol::UseCondStore() { + // Check that the server is capable of cond store, and the user + // hasn't disabled the use of constore for this server. + return m_useCondStore && + GetServerStateParser().GetCapabilityFlag() & kHasCondStoreCapability && + GetServerStateParser().fUseModSeq; +} + +bool nsImapProtocol::UseCompressDeflate() { + // Check that the server is capable of compression, and the user + // hasn't disabled the use of compression for this server. + return m_useCompressDeflate && GetServerStateParser().GetCapabilityFlag() & + kHasCompressDeflateCapability; +} + +////////////////////////////////////////////////////////////////////////////////////////////// +// The following is the implementation of nsImapMockChannel and an intermediary +// imap steam listener. The stream listener is used to make a clean binding +// between the imap mock channel and the memory cache channel (if we are reading +// from the cache) +// Used by both offline storage "cache" and by the system cache called "cache2". +////////////////////////////////////////////////////////////////////////////////////////////// + +// WARNING: the cache stream listener is intended to be accessed from the UI +// thread! it will NOT create another proxy for the stream listener that gets +// passed in... +class nsImapCacheStreamListener : public nsIStreamListener { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + + nsImapCacheStreamListener(); + + nsresult Init(nsIStreamListener* aStreamListener, + nsIImapMockChannel* aMockChannelToUse, bool cache2 = false); + + protected: + virtual ~nsImapCacheStreamListener(); + nsCOMPtr<nsIImapMockChannel> mChannelToUse; + nsCOMPtr<nsIStreamListener> mListener; + bool mCache2; // Initialized for cache2 usage + bool mStarting; // Used with cache2. Indicates 1st data segment is read. + + private: + static bool mGoodCache2; + static const uint32_t kPeekBufSize; + static nsresult Peeker(nsIInputStream* aInStr, void* aClosure, + const char* aBuffer, uint32_t aOffset, uint32_t aCount, + uint32_t* aCountWritten); +}; + +NS_IMPL_ADDREF(nsImapCacheStreamListener) +NS_IMPL_RELEASE(nsImapCacheStreamListener) + +NS_INTERFACE_MAP_BEGIN(nsImapCacheStreamListener) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIStreamListener) + NS_INTERFACE_MAP_ENTRY(nsIRequestObserver) + NS_INTERFACE_MAP_ENTRY(nsIStreamListener) +NS_INTERFACE_MAP_END + +nsImapCacheStreamListener::nsImapCacheStreamListener() {} +bool nsImapCacheStreamListener::mGoodCache2 = false; +const uint32_t nsImapCacheStreamListener::kPeekBufSize = 101; + +nsImapCacheStreamListener::~nsImapCacheStreamListener() { mStarting = true; } + +nsresult nsImapCacheStreamListener::Init(nsIStreamListener* aStreamListener, + nsIImapMockChannel* aMockChannelToUse, + bool aCache2 /*false*/) { + NS_ENSURE_ARG(aStreamListener); + NS_ENSURE_ARG(aMockChannelToUse); + + mChannelToUse = aMockChannelToUse; + mListener = aStreamListener; + mCache2 = aCache2; + mStarting = true; + + return NS_OK; +} + +NS_IMETHODIMP +nsImapCacheStreamListener::OnStartRequest(nsIRequest* request) { + if (!mChannelToUse) { + NS_ERROR("OnStartRequest called after OnStopRequest"); + return NS_ERROR_NULL_POINTER; + } + if (!mCache2 || !mStarting) { + return mListener->OnStartRequest(mChannelToUse); + } + return NS_OK; +} + +NS_IMETHODIMP +nsImapCacheStreamListener::OnStopRequest(nsIRequest* request, + nsresult aStatus) { + if (!mListener) { + NS_ERROR("OnStopRequest called twice"); + return NS_ERROR_NULL_POINTER; + } + + nsresult rv = NS_OK; + if (!mCache2 || !mStarting) { + rv = mListener->OnStopRequest(mChannelToUse, aStatus); + + mListener = nullptr; + mChannelToUse->Close(); + mChannelToUse = nullptr; + } + return rv; +} + +/* + * Called when cache2 is in effect on first available data segment returned + * to check that cache entry looks like it it a valid email header. With + * cache2 memory cache this could be done synchronously. But with disk cache + * it can only be done asynchronously like this. + * Note: If NS_OK returned, the peeked at bytes are consumed here and not passed + * on to the listener so a special return value is used. + */ +nsresult nsImapCacheStreamListener::Peeker(nsIInputStream* aInStr, + void* aClosure, const char* aBuffer, + uint32_t aOffset, uint32_t aCount, + uint32_t* aCountWritten) { + char peekBuf[kPeekBufSize]; + aCount = aCount > sizeof peekBuf ? sizeof peekBuf : aCount; + memcpy(peekBuf, aBuffer, aCount); + peekBuf[aCount] = 0; // Null terminate the starting header data. + int32_t findPos = MsgFindCharInSet(nsDependentCString(peekBuf), ":\n\r", 0); + // Check that the first line is a header line, i.e., with a ':' in it + // Or that it begins with "From " because some IMAP servers allow that, + // even though it's technically invalid. + mGoodCache2 = ((findPos != -1 && peekBuf[findPos] == ':') || + !(strncmp(peekBuf, "From ", 5))); + return NS_BASE_STREAM_WOULD_BLOCK; // So stream buffer not "consumed" +} + +NS_IMETHODIMP +nsImapCacheStreamListener::OnDataAvailable(nsIRequest* request, + nsIInputStream* aInStream, + uint64_t aSourceOffset, + uint32_t aCount) { + if (mCache2 && mStarting) { + // Peeker() does check of leading bytes and sets mGoodCache2. + uint32_t numRead; + aInStream->ReadSegments(Peeker, nullptr, kPeekBufSize - 1, &numRead); + MOZ_LOG(IMAPCache, LogLevel::Debug, + ("%s: mGoodCache2=%d(bool)", __func__, mGoodCache2)); + + if (mGoodCache2) { + // Do deferred setup of loadGroup and OnStartRequest and then forward + // the verified first segment to the actual listener. + mStarting = false; + mListener->OnStartRequest(mChannelToUse); + } else { + MOZ_LOG(IMAPCache, LogLevel::Error, + ("%s: cache entry bad so just read imap here", __func__)); + mChannelToUse->ReadFromImapConnection(); + return NS_ERROR_FAILURE; // no more starts, one more stop occurs + } + } + // Forward the segment to the actual listener. + return mListener->OnDataAvailable(mChannelToUse, aInStream, aSourceOffset, + aCount); +} + +NS_IMPL_ISUPPORTS_INHERITED(nsImapMockChannel, nsHashPropertyBag, + nsIImapMockChannel, nsIMailChannel, nsIChannel, + nsIRequest, nsICacheEntryOpenCallback, + nsITransportEventSink, nsISupportsWeakReference) + +nsImapMockChannel::nsImapMockChannel() + : mSuspendedMonitor("nsImapMockChannel"), mSuspended(false) { + m_cancelStatus = NS_OK; + mLoadFlags = 0; + mChannelClosed = false; + mReadingFromCache = false; + mContentLength = mozilla::dom::InternalResponse::UNKNOWN_BODY_SIZE; + mContentDisposition = nsIChannel::DISPOSITION_INLINE; + mWritingToCache = false; +} + +nsImapMockChannel::~nsImapMockChannel() { + // if we're offline, we may not get to close the channel correctly. + // we need to do this to send the url state change notification in + // the case of mem and disk cache reads. + MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread(), + "should only access mock channel on ui thread"); + if (!mChannelClosed) Close(); +} + +nsresult nsImapMockChannel::NotifyStartEndReadFromCache(bool start) { + nsresult rv = NS_OK; + mReadingFromCache = start; + nsCOMPtr<nsIImapUrl> imapUrl = do_QueryInterface(m_url, &rv); + nsCOMPtr<nsIImapProtocol> imapProtocol = do_QueryReferent(mProtocol); + if (imapUrl) { + nsCOMPtr<nsIImapMailFolderSink> folderSink; + rv = imapUrl->GetImapMailFolderSink(getter_AddRefs(folderSink)); + if (folderSink) { + nsCOMPtr<nsIMsgMailNewsUrl> mailUrl = do_QueryInterface(m_url); + rv = folderSink->SetUrlState(nullptr /* we don't know the protocol */, + mailUrl, start, false, m_cancelStatus); + + // Required for killing ImapProtocol thread + if (NS_FAILED(m_cancelStatus) && imapProtocol) + imapProtocol->TellThreadToDie(false); + } + } + return rv; +} + +NS_IMETHODIMP nsImapMockChannel::Close() { + if (mReadingFromCache) + NotifyStartEndReadFromCache(false); + else { + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(m_url); + if (mailnewsUrl) { + nsCOMPtr<nsICacheEntry> cacheEntry; + mailnewsUrl->GetMemCacheEntry(getter_AddRefs(cacheEntry)); + if (cacheEntry) cacheEntry->MarkValid(); + // remove the channel from the load group + nsCOMPtr<nsILoadGroup> loadGroup; + GetLoadGroup(getter_AddRefs(loadGroup)); + // if the mock channel wasn't initialized with a load group then + // use our load group (they may differ) + if (!loadGroup) mailnewsUrl->GetLoadGroup(getter_AddRefs(loadGroup)); + if (loadGroup) + loadGroup->RemoveRequest((nsIRequest*)this, nullptr, NS_OK); + } + } + + m_channelListener = nullptr; + mChannelClosed = true; + return NS_OK; +} + +NS_IMETHODIMP nsImapMockChannel::GetProgressEventSink( + nsIProgressEventSink** aProgressEventSink) { + NS_IF_ADDREF(*aProgressEventSink = mProgressEventSink); + return NS_OK; +} + +NS_IMETHODIMP nsImapMockChannel::SetProgressEventSink( + nsIProgressEventSink* aProgressEventSink) { + mProgressEventSink = aProgressEventSink; + return NS_OK; +} + +NS_IMETHODIMP nsImapMockChannel::GetChannelListener( + nsIStreamListener** aChannelListener) { + NS_IF_ADDREF(*aChannelListener = m_channelListener); + return NS_OK; +} + +// now implement our mock implementation of the channel interface...we forward +// all calls to the real channel if we have one...otherwise we return something +// bogus... + +NS_IMETHODIMP nsImapMockChannel::SetLoadGroup(nsILoadGroup* aLoadGroup) { + m_loadGroup = aLoadGroup; + return NS_OK; +} + +NS_IMETHODIMP nsImapMockChannel::GetLoadGroup(nsILoadGroup** aLoadGroup) { + NS_IF_ADDREF(*aLoadGroup = m_loadGroup); + return NS_OK; +} + +NS_IMETHODIMP nsImapMockChannel::GetTRRMode(nsIRequest::TRRMode* aTRRMode) { + return GetTRRModeImpl(aTRRMode); +} + +NS_IMETHODIMP nsImapMockChannel::SetTRRMode(nsIRequest::TRRMode aTRRMode) { + return SetTRRModeImpl(aTRRMode); +} + +NS_IMETHODIMP nsImapMockChannel::GetLoadInfo(nsILoadInfo** aLoadInfo) { + NS_IF_ADDREF(*aLoadInfo = m_loadInfo); + return NS_OK; +} + +NS_IMETHODIMP nsImapMockChannel::SetLoadInfo(nsILoadInfo* aLoadInfo) { + m_loadInfo = aLoadInfo; + return NS_OK; +} + +NS_IMETHODIMP nsImapMockChannel::GetOriginalURI(nsIURI** aURI) { + // IMap does not seem to have the notion of an original URI :-( + // *aURI = m_originalUrl ? m_originalUrl : m_url; + NS_IF_ADDREF(*aURI = m_url); + return NS_OK; +} + +NS_IMETHODIMP nsImapMockChannel::SetOriginalURI(nsIURI* aURI) { + // IMap does not seem to have the notion of an original URI :-( + // MOZ_ASSERT_UNREACHABLE("nsImapMockChannel::SetOriginalURI"); + // return NS_ERROR_NOT_IMPLEMENTED; + return NS_OK; // ignore +} + +NS_IMETHODIMP nsImapMockChannel::GetURI(nsIURI** aURI) { + NS_IF_ADDREF(*aURI = m_url); + return NS_OK; +} + +NS_IMETHODIMP nsImapMockChannel::SetURI(nsIURI* aURI) { + m_url = aURI; +#ifdef DEBUG_bienvenu + if (!aURI) printf("Clearing URI\n"); +#endif + if (m_url) { + // if we don't have a progress event sink yet, get it from the url for + // now... + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(m_url); + if (mailnewsUrl && !mProgressEventSink) { + nsCOMPtr<nsIMsgStatusFeedback> statusFeedback; + mailnewsUrl->GetStatusFeedback(getter_AddRefs(statusFeedback)); + mProgressEventSink = do_QueryInterface(statusFeedback); + } + // If this is a fetch URL and we can, get the message size from the message + // header and set it to be the content length. + // Note that for an attachment URL, this will set the content length to be + // equal to the size of the entire message. + nsCOMPtr<nsIImapUrl> imapUrl(do_QueryInterface(m_url)); + nsImapAction imapAction; + imapUrl->GetImapAction(&imapAction); + if (imapAction == nsIImapUrl::nsImapMsgFetch) { + nsCOMPtr<nsIMsgMessageUrl> msgUrl(do_QueryInterface(m_url)); + if (msgUrl) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + // A failure to get a message header isn't an error + msgUrl->GetMessageHeader(getter_AddRefs(msgHdr)); + if (msgHdr) { + uint32_t messageSize; + if (NS_SUCCEEDED(msgHdr->GetMessageSize(&messageSize))) + SetContentLength(messageSize); + } + } + } + } + return NS_OK; +} + +NS_IMETHODIMP nsImapMockChannel::Open(nsIInputStream** _retval) { + nsCOMPtr<nsIStreamListener> listener; + nsresult rv = + nsContentSecurityManager::doContentSecurityCheck(this, listener); + NS_ENSURE_SUCCESS(rv, rv); + if (m_url) { + nsCOMPtr<nsIImapUrl> imapUrl(do_QueryInterface(m_url, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + nsImapAction imapAction; + imapUrl->GetImapAction(&imapAction); + // If we're shutting down, and not running the kinds of urls we run at + // shutdown, then this should fail because running urls during + // shutdown will very likely fail and potentially hang. + nsCOMPtr<nsIMsgAccountManager> accountMgr = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + bool shuttingDown = false; + (void)accountMgr->GetShutdownInProgress(&shuttingDown); + if (shuttingDown && imapAction != nsIImapUrl::nsImapExpungeFolder && + imapAction != nsIImapUrl::nsImapDeleteAllMsgs && + imapAction != nsIImapUrl::nsImapDeleteFolder) + return NS_ERROR_FAILURE; + } + return NS_ImplementChannelOpen(this, _retval); +} + +NS_IMETHODIMP +nsImapMockChannel::OnCacheEntryAvailable(nsICacheEntry* entry, bool aNew, + nsresult status) { + if (MOZ_LOG_TEST(IMAPCache, LogLevel::Debug)) { + MOZ_LOG(IMAPCache, LogLevel::Debug, + ("%s: Create/write new cache entry=%s", __func__, + aNew ? "true" : "false")); + if (NS_SUCCEEDED(status)) { + nsAutoCString key; + entry->GetKey(key); + MOZ_LOG(IMAPCache, LogLevel::Debug, + ("%s: Cache entry key = |%s|", __func__, key.get())); + } + } + + // make sure we didn't close the channel before the async call back came in... + // hmmm....if we had write access and we canceled this mock channel then I + // wonder if we should be invalidating the cache entry before kicking out... + if (mChannelClosed) { + if (NS_SUCCEEDED(status)) { + entry->AsyncDoom(nullptr); + } + return NS_OK; + } + + if (!m_url) { + // Something has gone terribly wrong. + NS_WARNING("m_url is null in OnCacheEntryAvailable"); + return Cancel(NS_ERROR_UNEXPECTED); + } + + do { + // For "normal" read/write access we always see status == NS_OK here. aNew + // indicates whether the cache entry is new and needs to be written, or not + // new and can be read. If AsyncOpenURI() was called with access read-only, + // status == NS_ERROR_CACHE_KEY_NOT_FOUND can be received here and we just + // read the data directly from imap. + if (NS_FAILED(status)) { + MOZ_LOG(IMAPCache, LogLevel::Debug, + ("%s: status parameter bad, preference " + "browser.cache.memory.enable not true?", + __func__)); + break; + } + + nsresult rv = NS_OK; + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(m_url, &rv); + mailnewsUrl->SetMemCacheEntry(entry); + + if (aNew) { + // Writing cache so insert a "stream listener Tee" into the stream from + // the imap fetch to direct the message into the cache and to our current + // channel listener. But first get the size of the message to be fetched. + // If message too big to fit in cache, the message just goes to the + // stream listener. If unable to get the size, messageSize remains 0 so + // assume it fits in cache, right or wrong. + uint32_t messageSize = 0; + nsCOMPtr<nsIMsgMessageUrl> msgUrl(do_QueryInterface(m_url)); + if (msgUrl) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + msgUrl->GetMessageHeader(getter_AddRefs(msgHdr)); + if (msgHdr) { + msgHdr->GetMessageSize(&messageSize); + MOZ_LOG(IMAPCache, LogLevel::Debug, + ("%s: messageSize=%d", __func__, messageSize)); + } else + MOZ_LOG(IMAPCache, LogLevel::Debug, + ("%s: Can't get msgHdr", __func__)); + } + // Check if message fits in a cache entry. If too big, or if unable to + // create or initialize the tee or open the stream to the entry, will + // fall thought and only do ReadFromImapConnection() called below and the + // message will not be cached. + bool tooBig = + net::CacheObserver::EntryIsTooBig(messageSize, gUseDiskCache2); + if (!tooBig) { + // Message fits in cache. Create the tee. + nsCOMPtr<nsIStreamListenerTee> tee = + do_CreateInstance(NS_STREAMLISTENERTEE_CONTRACTID, &rv); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIOutputStream> out; + rv = entry->OpenOutputStream(0, -1, getter_AddRefs(out)); + if (NS_SUCCEEDED(rv)) { + rv = tee->Init(m_channelListener, out, nullptr); + m_channelListener = tee; + } else + NS_WARNING( + "IMAP Protocol failed to open output stream to Necko cache"); + } + } + if (tooBig || NS_FAILED(rv)) { + // Need this so next OpenCacheEntry() triggers OnCacheEntryAvailable() + // since nothing was actually written to cache. Without this there is no + // response to next OpenCacheEntry call. + entry->AsyncDoom(nullptr); + MOZ_LOG(IMAPCache, LogLevel::Debug, + ("%s: Not writing to cache, msg too big or other errors", + __func__)); + } else { + mWritingToCache = true; + MOZ_LOG(IMAPCache, LogLevel::Debug, + ("%s: Begin cache WRITE", __func__)); + } + } else { + // We are reading cache (!aNew) + mWritingToCache = false; + if (MOZ_LOG_TEST(IMAPCache, LogLevel::Debug)) { + int64_t size = 0; + rv = entry->GetDataSize(&size); + if (rv == NS_ERROR_IN_PROGRESS) + MOZ_LOG(IMAPCache, LogLevel::Debug, + ("%s: Concurrent cache READ, no size available", __func__)); + MOZ_LOG(IMAPCache, LogLevel::Debug, + ("%s: Begin cache READ, size=%" PRIi64, __func__, size)); + } + rv = ReadFromCache2(entry); + if (NS_SUCCEEDED(rv)) { + NotifyStartEndReadFromCache(true); + entry->MarkValid(); + return NS_OK; // Return here since reading from the cache succeeded. + } + entry->AsyncDoom(nullptr); // Doom entry if we failed to read from cache. + mailnewsUrl->SetMemCacheEntry( + nullptr); // We aren't going to be reading from the cache. + } + } while (false); + + // If reading from the cache failed or if we are writing into the cache, or if + // or message is too big for cache or other errors occur, do + // ReadFromImapConnection to fetch message from imap server. + if (!aNew) + MOZ_LOG(IMAPCache, LogLevel::Debug, + ("%s: Cache READ failed so read from imap", __func__)); + return ReadFromImapConnection(); +} + +NS_IMETHODIMP +nsImapMockChannel::OnCacheEntryCheck(nsICacheEntry* entry, uint32_t* aResult) { + *aResult = nsICacheEntryOpenCallback::ENTRY_WANTED; + + // Check concurrent read: We can't read concurrently since we don't know + // that the entry will ever be written successfully. It may be aborted + // due to a size limitation. If reading concurrently, the following function + // will return NS_ERROR_IN_PROGRESS. Then we tell the cache to wait until + // the write is finished. + int64_t size = 0; + nsresult rv = entry->GetDataSize(&size); + if (rv == NS_ERROR_IN_PROGRESS) { + *aResult = nsICacheEntryOpenCallback::RECHECK_AFTER_WRITE_FINISHED; + MOZ_LOG(IMAPCache, LogLevel::Debug, + ("OnCacheEntryCheck(): Attempted cache write while reading, will " + "try again")); + } + return NS_OK; +} + +nsresult nsImapMockChannel::OpenCacheEntry() { + nsresult rv; + if (!gCache2Storage) { + // Only need to do this once since cache2 is used by all accounts and + // folders. Get the cache storage object from the imap service. + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // Obtain the cache storage object used by all channels in this session. + // This will return disk cache (default) or memory cache as determined by + // the boolean pref "mail.imap.use_disk_cache2" + rv = imapService->GetCacheStorage(getter_AddRefs(gCache2Storage)); + NS_ENSURE_SUCCESS(rv, rv); + MOZ_LOG(IMAPCache, LogLevel::Debug, + ("%s: Obtained storage obj for |%s| cache2", __func__, + gUseDiskCache2 ? "disk" : "mem")); + } + + int32_t uidValidity = -1; + uint32_t cacheAccess = nsICacheStorage::OPEN_NORMALLY; + + nsCOMPtr<nsIImapUrl> imapUrl = do_QueryInterface(m_url, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIImapMailFolderSink> folderSink; + rv = imapUrl->GetImapMailFolderSink(getter_AddRefs(folderSink)); + if (folderSink) folderSink->GetUidValidity(&uidValidity); + + // If we're storing the message in the offline store, don't + // write/save to cache2 cache. (Not sure if this even happens!) + bool storeResultsOffline; + imapUrl->GetStoreResultsOffline(&storeResultsOffline); + if (storeResultsOffline) cacheAccess = nsICacheStorage::OPEN_READONLY; + + // clang-format off + MOZ_LOG(IMAPCache, LogLevel::Debug, + ("%s: For URL = |%s|", __func__, m_url->GetSpecOrDefault().get())); + // clang-format on + + // Use the uid validity as part of the cache key, so that if the uid validity + // changes, we won't re-use the wrong cache entries. + nsAutoCString extension; + extension.AppendInt(uidValidity, 16); + + // Open a cache entry where the key is the potentially modified URL. + nsAutoCString path; + m_url->GetPathQueryRef(path); + + // First we need to "normalise" the URL by extracting ?part= and &filename. + // The path should only contain: ?part=x.y&filename=file.ext + // These are seen in the wild: + // /;section=2?part=1.2&filename=A01.JPG + // ?section=2?part=1.2&filename=A01.JPG&type=image/jpeg&filename=A01.JPG + // ?part=1.2&type=image/jpeg&filename=IMG_C0030.jpg + // ?header=quotebody&part=1.2&filename=lijbmghmkilicioj.png + nsCString partQuery = MsgExtractQueryPart(path, "?part="); + if (partQuery.IsEmpty()) { + partQuery = MsgExtractQueryPart(path, "&part="); + if (!partQuery.IsEmpty()) { + // ? indicates a part query, so set the first character to that. + partQuery.SetCharAt('?', 0); + } + } + nsCString filenameQuery = MsgExtractQueryPart(path, "&filename="); + MOZ_LOG(IMAPCache, LogLevel::Debug, + ("%s: part = |%s|, filename = |%s|", __func__, partQuery.get(), + filenameQuery.get())); + + // Truncate path at either /; or ? + MsgRemoveQueryPart(path); + + nsCOMPtr<nsIURI> newUri; + rv = NS_MutateURI(m_url).SetPathQueryRef(path).Finalize(newUri); + NS_ENSURE_SUCCESS(rv, rv); + if (partQuery.IsEmpty()) { + // Not accessing a part but the whole message. + MOZ_LOG(IMAPCache, LogLevel::Debug, + ("%s: Call AsyncOpenURI on entire message", __func__)); + } else { + // Access just a part. Set up part extraction and read in the part from the + // whole cached message. Note: Parts are now never individually written to + // or read from cache. + SetupPartExtractorListener(imapUrl, m_channelListener); + MOZ_LOG(IMAPCache, LogLevel::Debug, + ("%s: Call AsyncOpenURI to read part from entire message cache", + __func__)); + } + return gCache2Storage->AsyncOpenURI(newUri, extension, cacheAccess, this); +} + +// Pumps content of cache2 entry to channel listener. If a part was +// requested in the original URL seen in OpenCacheEntry(), it will be extracted +// from the whole message by the channel listener. So to obtain a single part +// always requires reading the complete message from cache. +nsresult nsImapMockChannel::ReadFromCache2(nsICacheEntry* entry) { + NS_ENSURE_ARG(entry); + + bool useCacheEntry = true; + nsresult rv; + nsAutoCString entryKey; + + entry->GetKey(entryKey); + + // Compare cache entry size with message size. Init to an invalid value. + int64_t entrySize = -1; + + // We don't expect concurrent read here, so this call should always work. + rv = entry->GetDataSize(&entrySize); + + nsCOMPtr<nsIMsgMessageUrl> msgUrl(do_QueryInterface(m_url)); + if (msgUrl && NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + // A failure to get a message header isn't an automatic error + msgUrl->GetMessageHeader(getter_AddRefs(msgHdr)); + if (msgHdr) { + uint32_t messageSize; + if (NS_SUCCEEDED(rv = msgHdr->GetMessageSize(&messageSize)) && + messageSize != entrySize) { + // clang-format off + MOZ_LOG(IMAP, LogLevel::Warning, + ("%s: Size mismatch for %s: message %" PRIu32 + ", cache %" PRIi64, + __func__, entryKey.get(), messageSize, entrySize)); + MOZ_LOG(IMAPCache, LogLevel::Debug, + ("%s: Size mismatch for %s: message %" PRIu32 + ", cache %" PRIi64, + __func__, entryKey.get(), messageSize, entrySize)); + // clang-format on + useCacheEntry = false; + } + } + } + // Cache entry is invalid if GetDataSize() or GetMessageSize failed or if + // otherwise unable to obtain the cache entry size. (Not sure if it's possible + // to have a 0 length cache entry but negative is definitely invalid.) + if (NS_FAILED(rv) || entrySize < 1) useCacheEntry = false; + + nsCOMPtr<nsIInputStream> ins; + if (useCacheEntry) { + if (NS_SUCCEEDED(rv = entry->OpenInputStream(0, getter_AddRefs(ins)))) { + uint64_t bytesAvailable = 0; + rv = ins->Available(&bytesAvailable); + // Note: bytesAvailable will usually be zero (at least for disk cache + // since only async access occurs) so don't check it. + if (NS_FAILED(rv)) { + MOZ_LOG(IMAPCache, LogLevel::Debug, + ("%s: Input stream for disk cache not useable", __func__)); + useCacheEntry = false; + } + } + } + + if (NS_SUCCEEDED(rv) && useCacheEntry) { + nsCOMPtr<nsIInputStreamPump> pump; + if (NS_SUCCEEDED( + rv = NS_NewInputStreamPump(getter_AddRefs(pump), ins.forget()))) { + // Create and use a cache listener object. + RefPtr<nsImapCacheStreamListener> cacheListener = + new nsImapCacheStreamListener(); + + cacheListener->Init(m_channelListener, this, true); + rv = pump->AsyncRead(cacheListener); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIImapUrl> imapUrl = do_QueryInterface(m_url); + imapUrl->SetMsgLoadingFromCache(true); + // Set the cache entry's security info status as our security + // info status... + nsCOMPtr<nsITransportSecurityInfo> securityInfo; + entry->GetSecurityInfo(getter_AddRefs(securityInfo)); + SetSecurityInfo(securityInfo); + MOZ_LOG(IMAPCache, LogLevel::Debug, + ("%s: Cache entry accepted and being read", __func__)); + } // if AsyncRead succeeded. + } // new pump + } // if useCacheEntry + + if (!useCacheEntry || NS_FAILED(rv)) { + // Cache entry appears to be unusable. Return an error so will still attempt + // to read the data via just an imap fetch (the "old fashioned" way). + if (NS_SUCCEEDED(rv)) rv = NS_ERROR_FAILURE; + MOZ_LOG(IMAPCache, LogLevel::Debug, + ("%s: Cache entry rejected, returning error %" PRIx32, __func__, + static_cast<uint32_t>(rv))); + } + return rv; +} + +class nsReadFromImapConnectionFailure : public mozilla::Runnable { + public: + explicit nsReadFromImapConnectionFailure(nsImapMockChannel* aChannel) + : mozilla::Runnable("nsReadFromImapConnectionFailure"), + mImapMockChannel(aChannel) {} + + NS_IMETHOD Run() { + if (mImapMockChannel) { + mImapMockChannel->RunOnStopRequestFailure(); + } + return NS_OK; + } + + private: + RefPtr<nsImapMockChannel> mImapMockChannel; +}; + +nsresult nsImapMockChannel::RunOnStopRequestFailure() { + if (m_channelListener) { + m_channelListener->OnStopRequest(this, NS_MSG_ERROR_MSG_NOT_OFFLINE); + } + return NS_OK; +} + +// This is called when the message requested by the url isn't yet in offline +// store or not yet in cache. It is also called if the storage is corrupt. This +// creates an imap connection to process the url. This is usually called from +// the mock channel or possibly from nsImapCacheStreamListener::OnDataAvailable. +NS_IMETHODIMP nsImapMockChannel::ReadFromImapConnection() { + nsresult rv = NS_OK; + nsCOMPtr<nsIImapUrl> imapUrl = do_QueryInterface(m_url); + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(m_url); + + bool localOnly = false; + imapUrl->GetLocalFetchOnly(&localOnly); + if (localOnly) { + // This will cause an OnStartRunningUrl, and the subsequent close + // will then cause an OnStopRunningUrl with the cancel status. + NotifyStartEndReadFromCache(true); + Cancel(NS_MSG_ERROR_MSG_NOT_OFFLINE); + + // Dispatch error notification, so ReadFromImapConnection() returns *before* + // the error is sent to the listener's OnStopRequest(). This avoids + // endless recursion where the caller relies on async execution. + nsCOMPtr<nsIRunnable> event = new nsReadFromImapConnectionFailure(this); + NS_DispatchToCurrentThread(event); + return NS_MSG_ERROR_MSG_NOT_OFFLINE; + } + + nsCOMPtr<nsILoadGroup> loadGroup; + GetLoadGroup(getter_AddRefs(loadGroup)); + if (!loadGroup) // if we don't have one, the url will snag one from the msg + // window... + mailnewsUrl->GetLoadGroup(getter_AddRefs(loadGroup)); + + // okay, add the mock channel to the load group.. + if (loadGroup) + loadGroup->AddRequest((nsIRequest*)this, nullptr /* context isupports */); + + // loading the url consists of asking the server to add the url to it's imap + // event queue.... + nsCOMPtr<nsIMsgIncomingServer> server; + rv = mailnewsUrl->GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIImapIncomingServer> imapServer(do_QueryInterface(server, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + // Assume AsyncRead is always called from the UI thread..... + return imapServer->GetImapConnectionAndLoadUrl(imapUrl, m_channelListener); +} + +// for messages stored in our offline cache, we have special code to handle +// that... If it's in the local cache, we return true and we can abort the +// download because this method does the rest of the work. +bool nsImapMockChannel::ReadFromLocalCache() { + nsresult rv = NS_OK; + + nsCOMPtr<nsIImapUrl> imapUrl = do_QueryInterface(m_url); + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(m_url, &rv); + + bool useLocalCache = false; + mailnewsUrl->GetMsgIsInLocalCache(&useLocalCache); + if (!useLocalCache) { + return false; + } + + nsAutoCString messageIdString; + + SetupPartExtractorListener(imapUrl, m_channelListener); + + imapUrl->GetListOfMessageIds(messageIdString); + nsCOMPtr<nsIMsgFolder> folder; + rv = mailnewsUrl->GetFolder(getter_AddRefs(folder)); + NS_ENSURE_SUCCESS(rv, false); + if (!folder) { + return false; + } + // we want to create a file channel and read the msg from there. + nsMsgKey msgKey = strtoul(messageIdString.get(), nullptr, 10); + nsCOMPtr<nsIMsgDBHdr> hdr; + rv = folder->GetMessageHeader(msgKey, getter_AddRefs(hdr)); + NS_ENSURE_SUCCESS(rv, false); + nsCOMPtr<nsIInputStream> msgStream; + rv = folder->GetLocalMsgStream(hdr, getter_AddRefs(msgStream)); + NS_ENSURE_SUCCESS(rv, false); + // dougt - This may break the ablity to "cancel" a read from offline + // mail reading. fileChannel->SetLoadGroup(m_loadGroup); + RefPtr<nsImapCacheStreamListener> cacheListener = + new nsImapCacheStreamListener(); + cacheListener->Init(m_channelListener, this); + + // create a stream pump that will async read the message. + nsCOMPtr<nsIInputStreamPump> pump; + rv = NS_NewInputStreamPump(getter_AddRefs(pump), msgStream.forget()); + NS_ENSURE_SUCCESS(rv, false); + rv = pump->AsyncRead(cacheListener); + NS_ENSURE_SUCCESS(rv, false); + + // if the msg is unread, we should mark it read on the server. This lets + // the code running this url know we're loading from the cache, if it cares. + imapUrl->SetMsgLoadingFromCache(true); + return true; +} + +NS_IMETHODIMP nsImapMockChannel::AsyncOpen(nsIStreamListener* aListener) { + nsCOMPtr<nsIStreamListener> listener = aListener; + nsresult rv = + nsContentSecurityManager::doContentSecurityCheck(this, listener); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t port; + if (!m_url) return NS_ERROR_NULL_POINTER; + rv = m_url->GetPort(&port); + if (NS_FAILED(rv)) return rv; + + rv = NS_CheckPortSafety(port, "imap"); + if (NS_FAILED(rv)) return rv; + + // set the stream listener and then load the url + NS_ASSERTION(!m_channelListener, "shouldn't already have a listener"); + m_channelListener = listener; + nsCOMPtr<nsIImapUrl> imapUrl(do_QueryInterface(m_url)); + + nsImapAction imapAction; + imapUrl->GetImapAction(&imapAction); + + bool externalLink = true; + imapUrl->GetExternalLinkUrl(&externalLink); + + if (externalLink) { + // for security purposes, only allow imap urls originating from external + // sources perform a limited set of actions. Currently the allowed set + // includes: 1) folder selection 2) message fetch 3) message part fetch + + if (!(imapAction == nsIImapUrl::nsImapSelectFolder || + imapAction == nsIImapUrl::nsImapMsgFetch || + imapAction == nsIImapUrl::nsImapOpenMimePart || + imapAction == nsIImapUrl::nsImapMsgFetchPeek)) + return NS_ERROR_FAILURE; // abort the running of this url....it failed a + // security check + } + + if (ReadFromLocalCache()) { + (void)NotifyStartEndReadFromCache(true); + return NS_OK; + } + + // okay, it's not in the local cache, now check the memory cache... + // but we can't download for offline use from the memory cache + if (imapAction != nsIImapUrl::nsImapMsgDownloadForOffline) { + rv = OpenCacheEntry(); + if (NS_SUCCEEDED(rv)) return rv; + } + + SetupPartExtractorListener(imapUrl, m_channelListener); + // if for some reason open cache entry failed then just default to opening an + // imap connection for the url + return ReadFromImapConnection(); +} + +nsresult nsImapMockChannel::SetupPartExtractorListener( + nsIImapUrl* aUrl, nsIStreamListener* aConsumer) { + // if the url we are loading refers to a specific part then we need + // libmime to extract that part from the message for us. + bool refersToPart = false; + aUrl->GetMimePartSelectorDetected(&refersToPart); + if (refersToPart) { + nsCOMPtr<nsIStreamConverterService> converter = + do_GetService("@mozilla.org/streamConverters;1"); + if (converter && aConsumer) { + nsCOMPtr<nsIStreamListener> newConsumer; + converter->AsyncConvertData("message/rfc822", "*/*", aConsumer, + static_cast<nsIChannel*>(this), + getter_AddRefs(newConsumer)); + if (newConsumer) m_channelListener = newConsumer; + } + } + + return NS_OK; +} + +NS_IMETHODIMP nsImapMockChannel::GetLoadFlags(nsLoadFlags* aLoadFlags) { + //*aLoadFlags = nsIRequest::LOAD_NORMAL; + *aLoadFlags = mLoadFlags; + return NS_OK; +} + +NS_IMETHODIMP nsImapMockChannel::SetLoadFlags(nsLoadFlags aLoadFlags) { + mLoadFlags = aLoadFlags; + return NS_OK; // don't fail when trying to set this +} + +NS_IMETHODIMP nsImapMockChannel::GetContentType(nsACString& aContentType) { + if (mContentType.IsEmpty()) { + nsImapAction imapAction = 0; + if (m_url) { + nsCOMPtr<nsIImapUrl> imapUrl = do_QueryInterface(m_url); + if (imapUrl) { + imapUrl->GetImapAction(&imapAction); + } + } + if (imapAction == nsIImapUrl::nsImapSelectFolder) + aContentType.AssignLiteral("x-application-imapfolder"); + else + aContentType.AssignLiteral("message/rfc822"); + } else + aContentType = mContentType; + return NS_OK; +} + +NS_IMETHODIMP nsImapMockChannel::SetContentType( + const nsACString& aContentType) { + nsAutoCString charset; + nsresult rv = + NS_ParseResponseContentType(aContentType, mContentType, charset); + if (NS_FAILED(rv) || mContentType.IsEmpty()) + mContentType.AssignLiteral(UNKNOWN_CONTENT_TYPE); + return rv; +} + +NS_IMETHODIMP nsImapMockChannel::GetContentCharset( + nsACString& aContentCharset) { + aContentCharset.Assign(mCharset); + return NS_OK; +} + +NS_IMETHODIMP nsImapMockChannel::SetContentCharset( + const nsACString& aContentCharset) { + mCharset.Assign(aContentCharset); + return NS_OK; +} + +NS_IMETHODIMP +nsImapMockChannel::GetContentDisposition(uint32_t* aContentDisposition) { + *aContentDisposition = mContentDisposition; + return NS_OK; +} + +NS_IMETHODIMP +nsImapMockChannel::SetContentDisposition(uint32_t aContentDisposition) { + mContentDisposition = aContentDisposition; + return NS_OK; +} + +NS_IMETHODIMP +nsImapMockChannel::GetContentDispositionFilename( + nsAString& aContentDispositionFilename) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsImapMockChannel::SetContentDispositionFilename( + const nsAString& aContentDispositionFilename) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsImapMockChannel::GetContentDispositionHeader( + nsACString& aContentDispositionHeader) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP nsImapMockChannel::GetContentLength(int64_t* aContentLength) { + *aContentLength = mContentLength; + return NS_OK; +} + +NS_IMETHODIMP +nsImapMockChannel::SetContentLength(int64_t aContentLength) { + mContentLength = aContentLength; + return NS_OK; +} + +NS_IMETHODIMP nsImapMockChannel::GetOwner(nsISupports** aPrincipal) { + NS_IF_ADDREF(*aPrincipal = mOwner); + return NS_OK; +} + +NS_IMETHODIMP nsImapMockChannel::SetOwner(nsISupports* aPrincipal) { + mOwner = aPrincipal; + return NS_OK; +} + +NS_IMETHODIMP nsImapMockChannel::GetSecurityInfo( + nsITransportSecurityInfo** aSecurityInfo) { + NS_IF_ADDREF(*aSecurityInfo = mSecurityInfo); + return NS_OK; +} + +NS_IMETHODIMP nsImapMockChannel::SetSecurityInfo( + nsITransportSecurityInfo* aSecurityInfo) { + mSecurityInfo = aSecurityInfo; + return NS_OK; +} + +NS_IMETHODIMP +nsImapMockChannel::GetIsDocument(bool* aIsDocument) { + return NS_GetIsDocumentChannel(this, aIsDocument); +} + +//////////////////////////////////////////////////////////////////////////////// +// From nsIRequest +//////////////////////////////////////////////////////////////////////////////// + +NS_IMETHODIMP nsImapMockChannel::GetName(nsACString& result) { + if (m_url) return m_url->GetSpec(result); + result.Truncate(); + return NS_OK; +} + +NS_IMETHODIMP nsImapMockChannel::IsPending(bool* result) { + *result = m_channelListener != nullptr; + return NS_OK; +} + +NS_IMETHODIMP nsImapMockChannel::GetStatus(nsresult* status) { + *status = m_cancelStatus; + return NS_OK; +} + +NS_IMETHODIMP nsImapMockChannel::SetWritingToCache(bool aWriting) { + mWritingToCache = aWriting; + return NS_OK; +} + +NS_IMETHODIMP nsImapMockChannel::GetWritingToCache(bool* result) { + *result = mWritingToCache; + return NS_OK; +} + +NS_IMETHODIMP nsImapMockChannel::SetImapProtocol(nsIImapProtocol* aProtocol) { + mProtocol = do_GetWeakReference(aProtocol); + return NS_OK; +} + +NS_IMETHODIMP nsImapMockChannel::SetCanceledReason(const nsACString& aReason) { + return SetCanceledReasonImpl(aReason); +} + +NS_IMETHODIMP nsImapMockChannel::GetCanceledReason(nsACString& aReason) { + return GetCanceledReasonImpl(aReason); +} + +NS_IMETHODIMP nsImapMockChannel::CancelWithReason(nsresult aStatus, + const nsACString& aReason) { + return CancelWithReasonImpl(aStatus, aReason); +} + +NS_IMETHODIMP nsImapMockChannel::Cancel(nsresult status) { + MOZ_DIAGNOSTIC_ASSERT( + NS_IsMainThread(), + "nsImapMockChannel::Cancel should only be called from UI thread"); + MOZ_LOG(IMAPCache, LogLevel::Debug, + ("nsImapMockChannel::%s: entering", __func__)); + m_cancelStatus = status; + nsCOMPtr<nsIImapProtocol> imapProtocol = do_QueryReferent(mProtocol); + + // if we aren't reading from the cache and we get canceled...doom our cache + // entry if write is still in progress... + if (m_url) { + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(m_url); + MOZ_LOG(IMAPCache, LogLevel::Debug, + ("%s: Doom cache entry only if writing=%d(bool), url=%s", __func__, + mWritingToCache, m_url->GetSpecOrDefault().get())); + if (mWritingToCache) DoomCacheEntry(mailnewsUrl); + } + + // The associated ImapProtocol thread must be unblocked before being killed. + // Otherwise, it will be deadlocked. + ResumeAndNotifyOne(); + + // Required for killing ImapProtocol thread + if (imapProtocol) imapProtocol->TellThreadToDie(false); + + return NS_OK; +} + +NS_IMETHODIMP nsImapMockChannel::GetCanceled(bool* aCanceled) { + nsresult status = NS_ERROR_FAILURE; + GetStatus(&status); + *aCanceled = NS_FAILED(status); + return NS_OK; +} + +/** + * Suspends the current request. This may have the effect of closing + * any underlying transport (in order to free up resources), although + * any open streams remain logically opened and will continue delivering + * data when the transport is resumed. + * + * Calling cancel() on a suspended request must not send any + * notifications (such as onstopRequest) until the request is resumed. + * + * NOTE: some implementations are unable to immediately suspend, and + * may continue to deliver events already posted to an event queue. In + * general, callers should be capable of handling events even after + * suspending a request. + */ +NS_IMETHODIMP nsImapMockChannel::Suspend() { + MOZ_LOG(IMAP, LogLevel::Debug, ("Suspending [this=%p].", this)); + + mozilla::MonitorAutoLock lock(mSuspendedMonitor); + NS_ENSURE_TRUE(!mSuspended, NS_ERROR_NOT_AVAILABLE); + mSuspended = true; + + MOZ_LOG(IMAP, LogLevel::Debug, ("Suspended [this=%p].", this)); + + return NS_OK; +} + +/** + * Resumes the current request. This may have the effect of re-opening + * any underlying transport and will resume the delivery of data to + * any open streams. + */ +NS_IMETHODIMP nsImapMockChannel::Resume() { + MOZ_LOG(IMAP, LogLevel::Debug, ("Resuming [this=%p].", this)); + + nsresult rv = ResumeAndNotifyOne(); + + MOZ_LOG(IMAP, LogLevel::Debug, ("Resumed [this=%p].", this)); + + return rv; +} + +nsresult nsImapMockChannel::ResumeAndNotifyOne() { + mozilla::MonitorAutoLock lock(mSuspendedMonitor); + NS_ENSURE_TRUE(mSuspended, NS_ERROR_NOT_AVAILABLE); + mSuspended = false; + lock.Notify(); + + return NS_OK; +} + +NS_IMETHODIMP +nsImapMockChannel::GetNotificationCallbacks( + nsIInterfaceRequestor** aNotificationCallbacks) { + NS_IF_ADDREF(*aNotificationCallbacks = mCallbacks.get()); + return NS_OK; +} + +NS_IMETHODIMP +nsImapMockChannel::SetNotificationCallbacks( + nsIInterfaceRequestor* aNotificationCallbacks) { + mCallbacks = aNotificationCallbacks; + return NS_OK; +} + +NS_IMETHODIMP +nsImapMockChannel::OnTransportStatus(nsITransport* transport, nsresult status, + int64_t progress, int64_t progressMax) { + if (NS_FAILED(m_cancelStatus) || (mLoadFlags & LOAD_BACKGROUND) || !m_url) + return NS_OK; + + // these transport events should not generate any status messages + if (status == NS_NET_STATUS_RECEIVING_FROM || + status == NS_NET_STATUS_SENDING_TO) + return NS_OK; + + if (!mProgressEventSink) { + NS_QueryNotificationCallbacks(mCallbacks, m_loadGroup, mProgressEventSink); + if (!mProgressEventSink) return NS_OK; + } + + nsAutoCString host; + m_url->GetHost(host); + + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(m_url); + if (mailnewsUrl) { + nsCOMPtr<nsIMsgIncomingServer> server; + mailnewsUrl->GetServer(getter_AddRefs(server)); + if (server) server->GetHostName(host); + } + mProgressEventSink->OnStatus(this, status, NS_ConvertUTF8toUTF16(host).get()); + + return NS_OK; +} + +nsIMAPMailboxInfo::nsIMAPMailboxInfo(const nsACString& aName, char aDelimiter) { + mMailboxName.Assign(aName); + mDelimiter = aDelimiter; + mChildrenListed = false; +} + +nsIMAPMailboxInfo::~nsIMAPMailboxInfo() {} + +void nsIMAPMailboxInfo::SetChildrenListed(bool childrenListed) { + mChildrenListed = childrenListed; +} + +bool nsIMAPMailboxInfo::GetChildrenListed() { return mChildrenListed; } + +const nsACString& nsIMAPMailboxInfo::GetMailboxName() { return mMailboxName; } + +char nsIMAPMailboxInfo::GetDelimiter() { return mDelimiter; } diff --git a/comm/mailnews/imap/src/nsImapProtocol.h b/comm/mailnews/imap/src/nsImapProtocol.h new file mode 100644 index 0000000000..815ac103dc --- /dev/null +++ b/comm/mailnews/imap/src/nsImapProtocol.h @@ -0,0 +1,848 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsImapProtocol_h___ +#define nsImapProtocol_h___ + +#include "mozilla/Attributes.h" +#include "nsIImapProtocol.h" +#include "nsIImapUrl.h" + +#include "nsMsgProtocol.h" +#include "nsIStreamListener.h" +#include "nsIAsyncOutputStream.h" +#include "nsIAsyncInputStream.h" +#include "nsImapCore.h" +#include "nsString.h" +#include "nsIProgressEventSink.h" +#include "nsIInterfaceRequestor.h" +#include "nsISocketTransport.h" + +// UI Thread proxy helper +#include "nsIImapProtocolSink.h" + +#include "../public/nsIImapHostSessionList.h" +#include "nsImapServerResponseParser.h" +#include "nsImapFlagAndUidState.h" +#include "nsImapNamespace.h" +#include "nsTArray.h" +#include "nsIWeakReferenceUtils.h" +#include "nsMsgLineBuffer.h" // we need this to use the nsMsgLineStreamBuffer helper class... +#include "nsIInputStream.h" +#include "nsIMsgIncomingServer.h" +#include "nsCOMArray.h" +#include "nsIThread.h" +#include "nsIRunnable.h" +#include "nsIImapMockChannel.h" +#include "nsILoadGroup.h" +#include "nsCOMPtr.h" +#include "nsIMsgWindow.h" +#include "nsIImapHeaderXferInfo.h" +#include "nsMsgLineBuffer.h" +#include "nsIAsyncInputStream.h" +#include "nsIMsgFolder.h" +#include "nsIMsgAsyncPrompter.h" +#include "mozilla/ReentrantMonitor.h" +#include "nsSyncRunnableHelpers.h" +#include "nsICacheEntryOpenCallback.h" +#include "nsIProtocolProxyCallback.h" +#include "nsIStringBundle.h" +#include "nsHashPropertyBag.h" +#include "nsMailChannel.h" + +#include "mozilla/Monitor.h" + +class nsIMAPMessagePartID; +class nsIPrefBranch; + +#define kDownLoadCacheSize 16000u // was 1536 - try making it bigger + +typedef struct _msg_line_info { + const char* adoptedMessageLine; + uint32_t uidOfMessage; +} msg_line_info; + +class nsMsgImapLineDownloadCache : public nsIImapHeaderInfo, + public nsByteArray { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIIMAPHEADERINFO + nsMsgImapLineDownloadCache(); + uint32_t CurrentUID(); + uint32_t SpaceAvailable(); + bool CacheEmpty(); + + msg_line_info* GetCurrentLineInfo(); + + private: + virtual ~nsMsgImapLineDownloadCache(); + + msg_line_info* fLineInfo; + int32_t m_msgSize; +}; + +#define kNumHdrsToXfer 10 + +class nsMsgImapHdrXferInfo : public nsIImapHeaderXferInfo { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIIMAPHEADERXFERINFO + nsMsgImapHdrXferInfo(); + void ResetAll(); // reset HeaderInfos for re-use + void ReleaseAll(); // release HeaderInfos (frees up memory) + // this will return null if we're full, in which case the client code + // should transfer the headers and retry. + nsIImapHeaderInfo* StartNewHdr(); + // call when we've finished adding lines to current hdr + void FinishCurrentHdr(); + + private: + virtual ~nsMsgImapHdrXferInfo(); + nsCOMArray<nsIImapHeaderInfo> m_hdrInfos; + int32_t m_nextFreeHdrInfo; +}; + +// This class contains the name of a mailbox and whether or not +// its children have been listed. +class nsIMAPMailboxInfo { + public: + nsIMAPMailboxInfo(const nsACString& aName, char aDelimiter); + virtual ~nsIMAPMailboxInfo(); + + void SetChildrenListed(bool childrenListed); + bool GetChildrenListed(); + const nsACString& GetMailboxName(); + char GetDelimiter(); + + protected: + nsCString mMailboxName; + bool mChildrenListed; + char mDelimiter; +}; + +// State Flags (Note, I use the word state in terms of storing +// state information about the connection (authentication, have we sent +// commands, etc. I do not intend it to refer to protocol state) +// Use these flags in conjunction with SetFlag/TestFlag/ClearFlag instead +// of creating PRBools for everything.... +// clang-format off +#define IMAP_RECEIVED_GREETING 0x00000001 // should we pause for the next read +#define IMAP_CONNECTION_IS_OPEN 0x00000004 // is the connection currently open? +#define IMAP_WAITING_FOR_DATA 0x00000008 +#define IMAP_CLEAN_UP_URL_STATE 0x00000010 // processing clean up url state +#define IMAP_ISSUED_LANGUAGE_REQUEST 0x00000020 // make sure we only issue the language + // request once per connection... +#define IMAP_ISSUED_COMPRESS_REQUEST 0x00000040 // make sure we only request compression once + +// There are 3 types of progress strings for items downloaded from IMAP servers. +// An index is needed to keep track of the current count of the number of each +// item type downloaded. The IMAP_EMPTY_STRING_INDEX means no string displayed. +#define IMAP_NUMBER_OF_PROGRESS_STRINGS 4 +#define IMAP_HEADERS_STRING_INDEX 0 +#define IMAP_FLAGS_STRING_INDEX 1 +#define IMAP_MESSAGES_STRING_INDEX 2 +#define IMAP_EMPTY_STRING_INDEX 3 +// clang-format on + +/** + * nsImapProtocol is, among other things, the underlying nsIChannel + * implementation for the IMAP protocol. However, it's usually hidden away + * behind nsImapMockChannel objects. It also represents the 'real' connection + * to the IMAP server - it maintains the nsISocketTransport. + * Because there can be multiple IMAP requests queued up, NS_NewChannel() + * will return nsImapMockChannel objects instead, to keep the request in a + * holding pattern until the connection is free. At which time the mock + * channel will just forward calls onward to the nsImapProtocol. + * + * The url scheme we implement here encodes various IMAP commands as URLs. + * Some URLs are just traditional I/O based nsIChannel transactions, but + * many others have side effects. For example, an IMAP folder discovery + * command might cause the creation of the nsImapMailFolder hierarchy under + * the the nsImapIncomingServer. + * Such side effects are communicated via the various "Sink" interfaces. This + * helps decouple the IMAP code from the rest of the system: + * + * - nsIImapServerSink (implemented by nsImapIncomingServer) + * - nsIImapMailFolderSink (implemented by nsImapMailFolder) + * - nsIImapMessageSink (implemented by nsImapMailFolder) + * + * Internal to nsImapProtocol, these sink classes all have corresponding proxy + * implementations (ImapServerSinkProxy, ImapMailFolderSinkProxy and + * ImapMessageSinkProxy). These allow us to safely call the sink objects, in + * a synchronous fashion, from I/O threads (threads other than the main one). + * When an IMAP routine calls a member function of one of these sink proxies, + * it dispatches a call to the real sink object on the main thread, then + * blocks until the call is completed. + */ +class nsImapProtocol : public nsIImapProtocol, + public nsIInputStreamCallback, + public nsSupportsWeakReference, + public nsMsgProtocol, + public nsIImapProtocolSink, + public nsIMsgAsyncPromptListener, + public nsIProtocolProxyCallback { + public: + struct TCPKeepalive { + // For enabling and setting TCP keepalive (not related to IMAP IDLE). + std::atomic<bool> enabled; + std::atomic<int32_t> idleTimeS; + std::atomic<int32_t> retryIntervalS; + }; + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIINPUTSTREAMCALLBACK + NS_DECL_NSIPROTOCOLPROXYCALLBACK + nsImapProtocol(); + + virtual nsresult ProcessProtocolState(nsIURI* url, + nsIInputStream* inputStream, + uint64_t sourceOffset, + uint32_t length) override; + + ////////////////////////////////////////////////////////////////////////////////// + // we support the nsIImapProtocol interface + ////////////////////////////////////////////////////////////////////////////////// + NS_DECL_NSIIMAPPROTOCOL + + ////////////////////////////////////////////////////////////////////////////////// + // we support the nsIImapProtocolSink interface + ////////////////////////////////////////////////////////////////////////////////// + NS_DECL_NSIIMAPPROTOCOLSINK + + NS_DECL_NSIMSGASYNCPROMPTLISTENER + + // message id string utilities. + uint32_t CountMessagesInIdString(const char* idString); + static bool HandlingMultipleMessages(const nsCString& messageIdString); + // escape slashes and double quotes in username/passwords for insecure login. + static void EscapeUserNamePasswordString(const char* strToEscape, + nsCString* resultStr); + + // used to start fetching a message. + void GetShouldDownloadAllHeaders(bool* aResult); + void GetArbitraryHeadersToDownload(nsCString& aResult); + virtual void AdjustChunkSize(); + virtual void FetchMessage(const nsCString& messageIds, + nsIMAPeFetchFields whatToFetch, + const char* fetchModifier = nullptr, + uint32_t startByte = 0, uint32_t numBytes = 0, + char* part = 0); + void FetchTryChunking(const nsCString& messageIds, + nsIMAPeFetchFields whatToFetch, bool idIsUid, + char* part, uint32_t downloadSize, bool tryChunking); + void FallbackToFetchWholeMsg(const nsCString& messageId, + uint32_t messageSize); + // used when streaming a message fetch + virtual nsresult BeginMessageDownLoad( + uint32_t totalSize, // for user, headers and body + const char* contentType); // some downloads are header only + virtual void HandleMessageDownLoadLine(const char* line, bool isPartialLine, + char* lineCopy = nullptr); + virtual void NormalMessageEndDownload(); + virtual void AbortMessageDownLoad(); + virtual void PostLineDownLoadEvent(const char* line, uint32_t uid); + void FlushDownloadCache(); + + virtual void SetMailboxDiscoveryStatus(EMailboxDiscoverStatus status); + virtual EMailboxDiscoverStatus GetMailboxDiscoveryStatus(); + + virtual void ProcessMailboxUpdate(bool handlePossibleUndo); + // Send log output... + void Log(const char* logSubName, const char* extraInfo, const char* logData); + static void LogImapUrl(const char* logMsg, nsIImapUrl* imapUrl); + // Comment from 4.5: We really need to break out the thread synchronizer from + // the connection class...Not sure what this means + bool GetPseudoInterrupted(); + + uint32_t GetMessageSize(const nsACString& messageId); + bool GetSubscribingNow(); + + bool DeathSignalReceived(); + void ResetProgressInfo(); + void SetActive(bool active); + bool GetActive(); + + bool GetShowAttachmentsInline(); + + // Sets whether or not the content referenced by the current ActiveEntry has + // been modified. Used for MIME parts on demand. + void SetContentModified(IMAP_ContentModifiedType modified); + bool GetShouldFetchAllParts(); + bool GetIgnoreExpunges() { return m_ignoreExpunges; } + // Generic accessors required by the imap parser + char* CreateNewLineFromSocket(); + nsresult GetConnectionStatus(); + void SetConnectionStatus(nsresult status); + + // Cleanup the connection and shutdown the thread. + void TellThreadToDie(); + + const nsCString& + GetImapHostName(); // return the host name from the url for the + // current connection + const nsCString& GetImapUserName(); // return the user name from the identity + const char* + GetImapServerKey(); // return the user name from the incoming server; + + // state set by the imap parser... + void NotifyMessageFlags(imapMessageFlagsType flags, + const nsACString& keywords, nsMsgKey key, + uint64_t highestModSeq); + void NotifySearchHit(const char* hitLine); + + // Event handlers for the imap parser. + void DiscoverMailboxSpec(nsImapMailboxSpec* adoptedBoxSpec); + void AlertUserEventUsingName(const char* aMessageId); + void AlertUserEvent(const char* message); + void AlertUserEventFromServer(const char* aServerEvent, + bool aForIdle = false); + + void ProgressEventFunctionUsingName(const char* aMsgId); + void ProgressEventFunctionUsingNameWithString(const char* aMsgName, + const char* aExtraInfo); + void PercentProgressUpdateEvent(nsACString const& fmtStringName, + nsAString const& mailbox, + int64_t currentProgress, int64_t maxProgress); + void ShowProgress(); + + // utility function calls made by the server + + void Copy(const char* messageList, const char* destinationMailbox, + bool idsAreUid); + void Search(const char* searchCriteria, bool useUID, bool notifyHit = true); + // imap commands issued by the parser + void Store(const nsCString& aMessageList, const char* aMessageData, + bool aIdsAreUid); + void ProcessStoreFlags(const nsCString& messageIds, bool idsAreUids, + imapMessageFlagsType flags, bool addFlags); + void IssueUserDefinedMsgCommand(const char* command, const char* messageList); + void FetchMsgAttribute(const nsCString& messageIds, + const nsCString& attribute); + void Expunge(); + void UidExpunge(const nsCString& messageSet); + void ImapClose(bool shuttingDown = false, bool waitForResponse = true); + void Check(); + void SelectMailbox(const char* mailboxName); + // more imap commands + void Logout(bool shuttingDown = false, bool waitForResponse = true); + void Noop(); + void XServerInfo(); + void Netscape(); + void XMailboxInfo(const char* mailboxName); + void XAOL_Option(const char* option); + void MailboxData(); + void GetMyRightsForFolder(const char* mailboxName); + void Bodystructure(const nsCString& messageId, bool idIsUid); + + // this function does not ref count!!! be careful!!! + nsIImapUrl* GetCurrentUrl() { return m_runningUrl; } + + // acl and namespace stuff + // notifies libmsg that we have a new personal/default namespace that we're + // using + void CommitNamespacesForHostEvent(); + // notifies libmsg that we have new capability data for the current host + void CommitCapability(); + + // Adds a set of rights for a given user on a given mailbox on the current + // host. if userName is NULL, it means "me," or MYRIGHTS. rights is a single + // string of rights, as specified by RFC2086, the IMAP ACL extension. + void AddFolderRightsForUser(const char* mailboxName, const char* userName, + const char* rights); + // Clears all rights for the current folder, for all users. + void ClearAllFolderRights(); + void RefreshFolderACLView(const char* mailboxName, + nsImapNamespace* nsForMailbox); + + nsresult SetFolderAdminUrl(const char* mailboxName); + void HandleMemoryFailure(); + void HandleCurrentUrlError(); + + // UIDPLUS extension + void SetCopyResponseUid(const char* msgIdString); + + // Quota support + void UpdateFolderQuotaData(nsImapQuotaAction aAction, nsCString& aQuotaRoot, + uint64_t aUsed, uint64_t aMax); + + bool GetPreferPlainText() { return m_preferPlainText; } + + int32_t GetCurFetchSize() { return m_curFetchSize; } + + const nsString& GetEmptyMimePartString() { return m_emptyMimePartString; } + void SetCapabilityResponseOccurred(); + + // Start event loop. This is called on IMAP thread + bool RunImapThreadMainLoop(); + + private: + virtual ~nsImapProtocol(); + // the following flag is used to determine when a url is currently being run. + // It is cleared when we finish processng a url and it is set whenever we call + // Load on a url + bool m_urlInProgress; + + /** The nsIImapURL that is currently running. */ + nsCOMPtr<nsIImapUrl> m_runningUrl; + nsCOMPtr<nsIImapUrl> m_runningUrlLatest; + /** Current imap action associated with this connection. */ + nsImapAction m_imapAction; + + nsCString m_hostName; + nsCString m_userName; + nsCString m_serverKey; + char* m_dataOutputBuf; + RefPtr<nsMsgLineStreamBuffer> m_inputStreamBuffer; + nsCString m_trashFolderPath; + + /** The socket connection to the IMAP server. */ + nsCOMPtr<nsISocketTransport> m_transport; + nsCOMPtr<nsITransportSecurityInfo> m_securityInfo; + + /** Stream to handle data coming in from the IMAP server. */ + nsCOMPtr<nsIInputStream> m_inputStream; + + nsCOMPtr<nsIAsyncInputStream> m_channelInputStream; + nsCOMPtr<nsIAsyncOutputStream> m_channelOutputStream; + + /** The currently running request. */ + nsCOMPtr<nsIImapMockChannel> m_mockChannel; + + uint32_t m_bytesToChannel; + bool m_fetchingWholeMessage; + // nsCOMPtr<nsIRequest> mAsyncReadRequest; // we're going to cancel this when + // we're done with the conn. + + // ******* Thread support ******* + PRThread* m_thread; + mozilla::ReentrantMonitor + m_dataAvailableMonitor; // used to notify the arrival of data from the + // server + mozilla::ReentrantMonitor + m_urlReadyToRunMonitor; // used to notify the arrival of a new url to be + // processed + mozilla::ReentrantMonitor m_pseudoInterruptMonitor; + mozilla::ReentrantMonitor m_dataMemberMonitor; + mozilla::ReentrantMonitor m_threadDeathMonitor; + mozilla::ReentrantMonitor m_waitForBodyIdsMonitor; + mozilla::ReentrantMonitor m_fetchBodyListMonitor; + mozilla::ReentrantMonitor m_passwordReadyMonitor; + mozilla::Mutex mLock; + // If we get an async password prompt, this is where the UI thread + // stores the password, before notifying the imap thread of the password + // via the m_passwordReadyMonitor. + nsString m_password; + // Set to the result of nsImapServer::PromptPassword + nsresult m_passwordStatus; + bool m_passwordObtained; + + bool m_imapThreadIsRunning; + void ImapThreadMainLoop(void); + nsresult m_connectionStatus; + nsCString m_connectionType; + + bool m_nextUrlReadyToRun; + bool m_idleResponseReadyToHandle; + nsWeakPtr m_server; + + RefPtr<ImapMailFolderSinkProxy> m_imapMailFolderSink; + RefPtr<ImapMailFolderSinkProxy> m_imapMailFolderSinkSelected; + RefPtr<ImapMessageSinkProxy> m_imapMessageSink; + RefPtr<ImapServerSinkProxy> m_imapServerSink; + RefPtr<ImapServerSinkProxy> m_imapServerSinkLatest; + RefPtr<ImapProtocolSinkProxy> m_imapProtocolSink; + + // helper function to setup imap sink interface proxies + nsresult SetupSinkProxy(); + // End thread support stuff + nsresult LoadImapUrlInternal(); + + bool GetDeleteIsMoveToTrash(); + bool GetShowDeletedMessages(); + nsCString m_currentCommand; + nsImapServerResponseParser m_parser; + nsImapServerResponseParser& GetServerStateParser() { return m_parser; } + + bool HandleIdleResponses(); + virtual bool ProcessCurrentURL(); + void EstablishServerConnection(); + virtual void ParseIMAPandCheckForNewMail(const char* commandString = nullptr, + bool ignoreBadNOResponses = false); + // biff + void PeriodicBiff(); + void SendSetBiffIndicatorEvent(nsMsgBiffState newState); + + // folder opening and listing header functions + void FolderHeaderDump(uint32_t* msgUids, uint32_t msgCount); + void FolderMsgDump(uint32_t* msgUids, uint32_t msgCount, + nsIMAPeFetchFields fields); + void FolderMsgDumpLoop(uint32_t* msgUids, uint32_t msgCount, + nsIMAPeFetchFields fields); + void WaitForPotentialListOfBodysToFetch(nsTArray<nsMsgKey>& msgIdList); + void HeaderFetchCompleted(); + void UploadMessageFromFile(nsIFile* file, const char* mailboxName, + PRTime date, imapMessageFlagsType flags, + nsCString& keywords); + + // mailbox name utilities. + void CreateEscapedMailboxName(const char* rawName, nsCString& escapedName); + void SetupMessageFlagsString(nsCString& flagString, + imapMessageFlagsType flags, uint16_t userFlags); + + // body fetching listing data + bool m_fetchBodyListIsNew; + nsTArray<nsMsgKey> m_fetchBodyIdList; + + // initialization function given a new url and transport layer + nsresult SetupWithUrl(nsIURI* aURL, nsISupports* aConsumer); + nsresult SetupWithUrlCallback(nsIProxyInfo* proxyInfo); + void ReleaseUrlState(bool rerunningUrl); // release any state that is stored + // on a per action basis. + /** + * Last ditch effort to run the url without using an imap connection. + * If it turns out that we don't need to run the url at all (e.g., we're + * trying to download a single message for offline use and it has already + * been downloaded, this function will send the appropriate notifications. + * + * @returns true if the url has been run locally, or doesn't need to be run. + */ + bool TryToRunUrlLocally(nsIURI* aURL, nsISupports* aConsumer); + + //////////////////////////////////////////////////////////////////////////////////////// + // Communication methods --> Reading and writing protocol + //////////////////////////////////////////////////////////////////////////////////////// + + // SendData not only writes the NULL terminated data in dataBuffer to our + // output stream but it also informs the consumer that the data has been + // written to the stream. aSuppressLogging --> set to true if you wish to + // suppress logging for this particular command. this is useful for making + // sure we don't log authentication information like the user's password + // (which was encoded anyway), but still we shouldn't add that information to + // the log. + nsresult SendData(const char* dataBuffer, + bool aSuppressLogging = false) override; + + // state ported over from 4.5 + bool m_pseudoInterrupted; + bool m_active; + bool m_folderNeedsSubscribing; + bool m_folderNeedsACLRefreshed; + + bool m_threadShouldDie; + + // use to prevent re-entering TellThreadToDie. + bool m_inThreadShouldDie; + // If the UI thread has signalled the IMAP thread to die, and the + // connection has timed out, this will be set to FALSE. + bool m_safeToCloseConnection; + + RefPtr<nsImapFlagAndUidState> m_flagState; + nsMsgBiffState m_currentBiffState; + // manage the IMAP server command tags + // 11 = enough memory for the decimal representation of MAX_UINT + trailing + // nul + char m_currentServerCommandTag[11]; + uint32_t m_currentServerCommandTagNumber; + void IncrementCommandTagNumber(); + const char* GetServerCommandTag(); + + void StartTLS(); + + // login related methods. + nsresult GetPassword(nsString& password, bool aNewPasswordRequested); + void InitPrefAuthMethods(int32_t authMethodPrefValue, + nsIMsgIncomingServer* aServer); + nsresult ChooseAuthMethod(); + void MarkAuthMethodAsFailed(eIMAPCapabilityFlags failedAuthMethod); + void ResetAuthMethods(); + + // All of these methods actually issue protocol + void Capability(); // query host for capabilities. + void ID(); // send RFC 2971 app info to server + void EnableUTF8Accept(); + void EnableCondStore(); + void StartCompressDeflate(); + nsresult BeginCompressing(); + void Language(); // set the language on the server if it supports it + void Namespace(); + void InsecureLogin(const char* userName, const nsCString& password); + nsresult ClientID(); + nsresult AuthLogin(const char* userName, const nsString& password, + eIMAPCapabilityFlag flag); + nsresult SendDataParseIMAPandCheckForNewMail(const char* data, + const char* command); + void ProcessAuthenticatedStateURL(); + void ProcessAfterAuthenticated(); + void ProcessSelectedStateURL(); + bool TryToLogon(); + + // ProcessAuthenticatedStateURL() used to be one giant if statement. I've + // broken out a set of actions based on the imap action passed into the url. + // The following functions are imap protocol handlers for each action. They + // are called by ProcessAuthenticatedStateUrl. + void OnLSubFolders(); + void OnAppendMsgFromFile(); + + char* GetFolderPathString(); // OK to call from UI thread + + char* OnCreateServerSourceFolderPathString(); + char* OnCreateServerDestinationFolderPathString(); + nsresult CreateServerSourceFolderPathString(char** result); + void OnCreateFolder(const char* aSourceMailbox); + void OnEnsureExistsFolder(const char* aSourceMailbox); + void OnSubscribe(const char* aSourceMailbox); + void OnUnsubscribe(const char* aSourceMailbox); + void RefreshACLForFolderIfNecessary(const char* mailboxName); + void RefreshACLForFolder(const char* aSourceMailbox); + void GetACLForFolder(const char* aMailboxName); + void OnRefreshAllACLs(); + void OnListFolder(const char* aSourceMailbox, bool aBool); + void OnStatusForFolder(const char* sourceMailbox); + void OnDeleteFolder(const char* aSourceMailbox); + void OnRenameFolder(const char* aSourceMailbox); + void OnMoveFolderHierarchy(const char* aSourceMailbox); + void DeleteFolderAndMsgs(const char* aSourceMailbox); + void RemoveMsgsAndExpunge(); + void FindMailboxesIfNecessary(); + void CreateMailbox(const char* mailboxName); + void DeleteMailbox(const char* mailboxName); + void RenameMailbox(const char* existingName, const char* newName); + void RemoveHierarchyDelimiter(nsCString& mailboxName); + bool CreateMailboxRespectingSubscriptions(const char* mailboxName); + bool DeleteMailboxRespectingSubscriptions(const char* mailboxName); + bool RenameMailboxRespectingSubscriptions(const char* existingName, + const char* newName, + bool reallyRename); + // notify the fe that a folder was deleted + void FolderDeleted(const char* mailboxName); + // notify the fe that a folder creation failed + void FolderNotCreated(const char* mailboxName); + // notify the fe that a folder was deleted + void FolderRenamed(const char* oldName, const char* newName); + + bool FolderIsSelected(const char* mailboxName); + + bool MailboxIsNoSelectMailbox(const char* mailboxName); + bool FolderNeedsACLInitialized(const char* folderName); + void DiscoverMailboxList(); + void DiscoverAllAndSubscribedBoxes(); + void MailboxDiscoveryFinished(); + void NthLevelChildList(const char* onlineMailboxPrefix, int32_t depth); + // LIST SUBSCRIBED command (from RFC 5258) crashes some servers. so we need to + // identify those servers + bool GetListSubscribedIsBrokenOnServer(); + void Lsub(const char* mailboxPattern, bool addDirectoryIfNecessary); + void List(const char* mailboxPattern, bool addDirectoryIfNecessary, + bool useXLIST = false); + void Subscribe(const char* mailboxName); + void Unsubscribe(const char* mailboxName); + void Idle(); + void EndIdle(bool waitForResponse = true); + // Some imap servers include the mailboxName following the dir-separator in + // the list of subfolders of the mailboxName. In fact, they are the same. So + // we should decide if we should delete such subfolder and provide feedback if + // the delete operation succeed. + bool DeleteSubFolders(const char* aMailboxName, bool& aDeleteSelf); + bool RenameHierarchyByHand(const char* oldParentMailboxName, + const char* newParentMailboxName); + bool RetryUrl(); + + nsresult GlobalInitialization(nsIPrefBranch* aPrefBranch); + nsresult Configure(int32_t TooFastTime, int32_t IdealTime, + int32_t ChunkAddSize, int32_t ChunkSize, + int32_t ChunkThreshold, bool FetchByChunks); + nsresult GetMsgWindow(nsIMsgWindow** aMsgWindow); + // End Process AuthenticatedState Url helper methods + + virtual char const* GetType() override { return "imap"; } + + // Quota support + void GetQuotaDataIfSupported(const char* aBoxName); + + // CondStore support - true if server supports it, and the user hasn't + // disabled it. + bool UseCondStore(); + // false if pref "mail.server.serverxxx.use_condstore" is false; + bool m_useCondStore; + // COMPRESS=DEFLATE support - true if server supports it, and the user hasn't + // disabled it. + bool UseCompressDeflate(); + // false if pref "mail.server.serverxxx.use_compress_deflate" is false; + bool m_useCompressDeflate; + // these come from the nsIDBFolderInfo in the msgDatabase and + // are initialized in nsImapProtocol::SetupWithUrl. + uint64_t mFolderLastModSeq; + int32_t mFolderTotalMsgCount; + uint32_t mFolderHighestUID; + bool m_allowUTF8Accept; + + bool m_isGmailServer; + nsTArray<nsCString> mCustomDBHeaders; + nsTArray<nsCString> mCustomHeaders; + bool m_trackingTime; + PRTime m_startTime; + PRTime m_endTime; + PRTime m_lastActiveTime; + int32_t m_tooFastTime; + int32_t m_idealTime; + int32_t m_chunkAddSize; + int32_t m_chunkStartSize; + bool m_fetchByChunks; + bool m_sendID; + int32_t m_curFetchSize; + bool m_ignoreExpunges; + eIMAPCapabilityFlags m_prefAuthMethods; // set of capability flags (in + // nsImapCore.h) for auth methods + eIMAPCapabilityFlags m_failedAuthMethods; // ditto + eIMAPCapabilityFlag m_currentAuthMethod; // exactly one capability flag, or 0 + int32_t m_socketType; + int32_t m_chunkSize; + int32_t m_chunkThreshold; + RefPtr<nsMsgImapLineDownloadCache> m_downloadLineCache; + RefPtr<nsMsgImapHdrXferInfo> m_hdrDownloadCache; + nsCOMPtr<nsIImapHeaderInfo> m_curHdrInfo; + // mapping between mailboxes and the corresponding folder flags + nsTHashMap<nsCStringHashKey, int32_t> m_standardListMailboxes; + // mapping between special xlist mailboxes and the corresponding folder flags + nsTHashMap<nsCStringHashKey, int32_t> m_specialXListMailboxes; + + nsCOMPtr<nsIImapHostSessionList> m_hostSessionList; + + bool m_fromHeaderSeen; + + nsString mAcceptLanguages; + + nsCString m_clientId; + + // progress stuff + void SetProgressString(uint32_t aStringIndex); + + nsCString m_progressStringName; + uint32_t m_stringIndex; + int32_t m_progressCurrentNumber[IMAP_NUMBER_OF_PROGRESS_STRINGS]; + int32_t m_progressExpectedNumber; + nsCString m_lastProgressStringName; + int32_t m_lastPercent; + int64_t m_lastProgressTime; + + bool m_notifySearchHit; + bool m_needNoop; + bool m_idle; + bool m_useIdle; + int32_t m_noopCount; + bool m_autoSubscribe, m_autoUnsubscribe, m_autoSubscribeOnOpen; + bool m_closeNeededBeforeSelect; + bool m_retryUrlOnError; + bool m_preferPlainText; + bool m_forceSelect; + + int32_t m_uidValidity; // stored uid validity for the selected folder. + + enum EMailboxHierarchyNameState { + kNoOperationInProgress, + // kDiscoverBaseFolderInProgress, - Unused. Keeping for historical reasons. + kDiscoverTrashFolderInProgress, + kDeleteSubFoldersInProgress, + kListingForInfoOnly, + kListingForInfoAndDiscovery, + kDiscoveringNamespacesOnly, + kXListing, + kListingForFolderFlags, + kListingForCreate + }; + EMailboxHierarchyNameState m_hierarchyNameState; + EMailboxDiscoverStatus m_discoveryStatus; + nsTArray<nsIMAPMailboxInfo*> m_listedMailboxList; + nsTArray<nsCString>* m_deletableChildren; + uint32_t m_flagChangeCount; + PRTime m_lastCheckTime; + + bool CheckNeeded(); + + nsString m_emptyMimePartString; + + RefPtr<mozilla::mailnews::OAuth2ThreadHelper> mOAuth2Support; + bool m_capabilityResponseOccurred; + + nsresult IsTransportAlive(bool* alive); + nsresult TransportStartTLS(); + void GetTransportSecurityInfo(nsITransportSecurityInfo** aSecurityInfo); +}; + +// This small class is a "mock" channel because it is a mockery of the imap +// channel's implementation... it's a light weight channel that we can return to +// necko when they ask for a channel on a url before we actually have an imap +// protocol instance around which can run the url. Please see my comments in +// nsIImapMockChannel.idl for more details.. +// +// Threading concern: This class lives entirely in the UI thread. + +class nsICacheEntry; + +class nsImapMockChannel : public nsIImapMockChannel, + public nsICacheEntryOpenCallback, + public nsITransportEventSink, + public nsSupportsWeakReference, + public nsMailChannel, + public nsHashPropertyBag { + public: + friend class nsImapProtocol; + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIIMAPMOCKCHANNEL + NS_DECL_NSICHANNEL + NS_DECL_NSIREQUEST + NS_DECL_NSICACHEENTRYOPENCALLBACK + NS_DECL_NSITRANSPORTEVENTSINK + + nsImapMockChannel(); + static nsresult Create(const nsIID& iid, void** result); + nsresult RunOnStopRequestFailure(); + + protected: + virtual ~nsImapMockChannel(); + nsCOMPtr<nsIURI> m_url; + + nsCOMPtr<nsIURI> m_originalUrl; + nsCOMPtr<nsILoadGroup> m_loadGroup; + nsCOMPtr<nsILoadInfo> m_loadInfo; + nsCOMPtr<nsIStreamListener> m_channelListener; + nsresult m_cancelStatus; + nsLoadFlags mLoadFlags; + nsCOMPtr<nsIProgressEventSink> mProgressEventSink; + nsCOMPtr<nsIInterfaceRequestor> mCallbacks; + nsCOMPtr<nsISupports> mOwner; + nsCOMPtr<nsITransportSecurityInfo> mSecurityInfo; + nsCString mContentType; + nsCString mCharset; + nsWeakPtr mProtocol; + + bool mChannelClosed; + bool mReadingFromCache; + int64_t mContentLength; + bool mWritingToCache; + + mozilla::Monitor mSuspendedMonitor; + bool mSuspended; + + nsresult ResumeAndNotifyOne(); + + // cache related helper methods + nsresult OpenCacheEntry(); // makes a request to the cache service for a + // cache entry for a url + bool ReadFromLocalCache(); // attempts to read the url out of our local + // (offline) cache.... + nsresult ReadFromCache2(nsICacheEntry* entry); // pipes message from cache2 + // entry to channel listener + nsresult NotifyStartEndReadFromCache(bool start); + + // we end up daisy chaining multiple nsIStreamListeners into the load process. + nsresult SetupPartExtractorListener(nsIImapUrl* aUrl, + nsIStreamListener* aConsumer); + + uint32_t mContentDisposition; +}; + +#endif // nsImapProtocol_h___ diff --git a/comm/mailnews/imap/src/nsImapSearchResults.cpp b/comm/mailnews/imap/src/nsImapSearchResults.cpp new file mode 100644 index 0000000000..0932126a46 --- /dev/null +++ b/comm/mailnews/imap/src/nsImapSearchResults.cpp @@ -0,0 +1,72 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" // for pre-compiled headers + +#include "nsImapSearchResults.h" +#include "prmem.h" +#include "nsCRT.h" + +nsImapSearchResultSequence::nsImapSearchResultSequence() {} + +nsImapSearchResultSequence* +nsImapSearchResultSequence::CreateSearchResultSequence() { + return new nsImapSearchResultSequence; +} + +void nsImapSearchResultSequence::Clear(void) { + int32_t i = Length(); + while (0 <= --i) { + char* string = ElementAt(i); + PR_Free(string); + } + nsTArray<char*>::Clear(); +} + +nsImapSearchResultSequence::~nsImapSearchResultSequence() { Clear(); } + +void nsImapSearchResultSequence::ResetSequence() { Clear(); } + +void nsImapSearchResultSequence::AddSearchResultLine(const char* searchLine) { + // The first add becomes node 2. Fix this. + char* copiedSequence = PL_strdup(searchLine + 9); // 9 == "* SEARCH " + + if (copiedSequence) // if we can't allocate this then the search won't hit + AppendElement(copiedSequence); +} + +nsImapSearchResultIterator::nsImapSearchResultIterator( + nsImapSearchResultSequence& sequence) + : fSequence(sequence) { + ResetIterator(); +} + +nsImapSearchResultIterator::~nsImapSearchResultIterator() {} + +void nsImapSearchResultIterator::ResetIterator() { + fSequenceIndex = 0; + fCurrentLine = (char*)fSequence.SafeElementAt(fSequenceIndex); + fPositionInCurrentLine = fCurrentLine; +} + +int32_t nsImapSearchResultIterator::GetNextMessageNumber() { + int32_t returnValue = 0; + if (fPositionInCurrentLine) { + returnValue = atoi(fPositionInCurrentLine); + + // eat the current number + while (isdigit(*++fPositionInCurrentLine)) + ; + + if (*fPositionInCurrentLine == 0xD) // found CR, no more digits on line + { + fCurrentLine = (char*)fSequence.SafeElementAt(++fSequenceIndex); + fPositionInCurrentLine = fCurrentLine; + } else // eat the space + fPositionInCurrentLine++; + } + + return returnValue; +} diff --git a/comm/mailnews/imap/src/nsImapSearchResults.h b/comm/mailnews/imap/src/nsImapSearchResults.h new file mode 100644 index 0000000000..4eca197e29 --- /dev/null +++ b/comm/mailnews/imap/src/nsImapSearchResults.h @@ -0,0 +1,40 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsImapSearchResults_h___ +#define nsImapSearchResults_h___ + +#include "nsTArray.h" + +class nsImapSearchResultSequence : public nsTArray<char*> { + public: + virtual ~nsImapSearchResultSequence(); + static nsImapSearchResultSequence* CreateSearchResultSequence(); + + virtual void AddSearchResultLine(const char* searchLine); + virtual void ResetSequence(); + void Clear(); + + friend class nsImapSearchResultIterator; + + private: + nsImapSearchResultSequence(); +}; + +class nsImapSearchResultIterator { + public: + explicit nsImapSearchResultIterator(nsImapSearchResultSequence& sequence); + virtual ~nsImapSearchResultIterator(); + + void ResetIterator(); + int32_t GetNextMessageNumber(); // returns 0 at end of list + private: + nsImapSearchResultSequence& fSequence; + int32_t fSequenceIndex; + char* fCurrentLine; + char* fPositionInCurrentLine; +}; + +#endif diff --git a/comm/mailnews/imap/src/nsImapServerResponseParser.cpp b/comm/mailnews/imap/src/nsImapServerResponseParser.cpp new file mode 100644 index 0000000000..a52279b6c9 --- /dev/null +++ b/comm/mailnews/imap/src/nsImapServerResponseParser.cpp @@ -0,0 +1,2640 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" // for pre-compiled headers +#include "nsMimeTypes.h" +#include "nsImapCore.h" +#include "nsImapProtocol.h" +#include "nsImapServerResponseParser.h" +#include "nsIImapFlagAndUidState.h" +#include "nsImapNamespace.h" +#include "nsImapUtils.h" +#include "nsCRT.h" +#include "nsMsgUtils.h" +#include "mozilla/Logging.h" + +////////////////// nsImapServerResponseParser ///////////////////////// + +extern mozilla::LazyLogModule IMAP; // defined in nsImapProtocol.cpp + +nsImapServerResponseParser::nsImapServerResponseParser( + nsImapProtocol& imapProtocolConnection) + : nsImapGenericParser(), + fReportingErrors(true), + fCurrentFolderReadOnly(false), + fCurrentLineContainedFlagInfo(false), + fServerIsNetscape3xServer(false), + fSeqNumOfFirstUnseenMsg(0), + fNumberOfExistingMessages(0), + fNumberOfRecentMessages(0), + fSizeOfMostRecentMessage(0), + fTotalDownloadSize(0), + fCurrentCommandTag(nullptr), + fSelectedMailboxName(nullptr), + fIMAPstate(kNonAuthenticated), + fLastChunk(false), + fNextChunkStartsWithNewline(false), + fServerConnection(imapProtocolConnection), + fHostSessionList(nullptr) { + fSearchResults = nsImapSearchResultSequence::CreateSearchResultSequence(); + fFolderAdminUrl = nullptr; + fNetscapeServerVersionString = nullptr; + fXSenderInfo = nullptr; + fSupportsUserDefinedFlags = 0; + fSettablePermanentFlags = 0; + fCapabilityFlag = kCapabilityUndefined; + fLastAlert = nullptr; + fDownloadingHeaders = false; + fGotPermanentFlags = false; + fFolderUIDValidity = 0; + fHighestModSeq = 0; + fAuthChallenge = nullptr; + fStatusUnseenMessages = 0; + fStatusRecentMessages = 0; + fStatusNextUID = nsMsgKey_None; + fNextUID = nsMsgKey_None; + fStatusExistingMessages = 0; + fReceivedHeaderOrSizeForUID = nsMsgKey_None; + fUtf8AcceptEnabled = false; + fStdJunkNotJunkUseOk = false; +} + +nsImapServerResponseParser::~nsImapServerResponseParser() { + PR_Free(fCurrentCommandTag); + delete fSearchResults; + PR_Free(fFolderAdminUrl); + PR_Free(fNetscapeServerVersionString); + PR_Free(fXSenderInfo); + PR_Free(fLastAlert); + PR_Free(fSelectedMailboxName); + PR_Free(fAuthChallenge); +} + +bool nsImapServerResponseParser::LastCommandSuccessful() { + return (!CommandFailed() && !fServerConnection.DeathSignalReceived() && + nsImapGenericParser::LastCommandSuccessful()); +} + +// returns true if things look ok to continue +bool nsImapServerResponseParser::GetNextLineForParser(char** nextLine) { + bool rv = true; + *nextLine = fServerConnection.CreateNewLineFromSocket(); + if (fServerConnection.DeathSignalReceived() || + NS_FAILED(fServerConnection.GetConnectionStatus())) + rv = false; + // we'd really like to try to silently reconnect, but we shouldn't put this + // message up just in the interrupt case + if (NS_FAILED(fServerConnection.GetConnectionStatus()) && + !fServerConnection.DeathSignalReceived()) + fServerConnection.AlertUserEventUsingName("imapServerDisconnected"); + return rv; +} + +bool nsImapServerResponseParser::CommandFailed() { + return fCurrentCommandFailed; +} + +void nsImapServerResponseParser::SetCommandFailed(bool failed) { + fCurrentCommandFailed = failed; +} + +bool nsImapServerResponseParser::UntaggedResponse() { + return fUntaggedResponse; +} + +void nsImapServerResponseParser::SetFlagState(nsIImapFlagAndUidState* state) { + fFlagState = state; +} + +uint32_t nsImapServerResponseParser::SizeOfMostRecentMessage() { + return fSizeOfMostRecentMessage; +} + +// Call this when adding a pipelined command to the session +void nsImapServerResponseParser::IncrementNumberOfTaggedResponsesExpected( + const char* newExpectedTag) { + fNumberOfTaggedResponsesExpected++; + PR_Free(fCurrentCommandTag); + fCurrentCommandTag = PL_strdup(newExpectedTag); + if (!fCurrentCommandTag) HandleMemoryFailure(); +} + +void nsImapServerResponseParser::InitializeState() { + fCurrentCommandFailed = false; + fNumberOfRecentMessages = 0; + fReceivedHeaderOrSizeForUID = nsMsgKey_None; + fUntaggedResponse = false; +} + +// RFC3501: response = *(continue-req / response-data) response-done +// response-data = "*" SP (resp-cond-state / resp-cond-bye / +// mailbox-data / message-data / capability-data) CRLF +// continue-req = "+" SP (resp-text / base64) CRLF +void nsImapServerResponseParser::ParseIMAPServerResponse( + const char* aCurrentCommand, bool aIgnoreBadAndNOResponses, + char* aGreetingWithCapability) { + NS_ASSERTION(aCurrentCommand && *aCurrentCommand != '\r' && + *aCurrentCommand != '\n' && *aCurrentCommand != ' ', + "Invalid command string"); + bool sendingIdleDone = !strcmp(aCurrentCommand, "DONE" CRLF); + if (sendingIdleDone) fWaitingForMoreClientInput = false; + + // Reinitialize the parser + SetConnected(true); + SetSyntaxError(false); + + // Reinitialize our state + InitializeState(); + + // the default is to not pipeline + fNumberOfTaggedResponsesExpected = 1; + int numberOfTaggedResponsesReceived = 0; + + nsCString copyCurrentCommand(aCurrentCommand); + if (!fServerConnection.DeathSignalReceived()) { + char* placeInTokenString = nullptr; + char* tagToken = nullptr; + const char* commandToken = nullptr; + bool inIdle = false; + if (!sendingIdleDone) { + placeInTokenString = copyCurrentCommand.BeginWriting(); + tagToken = NS_strtok(WHITESPACE, &placeInTokenString); + commandToken = NS_strtok(WHITESPACE, &placeInTokenString); + } else + commandToken = "DONE"; + if (tagToken) { + PR_Free(fCurrentCommandTag); + fCurrentCommandTag = PL_strdup(tagToken); + if (!fCurrentCommandTag) HandleMemoryFailure(); + inIdle = commandToken && !strcmp(commandToken, "IDLE"); + } + + if (commandToken && ContinueParse()) + PreProcessCommandToken(commandToken, aCurrentCommand); + + if (ContinueParse()) { + ResetLexAnalyzer(); + + if (aGreetingWithCapability) { + PR_FREEIF(fCurrentLine); + fCurrentLine = aGreetingWithCapability; + } + + // When inIdle, only one pass through "do" and "while" below occurs. + do { + AdvanceToNextToken(); + + // untagged responses [RFC3501, Sec. 2.2.2] + while (ContinueParse() && fNextToken && *fNextToken == '*') { + response_data(); + if (ContinueParse()) { + if (!fAtEndOfLine) + SetSyntaxError(true); + else if (!inIdle && !fCurrentCommandFailed && + !aGreetingWithCapability) + AdvanceToNextToken(); + } + // For checking expected response to IDLE command below and + // for checking if an untagged response occurred by the caller. + fUntaggedResponse = true; + } + + // command continuation request [RFC3501, Sec. 7.5] + if (ContinueParse() && fNextToken && + *fNextToken == '+') // never pipeline APPEND or AUTHENTICATE + { + NS_ASSERTION( + (fNumberOfTaggedResponsesExpected - + numberOfTaggedResponsesReceived) == 1, + " didn't get the number of tagged responses we expected"); + numberOfTaggedResponsesReceived = fNumberOfTaggedResponsesExpected; + if (commandToken && !PL_strcasecmp(commandToken, "authenticate") && + placeInTokenString && + (!PL_strncasecmp(placeInTokenString, "CRAM-MD5", + strlen("CRAM-MD5")) || + !PL_strncasecmp(placeInTokenString, "NTLM", strlen("NTLM")) || + !PL_strncasecmp(placeInTokenString, "GSSAPI", + strlen("GSSAPI")) || + !PL_strncasecmp(placeInTokenString, "MSN", strlen("MSN")))) { + // we need to store the challenge from the server if we are using + // CRAM-MD5 or NTLM. + authChallengeResponse_data(); + } + } else + numberOfTaggedResponsesReceived++; + + if (numberOfTaggedResponsesReceived < fNumberOfTaggedResponsesExpected) + response_tagged(); + + } while ( + ContinueParse() && !inIdle && + (numberOfTaggedResponsesReceived < fNumberOfTaggedResponsesExpected)); + + // check and see if the server is waiting for more input + // it's possible that we ate this + while parsing certain responses (like + // cram data), in these cases, the parsing routine for that specific + // command will manually set fWaitingForMoreClientInput so we don't lose + // that information.... + if ((fNextToken && *fNextToken == '+') || inIdle) { + if (inIdle && + !((fNextToken && *fNextToken == '+') || fUntaggedResponse)) { + // IDLE "response" + will not be "eaten" as described above since it + // is not an authentication response. So if IDLE response does not + // begin with '+' (continuation) or '*' (untagged and probably useful + // response) then something is wrong and it is probably a tagged + // NO or BAD due to transient error or bad configuration of the + // server. + if (!PL_strcmp(fCurrentCommandTag, fNextToken)) { + response_tagged(); + } else { + // Expected tag doesn't match the received tag. Not good, start + // over. + response_fatal(); + } + // Show an alert notification containing the server response to bad + // IDLE. + fServerConnection.AlertUserEventFromServer(fCurrentLine, true); + } else { + fWaitingForMoreClientInput = true; + } + } + // if we aren't still waiting for more input.... + else if (!fWaitingForMoreClientInput && !aGreetingWithCapability) { + if (ContinueParse()) response_done(); + + if (ContinueParse() && !CommandFailed()) { + // a successful command may change the eIMAPstate + ProcessOkCommand(commandToken); + } else if (CommandFailed()) { + // a failed command may change the eIMAPstate + ProcessBadCommand(commandToken); + if (fReportingErrors && !aIgnoreBadAndNOResponses) + fServerConnection.AlertUserEventFromServer(fCurrentLine, false); + } + } + } + } else + SetConnected(false); +} + +void nsImapServerResponseParser::HandleMemoryFailure() { + fServerConnection.AlertUserEventUsingName("imapOutOfMemory"); + nsImapGenericParser::HandleMemoryFailure(); +} + +// SEARCH is the only command that requires pre-processing for now. +// others will be added here. +void nsImapServerResponseParser::PreProcessCommandToken( + const char* commandToken, const char* currentCommand) { + fCurrentCommandIsSingleMessageFetch = false; + fWaitingForMoreClientInput = false; + + if (!PL_strcasecmp(commandToken, "SEARCH")) + fSearchResults->ResetSequence(); + else if (!PL_strcasecmp(commandToken, "SELECT") && currentCommand) { + // the mailbox name must be quoted, so strip the quotes + const char* openQuote = PL_strchr(currentCommand, '"'); + NS_ASSERTION(openQuote, "expected open quote in imap server response"); + if (!openQuote) { // ill formed select command + openQuote = PL_strchr(currentCommand, ' '); + } + PR_Free(fSelectedMailboxName); + fSelectedMailboxName = PL_strdup(openQuote + 1); + if (fSelectedMailboxName) { + // strip the escape chars and the ending quote + char* currentChar = fSelectedMailboxName; + while (*currentChar) { + if (*currentChar == '\\') { + PL_strcpy(currentChar, currentChar + 1); + currentChar++; // skip what we are escaping + } else if (*currentChar == '\"') + *currentChar = 0; // end quote + else + currentChar++; + } + } else + HandleMemoryFailure(); + + // we don't want bogus info for this new box + // delete fFlagState; // not our object + // fFlagState = nullptr; + } else if (!PL_strcasecmp(commandToken, "CLOSE")) { + return; // just for debugging + // we don't want bogus info outside the selected state + // delete fFlagState; // not our object + // fFlagState = nullptr; + } else if (!PL_strcasecmp(commandToken, "UID")) { + nsCString copyCurrentCommand(currentCommand); + if (!fServerConnection.DeathSignalReceived()) { + char* placeInTokenString = copyCurrentCommand.BeginWriting(); + (void)NS_strtok(WHITESPACE, &placeInTokenString); // skip tag token + (void)NS_strtok(WHITESPACE, &placeInTokenString); // skip uid token + char* fetchToken = NS_strtok(WHITESPACE, &placeInTokenString); + if (!PL_strcasecmp(fetchToken, "FETCH")) { + char* uidStringToken = NS_strtok(WHITESPACE, &placeInTokenString); + // , and : are uid delimiters + if (!PL_strchr(uidStringToken, ',') && !PL_strchr(uidStringToken, ':')) + fCurrentCommandIsSingleMessageFetch = true; + } + } + } +} + +const char* nsImapServerResponseParser::GetSelectedMailboxName() { + return fSelectedMailboxName; +} + +nsImapSearchResultIterator* +nsImapServerResponseParser::CreateSearchResultIterator() { + return new nsImapSearchResultIterator(*fSearchResults); +} + +nsImapServerResponseParser::eIMAPstate +nsImapServerResponseParser::GetIMAPstate() { + return fIMAPstate; +} + +void nsImapServerResponseParser::PreauthSetAuthenticatedState() { + fIMAPstate = kAuthenticated; +} + +void nsImapServerResponseParser::ProcessOkCommand(const char* commandToken) { + if (!PL_strcasecmp(commandToken, "LOGIN") || + !PL_strcasecmp(commandToken, "AUTHENTICATE")) + fIMAPstate = kAuthenticated; + else if (!PL_strcasecmp(commandToken, "LOGOUT")) + fIMAPstate = kNonAuthenticated; + else if (!PL_strcasecmp(commandToken, "SELECT") || + !PL_strcasecmp(commandToken, "EXAMINE")) + fIMAPstate = kFolderSelected; + else if (!PL_strcasecmp(commandToken, "CLOSE")) { + fIMAPstate = kAuthenticated; + // we no longer have a selected mailbox. + PR_FREEIF(fSelectedMailboxName); + } else if ((!PL_strcasecmp(commandToken, "LIST")) || + (!PL_strcasecmp(commandToken, "LSUB")) || + (!PL_strcasecmp(commandToken, "XLIST"))) { + // fServerConnection.MailboxDiscoveryFinished(); + // This used to be reporting that we were finished + // discovering folders for each time we issued a + // LIST or LSUB. So if we explicitly listed the + // INBOX, or Trash, or namespaces, we would get multiple + // "done" states, even though we hadn't finished. + // Move this to be called from the connection object + // itself. + } else if (!PL_strcasecmp(commandToken, "FETCH")) { + if (!fZeroLengthMessageUidString.IsEmpty()) { + // "Deleting zero length message"); + fServerConnection.Store(fZeroLengthMessageUidString, "+Flags (\\Deleted)", + true); + if (LastCommandSuccessful()) fServerConnection.Expunge(); + + fZeroLengthMessageUidString.Truncate(); + } + } else if (!PL_strcasecmp(commandToken, "GETQUOTAROOT")) { + if (LastCommandSuccessful()) { + nsCString str; + fServerConnection.UpdateFolderQuotaData(kValidateQuota, str, 0, 0); + } + } +} + +void nsImapServerResponseParser::ProcessBadCommand(const char* commandToken) { + if (!PL_strcasecmp(commandToken, "LOGIN") || + !PL_strcasecmp(commandToken, "AUTHENTICATE")) + fIMAPstate = kNonAuthenticated; + else if (!PL_strcasecmp(commandToken, "LOGOUT")) + fIMAPstate = kNonAuthenticated; // ?? + else if (!PL_strcasecmp(commandToken, "SELECT") || + !PL_strcasecmp(commandToken, "EXAMINE")) + fIMAPstate = kAuthenticated; // nothing selected + else if (!PL_strcasecmp(commandToken, "CLOSE")) + fIMAPstate = kAuthenticated; // nothing selected +} + +// RFC3501: response-data = "*" SP (resp-cond-state / resp-cond-bye / +// mailbox-data / message-data / capability-data) CRLF +// These are ``untagged'' responses [RFC3501, Sec. 2.2.2] +/* + The RFC1730 grammar spec did not allow one symbol look ahead to determine + between mailbox_data / message_data so I combined the numeric possibilities + of mailbox_data and all of message_data into numeric_mailbox_data. + + It is assumed that the initial "*" is already consumed before calling this + method. The production implemented here is + response_data ::= (resp_cond_state / resp_cond_bye / + mailbox_data / numeric_mailbox_data / + capability_data) + CRLF +*/ +void nsImapServerResponseParser::response_data() { + AdvanceToNextToken(); + + if (ContinueParse()) { + // Instead of comparing lots of strings and make function calls, try to + // pre-flight the possibilities based on the first letter of the token. + switch (NS_ToUpper(fNextToken[0])) { + case 'O': // OK + if (NS_ToUpper(fNextToken[1]) == 'K') + resp_cond_state(false); + else + SetSyntaxError(true); + break; + case 'N': // NO + if (NS_ToUpper(fNextToken[1]) == 'O') + resp_cond_state(false); + else if (!PL_strcasecmp(fNextToken, "NAMESPACE")) + namespace_data(); + else + SetSyntaxError(true); + break; + case 'B': // BAD + if (!PL_strcasecmp(fNextToken, "BAD")) + resp_cond_state(false); + else if (!PL_strcasecmp(fNextToken, "BYE")) + resp_cond_bye(); + else + SetSyntaxError(true); + break; + case 'F': + if (!PL_strcasecmp(fNextToken, "FLAGS")) + mailbox_data(); + else + SetSyntaxError(true); + break; + case 'P': + if (PL_strcasecmp(fNextToken, "PERMANENTFLAGS")) + mailbox_data(); + else + SetSyntaxError(true); + break; + case 'L': + if (!PL_strcasecmp(fNextToken, "LIST") || + !PL_strcasecmp(fNextToken, "LSUB")) + mailbox_data(); + else if (!PL_strcasecmp(fNextToken, "LANGUAGE")) + language_data(); + else + SetSyntaxError(true); + break; + case 'M': + if (!PL_strcasecmp(fNextToken, "MAILBOX")) + mailbox_data(); + else if (!PL_strcasecmp(fNextToken, "MYRIGHTS")) + myrights_data(false); + else + SetSyntaxError(true); + break; + case 'S': + if (!PL_strcasecmp(fNextToken, "SEARCH")) + mailbox_data(); + else if (!PL_strcasecmp(fNextToken, "STATUS")) { + AdvanceToNextToken(); + if (fNextToken) { + char* mailboxName = CreateAstring(); + PL_strfree(mailboxName); + } + while (ContinueParse() && !fAtEndOfLine) { + AdvanceToNextToken(); + if (!fNextToken) break; + + if (*fNextToken == '(') fNextToken++; + if (!PL_strcasecmp(fNextToken, "UIDNEXT")) { + AdvanceToNextToken(); + if (fNextToken) { + fStatusNextUID = strtoul(fNextToken, nullptr, 10); + // if this token ends in ')', then it is the last token + // else we advance + if (*(fNextToken + strlen(fNextToken) - 1) == ')') + fNextToken += strlen(fNextToken) - 1; + } + } else if (!PL_strcasecmp(fNextToken, "MESSAGES")) { + AdvanceToNextToken(); + if (fNextToken) { + fStatusExistingMessages = strtoul(fNextToken, nullptr, 10); + // if this token ends in ')', then it is the last token + // else we advance + if (*(fNextToken + strlen(fNextToken) - 1) == ')') + fNextToken += strlen(fNextToken) - 1; + } + } else if (!PL_strcasecmp(fNextToken, "UNSEEN")) { + AdvanceToNextToken(); + if (fNextToken) { + fStatusUnseenMessages = strtoul(fNextToken, nullptr, 10); + // if this token ends in ')', then it is the last token + // else we advance + if (*(fNextToken + strlen(fNextToken) - 1) == ')') + fNextToken += strlen(fNextToken) - 1; + } + } else if (!PL_strcasecmp(fNextToken, "RECENT")) { + AdvanceToNextToken(); + if (fNextToken) { + fStatusRecentMessages = strtoul(fNextToken, nullptr, 10); + // if this token ends in ')', then it is the last token + // else we advance + if (*(fNextToken + strlen(fNextToken) - 1) == ')') + fNextToken += strlen(fNextToken) - 1; + } + } else if (*fNextToken == ')') + break; + else if (!fAtEndOfLine) + SetSyntaxError(true); + } + } else + SetSyntaxError(true); + break; + case 'C': + if (!PL_strcasecmp(fNextToken, "CAPABILITY")) + capability_data(); + else + SetSyntaxError(true); + break; + case 'V': + if (!PL_strcasecmp(fNextToken, "VERSION")) { + // figure out the version of the Netscape server here + PR_FREEIF(fNetscapeServerVersionString); + AdvanceToNextToken(); + if (!fNextToken) + SetSyntaxError(true); + else { + fNetscapeServerVersionString = CreateAstring(); + AdvanceToNextToken(); + if (fNetscapeServerVersionString) { + fServerIsNetscape3xServer = + (*fNetscapeServerVersionString == '3'); + } + } + skip_to_CRLF(); + } else + SetSyntaxError(true); + break; + case 'A': + if (!PL_strcasecmp(fNextToken, "ACL")) { + acl_data(); + } else if (!PL_strcasecmp(fNextToken, "ACCOUNT-URL")) { + fMailAccountUrl.Truncate(); + AdvanceToNextToken(); + if (!fNextToken) + SetSyntaxError(true); + else { + fMailAccountUrl.Adopt(CreateAstring()); + AdvanceToNextToken(); + } + } else + SetSyntaxError(true); + break; + case 'E': + if (!PL_strcasecmp(fNextToken, "ENABLED")) enable_data(); + break; + case 'X': + if (!PL_strcasecmp(fNextToken, "XSERVERINFO")) + xserverinfo_data(); + else if (!PL_strcasecmp(fNextToken, "XMAILBOXINFO")) + xmailboxinfo_data(); + else if (!PL_strcasecmp(fNextToken, "XAOL-OPTION")) + skip_to_CRLF(); + else if (!PL_strcasecmp(fNextToken, "XLIST")) + mailbox_data(); + else { + // check if custom command + nsAutoCString customCommand; + fServerConnection.GetCurrentUrl()->GetCommand(customCommand); + if (customCommand.Equals(fNextToken)) { + nsAutoCString customCommandResponse; + while (Connected() && !fAtEndOfLine) { + AdvanceToNextToken(); + customCommandResponse.Append(fNextToken); + customCommandResponse.Append(' '); + } + fServerConnection.GetCurrentUrl()->SetCustomCommandResult( + customCommandResponse); + } else + SetSyntaxError(true); + } + break; + case 'Q': + if (!PL_strcasecmp(fNextToken, "QUOTAROOT") || + !PL_strcasecmp(fNextToken, "QUOTA")) + quota_data(); + else + SetSyntaxError(true); + break; + case 'I': + id_data(); + break; + default: + if (IsNumericString(fNextToken)) + numeric_mailbox_data(); + else + SetSyntaxError(true); + break; + } + + if (ContinueParse()) PostProcessEndOfLine(); + } +} + +void nsImapServerResponseParser::PostProcessEndOfLine() { + // for now we only have to do one thing here + // a fetch response to a 'uid store' command might return the flags + // before it returns the uid of the message. So we need both before + // we report the new flag info to the front end + + // also check and be sure that there was a UID in the current response + if (fCurrentLineContainedFlagInfo && CurrentResponseUID()) { + fCurrentLineContainedFlagInfo = false; + nsCString customFlags; + fFlagState->GetCustomFlags(CurrentResponseUID(), + getter_Copies(customFlags)); + fServerConnection.NotifyMessageFlags(fSavedFlagInfo, customFlags, + CurrentResponseUID(), fHighestModSeq); + } +} + +/* + mailbox_data ::= "FLAGS" SPACE flag_list / + "LIST" SPACE mailbox_list / + "LSUB" SPACE mailbox_list / + "XLIST" SPACE mailbox_list / + "MAILBOX" SPACE text / + "SEARCH" [SPACE 1#nz_number] / + number SPACE "EXISTS" / number SPACE "RECENT" + +This production was changed to accommodate predictive parsing + + mailbox_data ::= "FLAGS" SPACE flag_list / + "LIST" SPACE mailbox_list / + "LSUB" SPACE mailbox_list / + "XLIST" SPACE mailbox_list / + "MAILBOX" SPACE text / + "SEARCH" [SPACE 1#nz_number] +*/ +void nsImapServerResponseParser::mailbox_data() { + if (!PL_strcasecmp(fNextToken, "FLAGS")) { + // this handles the case where we got the permanent flags response + // before the flags response, in which case, we want to ignore these flags. + if (fGotPermanentFlags) + skip_to_CRLF(); + else + parse_folder_flags(true); + } else if (!PL_strcasecmp(fNextToken, "LIST") || + !PL_strcasecmp(fNextToken, "XLIST")) { + AdvanceToNextToken(); + if (ContinueParse()) mailbox_list(false); + } else if (!PL_strcasecmp(fNextToken, "LSUB")) { + AdvanceToNextToken(); + if (ContinueParse()) mailbox_list(true); + } else if (!PL_strcasecmp(fNextToken, "MAILBOX")) + skip_to_CRLF(); + else if (!PL_strcasecmp(fNextToken, "SEARCH")) { + fSearchResults->AddSearchResultLine(fCurrentLine); + fServerConnection.NotifySearchHit(fCurrentLine); + skip_to_CRLF(); + } +} + +/* + mailbox_list ::= "(" #("\Marked" / "\Noinferiors" / + "\Noselect" / "\Unmarked" / flag_extension) ")" + SPACE (<"> QUOTED_CHAR <"> / nil) SPACE mailbox +*/ + +void nsImapServerResponseParser::mailbox_list(bool discoveredFromLsub) { + RefPtr<nsImapMailboxSpec> boxSpec = new nsImapMailboxSpec; + boxSpec->mFolderSelected = false; + boxSpec->mBoxFlags = kNoFlags; + boxSpec->mAllocatedPathName.Truncate(); + boxSpec->mHostName.Truncate(); + boxSpec->mConnection = &fServerConnection; + boxSpec->mFlagState = nullptr; + boxSpec->mDiscoveredFromLsub = discoveredFromLsub; + boxSpec->mOnlineVerified = true; + boxSpec->mBoxFlags &= ~kNameSpace; + + bool endOfFlags = false; + fNextToken++; // eat the first "(" + do { + if (!PL_strncasecmp(fNextToken, "\\Marked", 7)) + boxSpec->mBoxFlags |= kMarked; + else if (!PL_strncasecmp(fNextToken, "\\Unmarked", 9)) + boxSpec->mBoxFlags |= kUnmarked; + else if (!PL_strncasecmp(fNextToken, "\\Noinferiors", 12)) { + boxSpec->mBoxFlags |= kNoinferiors; + // RFC 5258 \Noinferiors implies \HasNoChildren + if (fCapabilityFlag & kHasListExtendedCapability) + boxSpec->mBoxFlags |= kHasNoChildren; + } else if (!PL_strncasecmp(fNextToken, "\\Noselect", 9)) + boxSpec->mBoxFlags |= kNoselect; + else if (!PL_strncasecmp(fNextToken, "\\Drafts", 7)) + boxSpec->mBoxFlags |= kImapDrafts; + else if (!PL_strncasecmp(fNextToken, "\\Trash", 6)) + boxSpec->mBoxFlags |= kImapXListTrash; + else if (!PL_strncasecmp(fNextToken, "\\Sent", 5)) + boxSpec->mBoxFlags |= kImapSent; + else if (!PL_strncasecmp(fNextToken, "\\Spam", 5) || + !PL_strncasecmp(fNextToken, "\\Junk", 5)) + boxSpec->mBoxFlags |= kImapSpam; + else if (!PL_strncasecmp(fNextToken, "\\Archive", 8)) + boxSpec->mBoxFlags |= kImapArchive; + else if (!PL_strncasecmp(fNextToken, "\\All", 4) || + !PL_strncasecmp(fNextToken, "\\AllMail", 8)) + boxSpec->mBoxFlags |= kImapAllMail; + else if (!PL_strncasecmp(fNextToken, "\\Inbox", 6)) + boxSpec->mBoxFlags |= kImapInbox; + else if (!PL_strncasecmp(fNextToken, "\\NonExistent", 11)) { + boxSpec->mBoxFlags |= kNonExistent; + // RFC 5258 \NonExistent implies \Noselect + boxSpec->mBoxFlags |= kNoselect; + } else if (!PL_strncasecmp(fNextToken, "\\Subscribed", 10)) + boxSpec->mBoxFlags |= kSubscribed; + else if (!PL_strncasecmp(fNextToken, "\\Remote", 6)) + boxSpec->mBoxFlags |= kRemote; + else if (!PL_strncasecmp(fNextToken, "\\HasChildren", 11)) + boxSpec->mBoxFlags |= kHasChildren; + else if (!PL_strncasecmp(fNextToken, "\\HasNoChildren", 13)) + boxSpec->mBoxFlags |= kHasNoChildren; + // we ignore flag other extensions + + endOfFlags = *(fNextToken + strlen(fNextToken) - 1) == ')'; + AdvanceToNextToken(); + } while (!endOfFlags && ContinueParse()); + + if (ContinueParse()) { + if (*fNextToken == '"') { + fNextToken++; + if (*fNextToken == '\\') // handle escaped char + boxSpec->mHierarchySeparator = *(fNextToken + 1); + else + boxSpec->mHierarchySeparator = *fNextToken; + } else // likely NIL. Discovered late in 4.02 that we do not handle + // literals here (e.g. {10} <10 chars>), although this is almost + // impossibly unlikely + boxSpec->mHierarchySeparator = kOnlineHierarchySeparatorNil; + AdvanceToNextToken(); + if (ContinueParse()) mailbox(boxSpec); + } +} + +/* mailbox ::= "INBOX" / astring + */ +void nsImapServerResponseParser::mailbox(nsImapMailboxSpec* boxSpec) { + char* boxname = nullptr; + const char* serverKey = fServerConnection.GetImapServerKey(); + bool xlistInbox = boxSpec->mBoxFlags & kImapInbox; + + if (!PL_strcasecmp(fNextToken, "INBOX") || xlistInbox) { + boxname = PL_strdup("INBOX"); + if (xlistInbox) PR_Free(CreateAstring()); + AdvanceToNextToken(); + } else { + boxname = CreateAstring(); + AdvanceToNextToken(); + } + + if (boxname && fHostSessionList) { + fHostSessionList->SetNamespaceHierarchyDelimiterFromMailboxForHost( + serverKey, boxname, boxSpec->mHierarchySeparator); + + nsImapNamespace* ns = nullptr; + fHostSessionList->GetNamespaceForMailboxForHost(serverKey, boxname, ns); + if (ns) { + switch (ns->GetType()) { + case kPersonalNamespace: + boxSpec->mBoxFlags |= kPersonalMailbox; + break; + case kPublicNamespace: + boxSpec->mBoxFlags |= kPublicMailbox; + break; + case kOtherUsersNamespace: + boxSpec->mBoxFlags |= kOtherUsersMailbox; + break; + default: // (kUnknownNamespace) + break; + } + boxSpec->mNamespaceForFolder = ns; + } + } + + if (!boxname) { + if (!fServerConnection.DeathSignalReceived()) HandleMemoryFailure(); + } else if (boxSpec->mConnection && boxSpec->mConnection->GetCurrentUrl()) { + boxSpec->mConnection->GetCurrentUrl()->AllocateCanonicalPath( + boxname, boxSpec->mHierarchySeparator, + getter_Copies(boxSpec->mAllocatedPathName)); + nsIURI* aURL = nullptr; + boxSpec->mConnection->GetCurrentUrl()->QueryInterface(NS_GET_IID(nsIURI), + (void**)&aURL); + if (aURL) aURL->GetHost(boxSpec->mHostName); + + NS_IF_RELEASE(aURL); + // storage for the boxSpec is now owned by server connection + fServerConnection.DiscoverMailboxSpec(boxSpec); + + // if this was cancelled by the user,then we sure don't want to + // send more mailboxes their way + if (NS_FAILED(fServerConnection.GetConnectionStatus())) SetConnected(false); + } + + if (boxname) PL_strfree(boxname); +} + +/* + message_data ::= nz_number SPACE ("EXPUNGE" / + ("FETCH" SPACE msg_fetch) / msg_obsolete) + +was changed to + +numeric_mailbox_data ::= number SPACE "EXISTS" / number SPACE "RECENT" + / nz_number SPACE ("EXPUNGE" / + ("FETCH" SPACE msg_fetch) / msg_obsolete) + +*/ +void nsImapServerResponseParser::numeric_mailbox_data() { + int32_t tokenNumber = atoi(fNextToken); + AdvanceToNextToken(); + + if (ContinueParse()) { + if (!PL_strcasecmp(fNextToken, "FETCH")) { + fFetchResponseIndex = tokenNumber; + AdvanceToNextToken(); + if (ContinueParse()) msg_fetch(); + } else if (!PL_strcasecmp(fNextToken, "EXISTS")) { + fNumberOfExistingMessages = tokenNumber; + AdvanceToNextToken(); + } else if (!PL_strcasecmp(fNextToken, "RECENT")) { + fNumberOfRecentMessages = tokenNumber; + AdvanceToNextToken(); + } else if (!PL_strcasecmp(fNextToken, "EXPUNGE")) { + if (!fServerConnection.GetIgnoreExpunges()) + fFlagState->ExpungeByIndex((uint32_t)tokenNumber); + skip_to_CRLF(); + } else + msg_obsolete(); + } +} + +/* +msg_fetch ::= "(" 1#("BODY" SPACE body / +"BODYSTRUCTURE" SPACE body / +"BODY[" section "]" SPACE nstring / +"ENVELOPE" SPACE envelope / +"FLAGS" SPACE "(" #(flag / "\Recent") ")" / +"INTERNALDATE" SPACE date_time / +"MODSEQ" SPACE "(" nz_number ")" / +"RFC822" [".HEADER" / ".TEXT"] SPACE nstring / +"RFC822.SIZE" SPACE number / +"UID" SPACE uniqueid) ")" + +*/ + +void nsImapServerResponseParser::msg_fetch() { + bool bNeedEndMessageDownload = false; + + // we have not seen a uid response or flags for this fetch, yet + fCurrentResponseUID = 0; + fCurrentLineContainedFlagInfo = false; + fSizeOfMostRecentMessage = 0; + // show any incremental progress, for instance, for header downloading + fServerConnection.ShowProgress(); + + fNextToken++; // eat the '(' character + + // some of these productions are ignored for now + while (ContinueParse() && (*fNextToken != ')')) { + if (!PL_strcasecmp(fNextToken, "FLAGS")) { + if (fCurrentResponseUID == 0) + fFlagState->GetUidOfMessage(fFetchResponseIndex - 1, + &fCurrentResponseUID); + + AdvanceToNextToken(); + if (ContinueParse()) flags(); + + if (ContinueParse()) { // eat the closing ')' + fNextToken++; + // there may be another ')' to close out + // msg_fetch. If there is then don't advance + if (*fNextToken != ')') AdvanceToNextToken(); + } + } else if (!PL_strcasecmp(fNextToken, "UID")) { + AdvanceToNextToken(); + if (ContinueParse()) { + fCurrentResponseUID = strtoul(fNextToken, nullptr, 10); + if (fCurrentResponseUID > fHighestRecordedUID) + fHighestRecordedUID = fCurrentResponseUID; + // size came before UID + if (fSizeOfMostRecentMessage) + fReceivedHeaderOrSizeForUID = CurrentResponseUID(); + // if this token ends in ')', then it is the last token + // else we advance + char lastTokenChar = *(fNextToken + strlen(fNextToken) - 1); + if (lastTokenChar == ')') + fNextToken += strlen(fNextToken) - 1; + else if (lastTokenChar < '0' || lastTokenChar > '9') { + // GIANT HACK + // this is a corrupt uid - see if it's pre 5.08 Zimbra omitting + // a space between the UID and MODSEQ + if (strlen(fNextToken) > 6 && + !strcmp("MODSEQ", fNextToken + strlen(fNextToken) - 6)) + fNextToken += strlen(fNextToken) - 6; + } else + AdvanceToNextToken(); + } + } else if (!PL_strcasecmp(fNextToken, "MODSEQ")) { + AdvanceToNextToken(); + if (ContinueParse()) { + fNextToken++; // eat '(' + uint64_t modSeq = ParseUint64Str(fNextToken); + if (modSeq > fHighestModSeq) fHighestModSeq = modSeq; + + if (PL_strcasestr(fNextToken, ")")) { + // eat token chars until we get the ')' + fNextToken = strchr(fNextToken, ')'); + if (fNextToken) { + fNextToken++; + if (*fNextToken != ')') AdvanceToNextToken(); + } else + SetSyntaxError(true); + } else { + SetSyntaxError(true); + } + } + } else if (!PL_strcasecmp(fNextToken, "RFC822") || + !PL_strcasecmp(fNextToken, "RFC822.HEADER") || + !PL_strncasecmp(fNextToken, "BODY[HEADER", 11) || + !PL_strncasecmp(fNextToken, "BODY[]", 6) || + !PL_strcasecmp(fNextToken, "RFC822.TEXT") || + (!PL_strncasecmp(fNextToken, "BODY[", 5) && + PL_strstr(fNextToken, "HEADER"))) { + if (fCurrentResponseUID == 0) + fFlagState->GetUidOfMessage(fFetchResponseIndex - 1, + &fCurrentResponseUID); + + if (!PL_strcasecmp(fNextToken, "RFC822.HEADER") || + !PL_strcasecmp(fNextToken, "BODY[HEADER]")) { + // all of this message's headers + AdvanceToNextToken(); + fDownloadingHeaders = true; + BeginMessageDownload(MESSAGE_RFC822); // initialize header parser + bNeedEndMessageDownload = false; + if (ContinueParse()) msg_fetch_headers(nullptr); + } else if (!PL_strncasecmp(fNextToken, "BODY[HEADER.FIELDS", 19)) { + fDownloadingHeaders = true; + BeginMessageDownload(MESSAGE_RFC822); // initialize header parser + // specific message header fields + while (ContinueParse() && fNextToken[strlen(fNextToken) - 1] != ']') + AdvanceToNextToken(); + if (ContinueParse()) { + bNeedEndMessageDownload = false; + AdvanceToNextToken(); + if (ContinueParse()) msg_fetch_headers(nullptr); + } + } else { + char* whereHeader = PL_strstr(fNextToken, "HEADER"); + if (whereHeader) { + const char* startPartNum = fNextToken + 5; + if (whereHeader > startPartNum) { + int32_t partLength = + whereHeader - startPartNum - 1; //-1 for the dot! + char* partNum = (char*)PR_CALLOC((partLength + 1) * sizeof(char)); + if (partNum) { + PL_strncpy(partNum, startPartNum, partLength); + if (ContinueParse()) { + if (PL_strstr(fNextToken, "FIELDS")) { + while (ContinueParse() && + fNextToken[strlen(fNextToken) - 1] != ']') + AdvanceToNextToken(); + } + if (ContinueParse()) { + AdvanceToNextToken(); + if (ContinueParse()) msg_fetch_headers(partNum); + } + } + PR_Free(partNum); + } + } else + SetSyntaxError(true); + } else { + fDownloadingHeaders = false; + + bool chunk = false; + int32_t origin = 0; + if (!PL_strncasecmp(fNextToken, "BODY[]<", 7)) { + char* tokenCopy = 0; + tokenCopy = PL_strdup(fNextToken); + if (tokenCopy) { + char* originString = + tokenCopy + 7; // where the byte number starts + char* closeBracket = PL_strchr(tokenCopy, '>'); + if (closeBracket && originString && *originString) { + *closeBracket = 0; + origin = atoi(originString); + chunk = true; + } + PR_Free(tokenCopy); + } + } + + AdvanceToNextToken(); + if (ContinueParse()) { + msg_fetch_content(chunk, origin, MESSAGE_RFC822); + } + } + } + } else if (!PL_strcasecmp(fNextToken, "RFC822.SIZE") || + !PL_strcasecmp(fNextToken, "XAOL.SIZE")) { + AdvanceToNextToken(); + if (ContinueParse()) { + bool sendEndMsgDownload = + (GetDownloadingHeaders() && + fReceivedHeaderOrSizeForUID == CurrentResponseUID()); + fSizeOfMostRecentMessage = strtoul(fNextToken, nullptr, 10); + fReceivedHeaderOrSizeForUID = CurrentResponseUID(); + if (sendEndMsgDownload) { + fServerConnection.NormalMessageEndDownload(); + fReceivedHeaderOrSizeForUID = nsMsgKey_None; + } + + if (fSizeOfMostRecentMessage == 0 && CurrentResponseUID()) { + // on no, bogus Netscape 2.0 mail server bug + char uidString[100]; + sprintf(uidString, "%ld", (long)CurrentResponseUID()); + + if (!fZeroLengthMessageUidString.IsEmpty()) + fZeroLengthMessageUidString += ","; + + fZeroLengthMessageUidString += uidString; + } + + // if this token ends in ')', then it is the last token + // else we advance + if (*(fNextToken + strlen(fNextToken) - 1) == ')') + fNextToken += strlen(fNextToken) - 1; + else + AdvanceToNextToken(); + } + } else if (!PL_strcasecmp(fNextToken, "XSENDER")) { + PR_FREEIF(fXSenderInfo); + AdvanceToNextToken(); + if (!fNextToken) + SetSyntaxError(true); + else { + fXSenderInfo = CreateAstring(); + AdvanceToNextToken(); + } + } else if (!PL_strcasecmp(fNextToken, "X-GM-MSGID")) { + AdvanceToNextToken(); + if (!fNextToken) + SetSyntaxError(true); + else { + fMsgID = CreateAtom(); + AdvanceToNextToken(); + nsCString msgIDValue; + msgIDValue.Assign(fMsgID); + if (fCurrentResponseUID == 0) + fFlagState->GetUidOfMessage(fFetchResponseIndex - 1, + &fCurrentResponseUID); + fFlagState->SetCustomAttribute(fCurrentResponseUID, "X-GM-MSGID"_ns, + msgIDValue); + PR_FREEIF(fMsgID); + } + } else if (!PL_strcasecmp(fNextToken, "X-GM-THRID")) { + AdvanceToNextToken(); + if (!fNextToken) + SetSyntaxError(true); + else { + fThreadID = CreateAtom(); + AdvanceToNextToken(); + nsCString threadIDValue; + threadIDValue.Assign(fThreadID); + if (fCurrentResponseUID == 0) + fFlagState->GetUidOfMessage(fFetchResponseIndex - 1, + &fCurrentResponseUID); + fFlagState->SetCustomAttribute(fCurrentResponseUID, "X-GM-THRID"_ns, + threadIDValue); + PR_FREEIF(fThreadID); + } + } else if (!PL_strcasecmp(fNextToken, "X-GM-LABELS")) { + AdvanceToNextToken(); + if (!fNextToken) + SetSyntaxError(true); + else { + fLabels = CreateParenGroup(); + nsCString labelsValue; + labelsValue.Assign(fLabels); + labelsValue.Cut(0, 1); + labelsValue.Cut(labelsValue.Length() - 1, 1); + if (fCurrentResponseUID == 0) + fFlagState->GetUidOfMessage(fFetchResponseIndex - 1, + &fCurrentResponseUID); + fFlagState->SetCustomAttribute(fCurrentResponseUID, "X-GM-LABELS"_ns, + labelsValue); + PR_FREEIF(fLabels); + } + } + + // I only fetch RFC822 so I should never see these BODY responses + else if (!PL_strcasecmp(fNextToken, "BODY")) + skip_to_CRLF(); // I never ask for this + else if (!PL_strncasecmp(fNextToken, "BODY[TEXT", 9)) { + // This parses the "preview" response (first 2048 bytes of body text). + mime_part_data(); // Note: TEXT is not an actual mime part. + } else if (!PL_strcasecmp(fNextToken, "ENVELOPE")) { + fDownloadingHeaders = true; + bNeedEndMessageDownload = true; + BeginMessageDownload(MESSAGE_RFC822); + envelope_data(); + } else if (!PL_strcasecmp(fNextToken, "INTERNALDATE")) { + fDownloadingHeaders = + true; // we only request internal date while downloading headers + if (!bNeedEndMessageDownload) BeginMessageDownload(MESSAGE_RFC822); + bNeedEndMessageDownload = true; + internal_date(); + } else if (!PL_strcasecmp(fNextToken, "XAOL-ENVELOPE")) { + fDownloadingHeaders = true; + if (!bNeedEndMessageDownload) BeginMessageDownload(MESSAGE_RFC822); + bNeedEndMessageDownload = true; + xaolenvelope_data(); + } else { + nsImapAction imapAction; + if (!fServerConnection.GetCurrentUrl()) return; + fServerConnection.GetCurrentUrl()->GetImapAction(&imapAction); + nsAutoCString userDefinedFetchAttribute; + fServerConnection.GetCurrentUrl()->GetCustomAttributeToFetch( + userDefinedFetchAttribute); + if ((imapAction == nsIImapUrl::nsImapUserDefinedFetchAttribute && + !strcmp(userDefinedFetchAttribute.get(), fNextToken)) || + imapAction == nsIImapUrl::nsImapUserDefinedMsgCommand) { + AdvanceToNextToken(); + char* fetchResult; + if (fNextToken[0] == '(') + // look through the tokens until we find the closing ')' + // we can have a result like the following: + // ((A B) (C D) (E F)) + fetchResult = CreateParenGroup(); + else { + fetchResult = CreateAstring(); + AdvanceToNextToken(); + } + if (imapAction == nsIImapUrl::nsImapUserDefinedFetchAttribute) + fServerConnection.GetCurrentUrl()->SetCustomAttributeResult( + nsDependentCString(fetchResult)); + if (imapAction == nsIImapUrl::nsImapUserDefinedMsgCommand) + fServerConnection.GetCurrentUrl()->SetCustomCommandResult( + nsDependentCString(fetchResult)); + PR_Free(fetchResult); + } else + SetSyntaxError(true); + } + } + + if (ContinueParse()) { + if (CurrentResponseUID() && CurrentResponseUID() != nsMsgKey_None && + fCurrentLineContainedFlagInfo && fFlagState) { + fFlagState->AddUidFlagPair(CurrentResponseUID(), fSavedFlagInfo, + fFetchResponseIndex - 1); + for (uint32_t i = 0; i < fCustomFlags.Length(); i++) + fFlagState->AddUidCustomFlagPair(CurrentResponseUID(), + fCustomFlags[i].get()); + fCustomFlags.Clear(); + } + + if (fFetchingAllFlags) + fCurrentLineContainedFlagInfo = + false; // do not fire if in PostProcessEndOfLine + + AdvanceToNextToken(); // eat the ')' ending token + // should be at end of line + if (bNeedEndMessageDownload) { + if (ContinueParse()) { + // complete the message download + fServerConnection.NormalMessageEndDownload(); + } else + fServerConnection.AbortMessageDownLoad(); + } + } +} + +typedef enum _envelopeItemType { + envelopeString, + envelopeAddress +} envelopeItemType; + +typedef struct { + const char* name; + envelopeItemType type; +} envelopeItem; + +// RFC3501: envelope = "(" env-date SP env-subject SP env-from SP +// env-sender SP env-reply-to SP env-to SP env-cc SP +// env-bcc SP env-in-reply-to SP env-message-id ")" +// env-date = nstring +// env-subject = nstring +// env-from = "(" 1*address ")" / nil +// env-sender = "(" 1*address ")" / nil +// env-reply-to= "(" 1*address ")" / nil +// env-to = "(" 1*address ")" / nil +// env-cc = "(" 1*address ")" / nil +// env-bcc = "(" 1*address ")" / nil +// env-in-reply-to = nstring +// env-message-id = nstring + +static const envelopeItem EnvelopeTable[] = { + {"Date", envelopeString}, {"Subject", envelopeString}, + {"From", envelopeAddress}, {"Sender", envelopeAddress}, + {"Reply-to", envelopeAddress}, {"To", envelopeAddress}, + {"Cc", envelopeAddress}, {"Bcc", envelopeAddress}, + {"In-reply-to", envelopeString}, {"Message-id", envelopeString}}; + +void nsImapServerResponseParser::envelope_data() { + AdvanceToNextToken(); + fNextToken++; // eat '(' + for (int tableIndex = 0; + tableIndex < (int)(sizeof(EnvelopeTable) / sizeof(EnvelopeTable[0])); + tableIndex++) { + if (!ContinueParse()) break; + if (*fNextToken == ')') { + SetSyntaxError(true); // envelope too short + break; + } + + nsAutoCString headerLine(EnvelopeTable[tableIndex].name); + headerLine += ": "; + bool headerNonNil = true; + if (EnvelopeTable[tableIndex].type == envelopeString) { + nsAutoCString strValue; + strValue.Adopt(CreateNilString()); + if (!strValue.IsEmpty()) + headerLine.Append(strValue); + else + headerNonNil = false; + } else { + nsAutoCString address; + parse_address(address); + headerLine += address; + if (address.IsEmpty()) headerNonNil = false; + } + if (headerNonNil) + fServerConnection.HandleMessageDownLoadLine(headerLine.get(), false); + + if (ContinueParse()) AdvanceToNextToken(); + } + // Now we should be at the end of the envelope and have *fToken == ')'. + // Skip this last parenthesis. + AdvanceToNextToken(); +} + +void nsImapServerResponseParser::xaolenvelope_data() { + // eat the opening '(' + fNextToken++; + + if (ContinueParse() && (*fNextToken != ')')) { + AdvanceToNextToken(); + fNextToken++; // eat '(' + nsAutoCString subject; + subject.Adopt(CreateNilString()); + nsAutoCString subjectLine("Subject: "); + subjectLine += subject; + fServerConnection.HandleMessageDownLoadLine(subjectLine.get(), false); + fNextToken++; // eat the next '(' + if (ContinueParse()) { + AdvanceToNextToken(); + if (ContinueParse()) { + nsAutoCString fromLine; + if (!strcmp(GetSelectedMailboxName(), "Sent Items")) { + // xaol envelope switches the From with the To, so we switch them back + // and create a fake from line From: user@aol.com + fromLine.AppendLiteral("To: "); + nsAutoCString fakeFromLine("From: "_ns); + fakeFromLine.Append(fServerConnection.GetImapUserName()); + fakeFromLine.AppendLiteral("@aol.com"); + fServerConnection.HandleMessageDownLoadLine(fakeFromLine.get(), + false); + } else { + fromLine.AppendLiteral("From: "); + } + parse_address(fromLine); + fServerConnection.HandleMessageDownLoadLine(fromLine.get(), false); + if (ContinueParse()) { + AdvanceToNextToken(); // ge attachment size + int32_t attachmentSize = atoi(fNextToken); + if (attachmentSize != 0) { + nsAutoCString attachmentLine("X-attachment-size: "); + attachmentLine.AppendInt(attachmentSize); + fServerConnection.HandleMessageDownLoadLine(attachmentLine.get(), + false); + } + } + if (ContinueParse()) { + AdvanceToNextToken(); // skip image size + int32_t imageSize = atoi(fNextToken); + if (imageSize != 0) { + nsAutoCString imageLine("X-image-size: "); + imageLine.AppendInt(imageSize); + fServerConnection.HandleMessageDownLoadLine(imageLine.get(), false); + } + } + if (ContinueParse()) AdvanceToNextToken(); // skip ) + } + } + } +} + +void nsImapServerResponseParser::parse_address(nsAutoCString& addressLine) { + // NOTE: Not sure this function correctly handling group address syntax. + // See Bug 1609846. + if (!strcmp(fNextToken, "NIL")) return; + bool firstAddress = true; + // should really look at chars here + NS_ASSERTION(*fNextToken == '(', "address should start with '('"); + fNextToken++; // eat the next '(' + while (ContinueParse() && *fNextToken == '(') { + NS_ASSERTION(*fNextToken == '(', "address should start with '('"); + fNextToken++; // eat the next '(' + + if (!firstAddress) addressLine += ", "; + + firstAddress = false; + char* personalName = CreateNilString(); + AdvanceToNextToken(); + char* atDomainList = CreateNilString(); + if (ContinueParse()) { + AdvanceToNextToken(); + char* mailboxName = CreateNilString(); + if (ContinueParse()) { + AdvanceToNextToken(); + char* hostName = CreateNilString(); + AdvanceToNextToken(); + if (mailboxName) { + addressLine += mailboxName; + } + if (hostName) { + addressLine += '@'; + addressLine += hostName; + PR_Free(hostName); + } + if (personalName) { + addressLine += " ("; + addressLine += personalName; + addressLine += ')'; + } + } + PR_Free(mailboxName); + } + PR_Free(personalName); + PR_Free(atDomainList); + + if (*fNextToken == ')') fNextToken++; + // if the next token isn't a ')' for the address term, + // then we must have another address pair left....so get the next + // token and continue parsing in this loop... + if (*fNextToken == '\0') AdvanceToNextToken(); + } + if (*fNextToken == ')') fNextToken++; + // AdvanceToNextToken(); // skip "))" +} + +void nsImapServerResponseParser::internal_date() { + AdvanceToNextToken(); + if (ContinueParse()) { + nsAutoCString dateLine("Date: "); + char* strValue = CreateNilString(); + if (strValue) { + dateLine += strValue; + free(strValue); + } + fServerConnection.HandleMessageDownLoadLine(dateLine.get(), false); + } + // advance the parser. + AdvanceToNextToken(); +} + +void nsImapServerResponseParser::flags() { + imapMessageFlagsType messageFlags = kNoImapMsgFlag; + fCustomFlags.Clear(); + + // clear the custom flags for this message + // otherwise the old custom flags will stay around + // see bug #191042 + if (fFlagState && CurrentResponseUID() != nsMsgKey_None) + fFlagState->ClearCustomFlags(CurrentResponseUID()); + + // eat the opening '(' + fNextToken++; + while (ContinueParse() && (*fNextToken != ')')) { + bool knownFlag = false; + if (*fNextToken == '\\') { + switch (NS_ToUpper(fNextToken[1])) { + case 'S': + if (!PL_strncasecmp(fNextToken, "\\Seen", 5)) { + messageFlags |= kImapMsgSeenFlag; + knownFlag = true; + } + break; + case 'A': + if (!PL_strncasecmp(fNextToken, "\\Answered", 9)) { + messageFlags |= kImapMsgAnsweredFlag; + knownFlag = true; + } + break; + case 'F': + if (!PL_strncasecmp(fNextToken, "\\Flagged", 8)) { + messageFlags |= kImapMsgFlaggedFlag; + knownFlag = true; + } + break; + case 'D': + if (!PL_strncasecmp(fNextToken, "\\Deleted", 8)) { + messageFlags |= kImapMsgDeletedFlag; + knownFlag = true; + } else if (!PL_strncasecmp(fNextToken, "\\Draft", 6)) { + messageFlags |= kImapMsgDraftFlag; + knownFlag = true; + } + break; + case 'R': + if (!PL_strncasecmp(fNextToken, "\\Recent", 7)) { + messageFlags |= kImapMsgRecentFlag; + knownFlag = true; + } + break; + default: + break; + } + } else if (*fNextToken == '$') { + switch (NS_ToUpper(fNextToken[1])) { + case 'M': + if ((fSupportsUserDefinedFlags & + (kImapMsgSupportUserFlag | kImapMsgSupportMDNSentFlag)) && + !PL_strncasecmp(fNextToken, "$MDNSent", 8)) { + messageFlags |= kImapMsgMDNSentFlag; + knownFlag = true; + } + break; + case 'F': + if ((fSupportsUserDefinedFlags & + (kImapMsgSupportUserFlag | kImapMsgSupportForwardedFlag)) && + !PL_strncasecmp(fNextToken, "$Forwarded", 10)) { + messageFlags |= kImapMsgForwardedFlag; + knownFlag = true; + } + break; + default: + break; + } + } + if (!knownFlag && fFlagState) { + nsAutoCString flag(fNextToken); + int32_t parenIndex = flag.FindChar(')'); + if (parenIndex > 0) flag.SetLength(parenIndex); + messageFlags |= kImapMsgCustomKeywordFlag; + if (CurrentResponseUID() != nsMsgKey_None && CurrentResponseUID() != 0) + fFlagState->AddUidCustomFlagPair(CurrentResponseUID(), flag.get()); + else + fCustomFlags.AppendElement(flag); + } + if (PL_strcasestr(fNextToken, ")")) { + // eat token chars until we get the ')' + while (*fNextToken != ')') fNextToken++; + } else + AdvanceToNextToken(); + } + + if (ContinueParse()) + while (*fNextToken != ')') fNextToken++; + + fCurrentLineContainedFlagInfo = true; // handled in PostProcessEndOfLine + fSavedFlagInfo = messageFlags; +} + +// RFC3501: resp-cond-state = ("OK" / "NO" / "BAD") SP resp-text +// ; Status condition +void nsImapServerResponseParser::resp_cond_state(bool isTagged) { + // According to RFC3501, Sec. 7.1, the untagged NO response "indicates a + // warning; the command can still complete successfully." + // However, the untagged BAD response "indicates a protocol-level error for + // which the associated command can not be determined; it can also indicate an + // internal server failure." + // Thus, we flag an error for a tagged NO response and for any BAD response. + if ((isTagged && !PL_strcasecmp(fNextToken, "NO")) || + !PL_strcasecmp(fNextToken, "BAD")) + fCurrentCommandFailed = true; + + AdvanceToNextToken(); + if (ContinueParse()) resp_text(); +} + +/* +resp_text ::= ["[" resp_text_code "]" SPACE] (text_mime2 / text) + + was changed to in order to enable a one symbol look ahead predictive + parser. + + resp_text ::= ["[" resp_text_code SPACE] (text_mime2 / text) +*/ +void nsImapServerResponseParser::resp_text() { + if (ContinueParse() && (*fNextToken == '[')) resp_text_code(); + + if (ContinueParse()) { + if (!PL_strcmp(fNextToken, "=?")) + text_mime2(); + else + text(); + } +} +/* + text_mime2 ::= "=?" <charset> "?" <encoding> "?" + <encoded-text> "?=" + ;; Syntax defined in [MIME-2] +*/ +void nsImapServerResponseParser::text_mime2() { skip_to_CRLF(); } + +/* + text ::= 1*TEXT_CHAR + +*/ +void nsImapServerResponseParser::text() { skip_to_CRLF(); } + +void nsImapServerResponseParser::parse_folder_flags(bool calledForFlags) { + uint16_t junkNotJunkFlags = 0; + + do { + AdvanceToNextToken(); + if (*fNextToken == '(') fNextToken++; + if (!PL_strncasecmp(fNextToken, "\\Seen", 5)) + fSettablePermanentFlags |= kImapMsgSeenFlag; + else if (!PL_strncasecmp(fNextToken, "\\Answered", 9)) + fSettablePermanentFlags |= kImapMsgAnsweredFlag; + else if (!PL_strncasecmp(fNextToken, "\\Flagged", 8)) + fSettablePermanentFlags |= kImapMsgFlaggedFlag; + else if (!PL_strncasecmp(fNextToken, "\\Deleted", 8)) + fSettablePermanentFlags |= kImapMsgDeletedFlag; + else if (!PL_strncasecmp(fNextToken, "\\Draft", 6)) + fSettablePermanentFlags |= kImapMsgDraftFlag; + else if (!PL_strncasecmp(fNextToken, "\\*", 2)) { + // User defined and special keywords (tags) can be defined and set for + // mailbox. Should only occur in PERMANENTFLAGS response. + fSupportsUserDefinedFlags |= kImapMsgSupportUserFlag; + fSupportsUserDefinedFlags |= kImapMsgSupportForwardedFlag; + fSupportsUserDefinedFlags |= kImapMsgSupportMDNSentFlag; + } + // Treat special and built-in $LabelX's as user defined and include + // $Junk/$NotJunk too. + else if (!PL_strncasecmp(fNextToken, "$MDNSent", 8)) + fSupportsUserDefinedFlags |= kImapMsgSupportMDNSentFlag; + else if (!PL_strncasecmp(fNextToken, "$Forwarded", 10)) + fSupportsUserDefinedFlags |= kImapMsgSupportForwardedFlag; + else if (!PL_strncasecmp(fNextToken, "$Junk", 5)) + junkNotJunkFlags |= 1; + else if (!PL_strncasecmp(fNextToken, "$NotJunk", 8)) + junkNotJunkFlags |= 2; + } while (!fAtEndOfLine && ContinueParse()); + + if (fFlagState) fFlagState->OrSupportedUserFlags(fSupportsUserDefinedFlags); + + if (calledForFlags) { + // Set true if both "$Junk" and "$NotJunk" appear in FLAGS. + fStdJunkNotJunkUseOk = (junkNotJunkFlags == 3); + } +} +/* + resp_text_code ::= ("ALERT" / "PARSE" / + "PERMANENTFLAGS" SPACE "(" #(flag / "\*") ")" / + "READ-ONLY" / "READ-WRITE" / "TRYCREATE" / + "UIDVALIDITY" SPACE nz_number / + "UNSEEN" SPACE nz_number / + "HIGHESTMODSEQ" SPACE nz_number / + "NOMODSEQ" / + atom [SPACE 1*<any TEXT_CHAR except "]">] ) + "]" + + +*/ +void nsImapServerResponseParser::resp_text_code() { + // this is a special case way of advancing the token + // strtok won't break up "[ALERT]" into separate tokens + if (strlen(fNextToken) > 1) + fNextToken++; + else + AdvanceToNextToken(); + + if (ContinueParse()) { + if (!PL_strcasecmp(fNextToken, "ALERT]")) { + char* alertMsg = fCurrentTokenPlaceHolder; // advance past ALERT] + if (alertMsg && *alertMsg && + (!fLastAlert || PL_strcmp(fNextToken, fLastAlert))) { + fServerConnection.AlertUserEvent(alertMsg); + PR_Free(fLastAlert); + fLastAlert = PL_strdup(alertMsg); + } + AdvanceToNextToken(); + } else if (!PL_strcasecmp(fNextToken, "PARSE]")) { + // do nothing for now + AdvanceToNextToken(); + } else if (!PL_strcasecmp(fNextToken, "NETSCAPE]")) { + skip_to_CRLF(); + } else if (!PL_strcasecmp(fNextToken, "PERMANENTFLAGS")) { + uint32_t saveSettableFlags = fSettablePermanentFlags; + fSupportsUserDefinedFlags = 0; // assume no unless told + fSettablePermanentFlags = 0; // assume none, unless told otherwise. + parse_folder_flags(false); + // if the server tells us there are no permanent flags, we're + // just going to pretend that the FLAGS response flags, if any, are + // permanent in case the server is broken. This will allow us + // to store delete and seen flag changes - if they're not permanent, + // they're not permanent, but at least we'll try to set them. + if (!fSettablePermanentFlags) fSettablePermanentFlags = saveSettableFlags; + fGotPermanentFlags = true; + } else if (!PL_strcasecmp(fNextToken, "READ-ONLY]")) { + fCurrentFolderReadOnly = true; + AdvanceToNextToken(); + } else if (!PL_strcasecmp(fNextToken, "READ-WRITE]")) { + fCurrentFolderReadOnly = false; + AdvanceToNextToken(); + } else if (!PL_strcasecmp(fNextToken, "TRYCREATE]")) { + // do nothing for now + AdvanceToNextToken(); + } else if (!PL_strcasecmp(fNextToken, "UIDVALIDITY")) { + AdvanceToNextToken(); + if (ContinueParse()) { + fFolderUIDValidity = strtoul(fNextToken, nullptr, 10); + fHighestRecordedUID = 0; + AdvanceToNextToken(); + } + } else if (!PL_strcasecmp(fNextToken, "UNSEEN")) { + AdvanceToNextToken(); + if (ContinueParse()) { + // Note: As a response code, "UNSEEN" is NOT the number of + // unseen/unread messages. It is the lowest sequence number of the first + // unseen/unread message in the mailbox. + fSeqNumOfFirstUnseenMsg = strtoul(fNextToken, nullptr, 10); + AdvanceToNextToken(); + } + } else if (!PL_strcasecmp(fNextToken, "UIDNEXT")) { + AdvanceToNextToken(); + if (ContinueParse()) { + fNextUID = strtoul(fNextToken, nullptr, 10); + AdvanceToNextToken(); + } + } else if (!PL_strcasecmp(fNextToken, "APPENDUID")) { + AdvanceToNextToken(); + if (ContinueParse()) { + // ** jt -- the returned uidvalidity is the destination folder + // uidvalidity; don't use it for current folder + // fFolderUIDValidity = atoi(fNextToken); + // fHighestRecordedUID = 0; ??? this should be wrong + AdvanceToNextToken(); + if (ContinueParse()) { + fCurrentResponseUID = strtoul(fNextToken, nullptr, 10); + AdvanceToNextToken(); + } + } + } else if (!PL_strcasecmp(fNextToken, "COPYUID")) { + AdvanceToNextToken(); + if (ContinueParse()) { + // ** jt -- destination folder uidvalidity + // fFolderUIDValidity = atoi(fNextToken); + // original message set; ignore it + AdvanceToNextToken(); + if (ContinueParse()) { + // the resulting message set; should be in the form of + // either uid or uid1:uid2 + AdvanceToNextToken(); + // clear copy response uid + fServerConnection.SetCopyResponseUid(fNextToken); + } + if (ContinueParse()) AdvanceToNextToken(); + } + } else if (!PL_strcasecmp(fNextToken, "HIGHESTMODSEQ")) { + AdvanceToNextToken(); + if (ContinueParse()) { + fHighestModSeq = ParseUint64Str(fNextToken); + fUseModSeq = true; + AdvanceToNextToken(); + } + } else if (!PL_strcasecmp(fNextToken, "NOMODSEQ]")) { + fHighestModSeq = 0; + fUseModSeq = false; + skip_to_CRLF(); + } else if (!PL_strcasecmp(fNextToken, "CAPABILITY")) { + capability_data(); + } else if (!PL_strcasecmp(fNextToken, "MYRIGHTS")) { + myrights_data(true); + } else // just text + { + // do nothing but eat tokens until we see the ] or CRLF + // we should see the ] but we don't want to go into an + // endless loop if the CRLF is not there + do { + AdvanceToNextToken(); + } while (!PL_strcasestr(fNextToken, "]") && !fAtEndOfLine && + ContinueParse()); + } + } +} + +// RFC3501: response-done = response-tagged / response-fatal +void nsImapServerResponseParser::response_done() { + if (ContinueParse()) { + if (!PL_strcmp(fCurrentCommandTag, fNextToken)) + response_tagged(); + else + response_fatal(); + } +} + +// RFC3501: response-tagged = tag SP resp-cond-state CRLF +void nsImapServerResponseParser::response_tagged() { + // eat the tag + AdvanceToNextToken(); + if (ContinueParse()) { + resp_cond_state(true); + if (ContinueParse()) { + if (!fAtEndOfLine) + SetSyntaxError(true); + else if (!fCurrentCommandFailed) + ResetLexAnalyzer(); + } + } +} + +// RFC3501: response-fatal = "*" SP resp-cond-bye CRLF +// ; Server closes connection immediately +void nsImapServerResponseParser::response_fatal() { + // eat the "*" + AdvanceToNextToken(); + if (ContinueParse()) resp_cond_bye(); +} + +// RFC3501: resp-cond-bye = "BYE" SP resp-text +void nsImapServerResponseParser::resp_cond_bye() { + SetConnected(false); + fIMAPstate = kNonAuthenticated; +} + +void nsImapServerResponseParser::msg_fetch_headers(const char* partNum) { + msg_fetch_content(false, 0, MESSAGE_RFC822); +} + +/* nstring ::= string / nil +string ::= quoted / literal +nil ::= "NIL" + +*/ +void nsImapServerResponseParser::msg_fetch_content(bool chunk, int32_t origin, + const char* content_type) { + // setup the stream for downloading this message. + // Note: no longer concerned with body shell issues since now we only fetch + // full message. + if ((!chunk || (origin == 0)) && !GetDownloadingHeaders()) { + if (NS_FAILED(BeginMessageDownload(content_type))) return; + } + + if (PL_strcasecmp(fNextToken, "NIL")) { + if (*fNextToken == '"') + fLastChunk = msg_fetch_quoted(); + else + fLastChunk = msg_fetch_literal(chunk, origin); + } else + AdvanceToNextToken(); // eat "NIL" + + if (fLastChunk) { + // complete the message download + if (ContinueParse()) { + if (fReceivedHeaderOrSizeForUID == CurrentResponseUID()) { + fServerConnection.NormalMessageEndDownload(); + fReceivedHeaderOrSizeForUID = nsMsgKey_None; + } else + fReceivedHeaderOrSizeForUID = CurrentResponseUID(); + } else + fServerConnection.AbortMessageDownLoad(); + } +} + +void nsImapServerResponseParser::mime_part_data() { + char* checkOriginToken = PL_strdup(fNextToken); + if (checkOriginToken) { + uint32_t origin = 0; + bool originFound = false; + char* whereStart = PL_strchr(checkOriginToken, '<'); + if (whereStart) { + char* whereEnd = PL_strchr(whereStart, '>'); + if (whereEnd) { + *whereEnd = 0; + whereStart++; + origin = atoi(whereStart); + originFound = true; + } + } + PR_Free(checkOriginToken); + AdvanceToNextToken(); + msg_fetch_content(originFound, origin, + MESSAGE_RFC822); // keep content type as message/rfc822, + // even though the + // MIME part might not be, because then libmime will + // still handle and decode it. + } else + HandleMemoryFailure(); +} + +/* +quoted ::= <"> *QUOTED_CHAR <"> + + QUOTED_CHAR ::= <any TEXT_CHAR except quoted_specials> / + "\" quoted_specials + + quoted_specials ::= <"> / "\" +*/ + +bool nsImapServerResponseParser::msg_fetch_quoted() { + // *Should* never get a quoted string in response to a chunked download, + // but the RFCs don't forbid it + char* q = CreateQuoted(); + if (q) { + numberOfCharsInThisChunk = PL_strlen(q); + fServerConnection.HandleMessageDownLoadLine(q, false, q); + PR_Free(q); + } else + numberOfCharsInThisChunk = 0; + + AdvanceToNextToken(); + bool lastChunk = + ((fServerConnection.GetCurFetchSize() == 0) || + (numberOfCharsInThisChunk != fServerConnection.GetCurFetchSize())); + return lastChunk; +} + +/* msg_obsolete ::= "COPY" / ("STORE" SPACE msg_fetch) +;; OBSOLETE untagged data responses */ +void nsImapServerResponseParser::msg_obsolete() { + if (!PL_strcasecmp(fNextToken, "COPY")) + AdvanceToNextToken(); + else if (!PL_strcasecmp(fNextToken, "STORE")) { + AdvanceToNextToken(); + if (ContinueParse()) msg_fetch(); + } else + SetSyntaxError(true); +} + +void nsImapServerResponseParser::capability_data() { + int32_t endToken = -1; + fCapabilityFlag = kCapabilityDefined | kHasAuthOldLoginCapability; + do { + AdvanceToNextToken(); + if (fNextToken) { + nsCString token(fNextToken); + endToken = token.FindChar(']'); + if (endToken >= 0) token.SetLength(endToken); + + if (token.Equals("AUTH=LOGIN", nsCaseInsensitiveCStringComparator)) + fCapabilityFlag |= kHasAuthLoginCapability; + else if (token.Equals("AUTH=PLAIN", nsCaseInsensitiveCStringComparator)) + fCapabilityFlag |= kHasAuthPlainCapability; + else if (token.Equals("AUTH=CRAM-MD5", + nsCaseInsensitiveCStringComparator)) + fCapabilityFlag |= kHasCRAMCapability; + else if (token.Equals("AUTH=NTLM", nsCaseInsensitiveCStringComparator)) + fCapabilityFlag |= kHasAuthNTLMCapability; + else if (token.Equals("AUTH=GSSAPI", nsCaseInsensitiveCStringComparator)) + fCapabilityFlag |= kHasAuthGssApiCapability; + else if (token.Equals("AUTH=MSN", nsCaseInsensitiveCStringComparator)) + fCapabilityFlag |= kHasAuthMSNCapability; + else if (token.Equals("AUTH=EXTERNAL", + nsCaseInsensitiveCStringComparator)) + fCapabilityFlag |= kHasAuthExternalCapability; + else if (token.Equals("AUTH=XOAUTH2", nsCaseInsensitiveCStringComparator)) + fCapabilityFlag |= kHasXOAuth2Capability; + else if (token.Equals("STARTTLS", nsCaseInsensitiveCStringComparator)) + fCapabilityFlag |= kHasStartTLSCapability; + else if (token.Equals("LOGINDISABLED", + nsCaseInsensitiveCStringComparator)) + fCapabilityFlag &= ~kHasAuthOldLoginCapability; // remove flag + else if (token.Equals("XSENDER", nsCaseInsensitiveCStringComparator)) + fCapabilityFlag |= kHasXSenderCapability; + else if (token.Equals("IMAP4", nsCaseInsensitiveCStringComparator)) + fCapabilityFlag |= kIMAP4Capability; + else if (token.Equals("IMAP4rev1", nsCaseInsensitiveCStringComparator)) + fCapabilityFlag |= kIMAP4rev1Capability; + else if (Substring(token, 0, 5) + .Equals("IMAP4", nsCaseInsensitiveCStringComparator)) + fCapabilityFlag |= kIMAP4other; + else if (token.Equals("X-NO-ATOMIC-RENAME", + nsCaseInsensitiveCStringComparator)) + fCapabilityFlag |= kNoHierarchyRename; + else if (token.Equals("X-NON-HIERARCHICAL-RENAME", + nsCaseInsensitiveCStringComparator)) + fCapabilityFlag |= kNoHierarchyRename; + else if (token.Equals("NAMESPACE", nsCaseInsensitiveCStringComparator)) + fCapabilityFlag |= kNamespaceCapability; + else if (token.Equals("ID", nsCaseInsensitiveCStringComparator)) + fCapabilityFlag |= kHasIDCapability; + else if (token.Equals("ACL", nsCaseInsensitiveCStringComparator)) + fCapabilityFlag |= kACLCapability; + else if (token.Equals("XSERVERINFO", nsCaseInsensitiveCStringComparator)) + fCapabilityFlag |= kXServerInfoCapability; + else if (token.Equals("UIDPLUS", nsCaseInsensitiveCStringComparator)) + fCapabilityFlag |= kUidplusCapability; + else if (token.Equals("LITERAL+", nsCaseInsensitiveCStringComparator)) + fCapabilityFlag |= kLiteralPlusCapability; + else if (token.Equals("XAOL-OPTION", nsCaseInsensitiveCStringComparator)) + fCapabilityFlag |= kAOLImapCapability; + else if (token.Equals("X-GM-EXT-1", nsCaseInsensitiveCStringComparator)) + fCapabilityFlag |= kGmailImapCapability; + else if (token.Equals("QUOTA", nsCaseInsensitiveCStringComparator)) + fCapabilityFlag |= kQuotaCapability; + else if (token.Equals("LANGUAGE", nsCaseInsensitiveCStringComparator)) + fCapabilityFlag |= kHasLanguageCapability; + else if (token.Equals("IDLE", nsCaseInsensitiveCStringComparator)) + fCapabilityFlag |= kHasIdleCapability; + else if (token.Equals("CONDSTORE", nsCaseInsensitiveCStringComparator)) + fCapabilityFlag |= kHasCondStoreCapability; + else if (token.Equals("ENABLE", nsCaseInsensitiveCStringComparator)) + fCapabilityFlag |= kHasEnableCapability; + else if (token.Equals("LIST-EXTENDED", + nsCaseInsensitiveCStringComparator)) + fCapabilityFlag |= kHasListExtendedCapability; + else if (token.Equals("XLIST", nsCaseInsensitiveCStringComparator)) + fCapabilityFlag |= kHasXListCapability; + else if (token.Equals("SPECIAL-USE", nsCaseInsensitiveCStringComparator)) + fCapabilityFlag |= kHasSpecialUseCapability; + else if (token.Equals("COMPRESS=DEFLATE", + nsCaseInsensitiveCStringComparator)) + fCapabilityFlag |= kHasCompressDeflateCapability; + else if (token.Equals("MOVE", nsCaseInsensitiveCStringComparator)) + fCapabilityFlag |= kHasMoveCapability; + else if (token.Equals("HIGHESTMODSEQ", + nsCaseInsensitiveCStringComparator)) + fCapabilityFlag |= kHasHighestModSeqCapability; + else if (token.Equals("CLIENTID", nsCaseInsensitiveCStringComparator)) + fCapabilityFlag |= kHasClientIDCapability; + else if (token.Equals("UTF8=ACCEPT", + nsCaseInsensitiveCStringComparator) || + token.Equals("UTF8=ONLY", nsCaseInsensitiveCStringComparator)) + fCapabilityFlag |= kHasUTF8AcceptCapability; + } + } while (fNextToken && endToken < 0 && !fAtEndOfLine && ContinueParse()); + + nsImapProtocol* navCon = &fServerConnection; + NS_ASSERTION(navCon, + "null imap protocol connection while parsing capability " + "response"); // we should always have this + if (navCon) { + navCon->CommitCapability(); + fServerConnection.SetCapabilityResponseOccurred(); + } + skip_to_CRLF(); +} + +void nsImapServerResponseParser::xmailboxinfo_data() { + AdvanceToNextToken(); + if (!fNextToken) return; + + char* mailboxName = CreateAstring(); // PL_strdup(fNextToken); + if (mailboxName) { + do { + AdvanceToNextToken(); + if (fNextToken) { + if (!PL_strcmp("MANAGEURL", fNextToken)) { + AdvanceToNextToken(); + fFolderAdminUrl = CreateAstring(); + } else if (!PL_strcmp("POSTURL", fNextToken)) { + AdvanceToNextToken(); + // ignore this for now... + } + } + } while (fNextToken && !fAtEndOfLine && ContinueParse()); + } +} + +void nsImapServerResponseParser::xserverinfo_data() { + do { + AdvanceToNextToken(); + if (!fNextToken) break; + if (!PL_strcmp("MANAGEACCOUNTURL", fNextToken)) { + AdvanceToNextToken(); + fMailAccountUrl.Adopt(CreateNilString()); + } else if (!PL_strcmp("MANAGELISTSURL", fNextToken)) { + AdvanceToNextToken(); + fManageListsUrl.Adopt(CreateNilString()); + } else if (!PL_strcmp("MANAGEFILTERSURL", fNextToken)) { + AdvanceToNextToken(); + fManageFiltersUrl.Adopt(CreateNilString()); + } + } while (fNextToken && !fAtEndOfLine && ContinueParse()); +} + +void nsImapServerResponseParser::enable_data() { + do { + // eat each enable response; + AdvanceToNextToken(); + if (!PL_strcasecmp("UTF8=ACCEPT", fNextToken)) fUtf8AcceptEnabled = true; + } while (fNextToken && !fAtEndOfLine && ContinueParse()); +} + +void nsImapServerResponseParser::language_data() { + // we may want to go out and store the language returned to us + // by the language command in the host info session stuff. + + // for now, just eat the language.... + do { + // eat each language returned to us + AdvanceToNextToken(); + } while (fNextToken && !fAtEndOfLine && ContinueParse()); +} + +// cram/auth response data ::= "+" SPACE challenge CRLF +// the server expects more client data after issuing its challenge + +void nsImapServerResponseParser::authChallengeResponse_data() { + AdvanceToNextToken(); + fAuthChallenge = strdup(fNextToken); + fWaitingForMoreClientInput = true; + + skip_to_CRLF(); +} + +void nsImapServerResponseParser::namespace_data() { + EIMAPNamespaceType namespaceType = kPersonalNamespace; + bool namespacesCommitted = false; + const char* serverKey = fServerConnection.GetImapServerKey(); + while ((namespaceType != kUnknownNamespace) && ContinueParse()) { + AdvanceToNextToken(); + while (fAtEndOfLine && ContinueParse()) AdvanceToNextToken(); + if (!PL_strcasecmp(fNextToken, "NIL")) { + // No namespace for this type. + // Don't add anything to the Namespace object. + } else if (fNextToken[0] == '(') { + // There may be multiple namespaces of the same type. + // Go through each of them and add them to our Namespace object. + + fNextToken++; + while (fNextToken[0] == '(' && ContinueParse()) { + // we have another namespace for this namespace type + fNextToken++; + if (fNextToken[0] != '"') { + SetSyntaxError(true); + } else { + char* namespacePrefix = CreateQuoted(false); + + AdvanceToNextToken(); + const char* quotedDelimiter = fNextToken; + char namespaceDelimiter = '\0'; + + if (quotedDelimiter[0] == '"') { + quotedDelimiter++; + namespaceDelimiter = quotedDelimiter[0]; + } else if (!PL_strncasecmp(quotedDelimiter, "NIL", 3)) { + // NIL hierarchy delimiter. Leave namespace delimiter nullptr. + } else { + // not quoted or NIL. + SetSyntaxError(true); + } + if (ContinueParse()) { + // Add code to parse the TRANSLATE attribute if it is present.... + // we'll also need to expand the name space code to take in the + // translated prefix name. + + // Add it to a temporary list in the host. + if (fHostSessionList) { + nsImapNamespace* newNamespace = new nsImapNamespace( + namespaceType, namespacePrefix, namespaceDelimiter, false); + fHostSessionList->AddNewNamespaceForHost(serverKey, newNamespace); + } + + skip_to_close_paren(); // Ignore any extension data + + bool endOfThisNamespaceType = (fNextToken[0] == ')'); + if (!endOfThisNamespaceType && + fNextToken[0] != + '(') // no space between namespaces of the same type + { + SetSyntaxError(true); + } + } + PR_Free(namespacePrefix); + } + } + } else { + SetSyntaxError(true); + } + switch (namespaceType) { + case kPersonalNamespace: + namespaceType = kOtherUsersNamespace; + break; + case kOtherUsersNamespace: + namespaceType = kPublicNamespace; + break; + default: + namespaceType = kUnknownNamespace; + break; + } + } + if (ContinueParse()) { + nsImapProtocol* navCon = &fServerConnection; + NS_ASSERTION( + navCon, + "null protocol connection while parsing namespace"); // we should + // always have + // this + if (navCon) { + navCon->CommitNamespacesForHostEvent(); + namespacesCommitted = true; + } + } + skip_to_CRLF(); + + if (!namespacesCommitted && fHostSessionList) { + bool success; + fHostSessionList->FlushUncommittedNamespacesForHost(serverKey, success); + } +} + +void nsImapServerResponseParser::myrights_data(bool unsolicited) { + AdvanceToNextToken(); + if (ContinueParse() && !fAtEndOfLine) { + char* mailboxName; + // an unsolicited myrights response won't have the mailbox name in + // the response, so we use the selected mailbox name. + if (unsolicited) { + mailboxName = strdup(fSelectedMailboxName); + } else { + mailboxName = CreateAstring(); + if (mailboxName) AdvanceToNextToken(); + } + if (mailboxName) { + if (ContinueParse()) { + char* myrights = CreateAstring(); + if (myrights) { + nsImapProtocol* navCon = &fServerConnection; + NS_ASSERTION( + navCon, "null connection parsing my rights"); // we should always + // have this + if (navCon) + navCon->AddFolderRightsForUser(mailboxName, + nullptr /* means "me" */, myrights); + PR_Free(myrights); + } else { + HandleMemoryFailure(); + } + if (ContinueParse()) AdvanceToNextToken(); + } + PR_Free(mailboxName); + } else { + HandleMemoryFailure(); + } + } else { + SetSyntaxError(true); + } +} + +void nsImapServerResponseParser::acl_data() { + AdvanceToNextToken(); + if (ContinueParse() && !fAtEndOfLine) { + char* mailboxName = CreateAstring(); // PL_strdup(fNextToken); + if (mailboxName && ContinueParse()) { + AdvanceToNextToken(); + while (ContinueParse() && !fAtEndOfLine) { + char* userName = CreateAstring(); // PL_strdup(fNextToken); + if (userName && ContinueParse()) { + AdvanceToNextToken(); + if (ContinueParse()) { + char* rights = CreateAstring(); // PL_strdup(fNextToken); + if (rights) { + fServerConnection.AddFolderRightsForUser(mailboxName, userName, + rights); + PR_Free(rights); + } else + HandleMemoryFailure(); + + if (ContinueParse()) AdvanceToNextToken(); + } + PR_Free(userName); + } else + HandleMemoryFailure(); + } + PR_Free(mailboxName); + } else + HandleMemoryFailure(); + } +} + +// RFC2087: quotaroot_response = "QUOTAROOT" SP astring *(SP astring) +// quota_response = "QUOTA" SP astring SP quota_list +// quota_list = "(" [quota_resource *(SP quota_resource)] ")" +// quota_resource = atom SP number SP number +// draft-melnikov-extra-quota-00 proposes some additions to RFC2087 and +// improves the documentation. We still only support RFC2087 capability QUOTA +// and command GETQUOTAROOT and its untagged QUOTAROOT and QUOTA responses. +void nsImapServerResponseParser::quota_data() { + if (!PL_strcasecmp(fNextToken, "QUOTAROOT")) { + // Ignore QUOTAROOT response (except to invalidate previously stored data). + nsCString quotaroot; + AdvanceToNextToken(); + while (ContinueParse() && !fAtEndOfLine) { + quotaroot.Adopt(CreateAstring()); + AdvanceToNextToken(); + } + // Invalidate any previously stored quota data. Updated QUOTA data follows. + fServerConnection.UpdateFolderQuotaData(kInvalidateQuota, quotaroot, 0, 0); + } else if (!PL_strcasecmp(fNextToken, "QUOTA")) { + // Should have one QUOTA response per QUOTAROOT. + uint64_t usage, limit; + AdvanceToNextToken(); + if (ContinueParse()) { + nsCString quotaroot; + quotaroot.Adopt(CreateAstring()); + nsCString resource; + AdvanceToNextToken(); + if (fNextToken) { + if (fNextToken[0] == '(') fNextToken++; + // Should have zero or more "resource|usage|limit" triplet per quotaroot + // name. See draft-melnikov-extra-quota-00 for specific examples. Well + // known resources are STORAGE (in Kbytes), number of MESSAGEs and + // number of MAILBOXes. However, servers typically only set a quota on + // STORAGE in KBytes. A mailbox can have multiple quotaroots but + // typically only one and with a single resource. + while (ContinueParse() && !fAtEndOfLine) { + resource.Adopt(CreateAstring()); + AdvanceToNextToken(); + usage = atoll(fNextToken); + AdvanceToNextToken(); + nsAutoCString limitToken(fNextToken); + if (fNextToken[strlen(fNextToken) - 1] == ')') + limitToken.SetLength(strlen(fNextToken) - 1); + limit = atoll(limitToken.get()); + // Some servers don't define a quotaroot name which we displays as + // blank. + nsCString quotaRootResource(quotaroot); + if (!quotaRootResource.IsEmpty()) { + quotaRootResource.AppendLiteral(" / "); + } + quotaRootResource.Append(resource); + fServerConnection.UpdateFolderQuotaData( + kStoreQuota, quotaRootResource, usage, limit); + AdvanceToNextToken(); + } + } + } + } else { + SetSyntaxError(true); + } +} + +void nsImapServerResponseParser::id_data() { + AdvanceToNextToken(); + if (!PL_strcasecmp(fNextToken, "NIL")) + AdvanceToNextToken(); + else + fServerIdResponse.Adopt(CreateParenGroup()); + skip_to_CRLF(); +} + +bool nsImapServerResponseParser::GetDownloadingHeaders() { + return fDownloadingHeaders; +} + +void nsImapServerResponseParser::ResetCapabilityFlag() {} + +/* + literal ::= "{" number "}" CRLF *CHAR8 + Number represents the number of CHAR8 octets + */ + +// Processes a message body, header or message part fetch response. Typically +// the full message, header or part are processed in one call (effectively, one +// chunk), and parameter `chunk` is false and `origin` (offset into the +// response) is 0. But under some conditions and larger messages, multiple calls +// will occur to process the message in multiple chunks and parameter `chunk` +// will be true and parameter `origin` will increase by the chunk size from +// initially 0 with each call. This function returns true if this is the last or +// only chunk. This signals the caller that the stream should be closed since +// the message response has been processed. +bool nsImapServerResponseParser::msg_fetch_literal(bool chunk, int32_t origin) { + numberOfCharsInThisChunk = atoi(fNextToken + 1); + // If we didn't request a specific size, or the server isn't returning exactly + // as many octets as we requested, this must be the last or only chunk + bool lastChunk = (!chunk || (numberOfCharsInThisChunk != + fServerConnection.GetCurFetchSize())); + + // clang-format off + if (lastChunk) + MOZ_LOG(IMAP, mozilla::LogLevel::Debug, + ("PARSER: msg_fetch_literal() chunking=%s, requested=%d, receiving=%d", + chunk ? "true":"false", fServerConnection.GetCurFetchSize(), + numberOfCharsInThisChunk)); + // clang-format on + + charsReadSoFar = 0; + + while (ContinueParse() && !fServerConnection.DeathSignalReceived() && + (charsReadSoFar < numberOfCharsInThisChunk)) { + AdvanceToNextLine(); + if (ContinueParse()) { + // When "\r\n" (CRLF) is split across two chunks, the '\n' at the + // beginning of the next chunk might be set to an empty line consisting + // only of "\r\n". This is observed while running unit tests with the imap + // "fake" server. The unexpected '\r' is discarded here. However, with + // several real world servers tested, e.g., Dovecot, Gmail, Outlook, Yahoo + // etc., the leading + // '\r' is not inserted so the beginning line of the next chunk remains + // just '\n' and no discard is required. + // In any case, this "orphan" line is ignored and not processed below. + if (fNextChunkStartsWithNewline && (*fCurrentLine == '\r')) { + // Cause fCurrentLine to point to '\n' which discards the '\r'. + char* usableCurrentLine = PL_strdup(fCurrentLine + 1); + PR_Free(fCurrentLine); + fCurrentLine = usableCurrentLine; + } + + // strlen() *would* fail on data containing \0, but the above + // AdvanceToNextLine() in nsMsgLineStreamBuffer::ReadNextLine() we replace + // '\0' with ' ' (blank) because who cares about binary transparency, and + // anyway \0 in this context violates RFCs. + charsReadSoFar += strlen(fCurrentLine); + if (!fDownloadingHeaders && fCurrentCommandIsSingleMessageFetch) { + fServerConnection.ProgressEventFunctionUsingName( + "imapDownloadingMessage"); + if (fTotalDownloadSize > 0) + fServerConnection.PercentProgressUpdateEvent( + ""_ns, u""_ns, charsReadSoFar + origin, fTotalDownloadSize); + } + if (charsReadSoFar > numberOfCharsInThisChunk) { + // This is the last line of a chunk. "Literal" here means actual email + // data and its EOLs, without imap protocol elements and their EOLs. End + // of line is defined by two characters \r\n (i.e., CRLF, 0xd,0xa) + // specified by RFC822. Here is an example the most typical last good + // line of a chunk: "1s8AA5i4AAvF4QAG6+sAAD0bAPsAAAAA1OAAC)\r\n", where + // ")\r\n" are non-literals. This an example of the last "good" line of + // a chunk that terminates with \r\n + // "FxcA/wAAAALN2gADu80ACS0nAPpVVAD1wNAABF5YAPhAJgD31+QABAAAAP8oMQD+HBwA/umj\r\n" + // followed by another line of non-literal data: + // " UID 1004)\r\n". These two are concatenated into a single string + // pointed to by fCurrentLine. The extra "non-literal data" on the last + // chunk line makes the charsReadSoFar greater than + // numberOfCharsInThisChunk (the configured chunk size). A problem + // occurs if the \r\n of the long line above is split between chunks and + // \n is contained in the next chunk. For example, if last lines of + // chunk X are: + // "/gAOC/wA/QAAAAAAAAAA8wACCvz+AgIEAAD8/P4ABQUAAPoAAAD+AAEA/voHAAQGBQD/BAQA\r" + // ")\r\n" + // and the first two lines of chunk X+1 are: + // "\n" + // "APwAAAAAmZkA/wAAAAAREQD/AAAAAquVAAbk8QAHCBAAAPD0AAP5+wABRCoA+0BgAP0AAAAA\r\n" + // The missing '\n' on the last line of chunk X must be added back and + // the line consisting only of "\n" in chunk X+1 must be ignored in + // order to produce the the correct output. This is needed to insure + // that the signature verification of cryptographically signed emails + // does not fail due to missing or extra EOL characters. Otherwise, the + // extra or missing \n or \r doesn't really matter. + // + // Special case observed only with the "fake" imap server used with TB + // unit test. When the "\r\n" at the end of a chunk is split as + // described above, the \n at the beginning of the next chunk may + // actually be "\r\n" like this example: Last lines of chunk X + // "/gAOC/wA/QAAAAAAAAAA8wACCvz+AgIEAAD8/P4ABQUAAPoAAAD+AAEA/voHAAQGBQD/BAQA\r" + // ")\r\n" + // and the first two lines of chunk X+1: + // "\r\n" <-- The code changes this to just "\n" like it should be. + // "APwAAAAAmZkA/wAAAAAREQD/AAAAAquVAAbk8QAHCBAAAPD0AAP5+wABRCoA+0BgAP0AAAAA\r\n" + // + // Implementation: + // Obtain pointer to last literal in chunk X, e.g., 'C' in 1st example + // above, or to the \n or \r in the other examples. + char* displayEndOfLine = + (fCurrentLine + strlen(fCurrentLine) - + (charsReadSoFar - numberOfCharsInThisChunk + 1)); + // Save so original unmodified fCurrentLine is restored below. + char saveit1 = displayEndOfLine[1]; + char saveit2 = 0; // Keep compiler happy. + // Determine if EOL is split such that Chunk X has the \r and chunk + // X+1 has the \n. + fNextChunkStartsWithNewline = (displayEndOfLine[0] == '\r'); + if (fNextChunkStartsWithNewline) { + saveit2 = displayEndOfLine[2]; + // Add the missing newline and terminate the string. + displayEndOfLine[1] = '\n'; + displayEndOfLine[2] = 0; + // This is a good thing to log. + MOZ_LOG(IMAP, mozilla::LogLevel::Info, + ("PARSER: CR/LF split at chunk boundary")); + } else { + // Typical case where EOLs are not split. Terminate the string. + displayEndOfLine[1] = 0; + } + // Process this modified string pointed to by fCurrentLine. + fServerConnection.HandleMessageDownLoadLine(fCurrentLine, !lastChunk); + // Restore fCurrentLine's original content. + displayEndOfLine[1] = saveit1; + if (fNextChunkStartsWithNewline) displayEndOfLine[2] = saveit2; + } else { + // Not the last line of a chunk. + bool processTheLine = true; + if (fNextChunkStartsWithNewline && origin > 0) { + // A split of the \r\n between chunks was detected. Ignore orphan \n + // on line by itself which can occur on the first line of a 2nd or + // later chunk. Line length should be 1 and the only character should + // be \n. Note: If previous message ended with just \r, don't expect + // the first chunk of a message (origin == 0) to begin with \n. + // (Typically, there is only one chunk required for a message or + // header response unless its size exceeds the chunking threshold.) + if (strlen(fCurrentLine) > 1 || fCurrentLine[0] != '\n') { + // In case expected orphan \n is not really there, go ahead and + // process the line. This should theoretically not occur but rarely, + // and for yet to be determined reasons, it does. Logging may help. + NS_WARNING( + "'\\n' is not the only character in this line as expected!"); + MOZ_LOG(IMAP, mozilla::LogLevel::Debug, + ("PARSER: expecting just '\\n' but line is = |%s|", + fCurrentLine)); + } else { + // Discard the line containing only \n. + processTheLine = false; + MOZ_LOG(IMAP, mozilla::LogLevel::Debug, + ("PARSER: discarding lone '\\n'")); + } + } + if (processTheLine) { + fServerConnection.HandleMessageDownLoadLine( + fCurrentLine, + !lastChunk && (charsReadSoFar == numberOfCharsInThisChunk), + fCurrentLine); + } + fNextChunkStartsWithNewline = false; + } + } + } + + if (ContinueParse()) { + if (charsReadSoFar > numberOfCharsInThisChunk) { + // move the lexical analyzer state to the end of this message because this + // message fetch ends in the middle of this line. + AdvanceTokenizerStartingPoint( + strlen(fCurrentLine) - (charsReadSoFar - numberOfCharsInThisChunk)); + AdvanceToNextToken(); + } else { + skip_to_CRLF(); + AdvanceToNextToken(); + } + } else { + // Don't typically (maybe never?) see this. + fNextChunkStartsWithNewline = false; + } + return lastChunk; +} + +bool nsImapServerResponseParser::CurrentFolderReadOnly() { + return fCurrentFolderReadOnly; +} + +int32_t nsImapServerResponseParser::NumberOfMessages() { + return fNumberOfExistingMessages; +} + +int32_t nsImapServerResponseParser::NumberOfRecentMessages() { + return fNumberOfRecentMessages; +} + +int32_t nsImapServerResponseParser::FolderUID() { return fFolderUIDValidity; } + +void nsImapServerResponseParser::SetCurrentResponseUID(uint32_t uid) { + if (uid > 0) fCurrentResponseUID = uid; +} + +uint32_t nsImapServerResponseParser::CurrentResponseUID() { + return fCurrentResponseUID; +} + +uint32_t nsImapServerResponseParser::HighestRecordedUID() { + return fHighestRecordedUID; +} + +void nsImapServerResponseParser::ResetHighestRecordedUID() { + fHighestRecordedUID = 0; +} + +bool nsImapServerResponseParser::IsNumericString(const char* string) { + int i; + for (i = 0; i < (int)PL_strlen(string); i++) { + if (!isdigit(string[i])) { + return false; + } + } + + return true; +} + +// Capture the mailbox state for folder select/update and for status. +// If mailboxName is null, we've done imap SELECT; otherwise STATUS. +already_AddRefed<nsImapMailboxSpec> +nsImapServerResponseParser::CreateCurrentMailboxSpec( + const char* mailboxName /* = nullptr */) { + RefPtr<nsImapMailboxSpec> returnSpec = new nsImapMailboxSpec; + const char* mailboxNameToConvert = + (mailboxName) ? mailboxName : fSelectedMailboxName; + if (mailboxNameToConvert) { + const char* serverKey = fServerConnection.GetImapServerKey(); + nsImapNamespace* ns = nullptr; + if (serverKey && fHostSessionList) + fHostSessionList->GetNamespaceForMailboxForHost( + serverKey, mailboxNameToConvert, ns); // for + // delimiter + returnSpec->mHierarchySeparator = (ns) ? ns->GetDelimiter() : '/'; + } + + returnSpec->mFolderSelected = !mailboxName; + returnSpec->mFolder_UIDVALIDITY = fFolderUIDValidity; + returnSpec->mHighestModSeq = fHighestModSeq; + // clang-format off + returnSpec->mNumOfMessages = + (mailboxName) ? fStatusExistingMessages : fNumberOfExistingMessages; + returnSpec->mNumOfUnseenMessages = + (mailboxName) ? fStatusUnseenMessages : -1; + returnSpec->mNumOfRecentMessages = + (mailboxName) ? fStatusRecentMessages : fNumberOfRecentMessages; + returnSpec->mNextUID = + (mailboxName) ? fStatusNextUID : fNextUID; + // clang-format on + + returnSpec->mSupportedUserFlags = fSupportsUserDefinedFlags; + + returnSpec->mBoxFlags = kNoFlags; // stub + returnSpec->mOnlineVerified = false; // Fabricated. Flags aren't verified. + returnSpec->mAllocatedPathName.Assign(mailboxNameToConvert); + returnSpec->mConnection = &fServerConnection; + if (returnSpec->mConnection) { + nsIURI* aUrl = nullptr; + nsresult rv = NS_OK; + returnSpec->mConnection->GetCurrentUrl()->QueryInterface(NS_GET_IID(nsIURI), + (void**)&aUrl); + if (NS_SUCCEEDED(rv) && aUrl) aUrl->GetHost(returnSpec->mHostName); + + NS_IF_RELEASE(aUrl); + } else + returnSpec->mHostName.Truncate(); + + if (fFlagState) + returnSpec->mFlagState = fFlagState; // copies flag state + else + returnSpec->mFlagState = nullptr; + + return returnSpec.forget(); +} +// Reset the flag state. +void nsImapServerResponseParser::ResetFlagInfo() { + if (fFlagState) fFlagState->Reset(); +} + +bool nsImapServerResponseParser::GetLastFetchChunkReceived() { + return fLastChunk; +} + +void nsImapServerResponseParser::ClearLastFetchChunkReceived() { + fLastChunk = false; +} + +int32_t nsImapServerResponseParser::GetNumBytesFetched() { + return numberOfCharsInThisChunk; +} + +void nsImapServerResponseParser::ClearNumBytesFetched() { + numberOfCharsInThisChunk = 0; +} + +void nsImapServerResponseParser::SetHostSessionList( + nsIImapHostSessionList* aHostSessionList) { + fHostSessionList = aHostSessionList; +} + +void nsImapServerResponseParser::SetSyntaxError(bool error, const char* msg) { + nsImapGenericParser::SetSyntaxError(error, msg); + if (error) { + if (!fCurrentLine) { + HandleMemoryFailure(); + fServerConnection.Log("PARSER", ("Internal Syntax Error: %s: <no line>"), + msg); + } else { + if (!strcmp(fCurrentLine, CRLF)) + fServerConnection.Log("PARSER", "Internal Syntax Error: %s: <CRLF>", + msg); + else { + if (msg) + fServerConnection.Log("PARSER", "Internal Syntax Error: %s:", msg); + fServerConnection.Log("PARSER", "Internal Syntax Error on line: %s", + fCurrentLine); + } + } + } +} + +nsresult nsImapServerResponseParser::BeginMessageDownload( + const char* content_type) { + nsresult rv = fServerConnection.BeginMessageDownLoad(fSizeOfMostRecentMessage, + content_type); + if (NS_FAILED(rv)) { + skip_to_CRLF(); + fServerConnection.PseudoInterrupt(true); + fServerConnection.AbortMessageDownLoad(); + } + return rv; +} diff --git a/comm/mailnews/imap/src/nsImapServerResponseParser.h b/comm/mailnews/imap/src/nsImapServerResponseParser.h new file mode 100644 index 0000000000..43d6a73611 --- /dev/null +++ b/comm/mailnews/imap/src/nsImapServerResponseParser.h @@ -0,0 +1,275 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef _nsIMAPServerResponseParser_H_ +#define _nsIMAPServerResponseParser_H_ + +#include "mozilla/Attributes.h" +#include "../public/nsIImapHostSessionList.h" +#include "nsImapSearchResults.h" +#include "nsString.h" +#include "MailNewsTypes.h" +#include "nsTArray.h" +#include "nsImapUtils.h" + +class nsImapSearchResultIterator; +class nsIImapFlagAndUidState; + +#include "nsImapGenericParser.h" + +class nsImapServerResponseParser : public nsImapGenericParser { + public: + explicit nsImapServerResponseParser(nsImapProtocol& imapConnection); + virtual ~nsImapServerResponseParser(); + + // Overridden from the base parser class + virtual bool LastCommandSuccessful() override; + virtual void HandleMemoryFailure() override; + + // aignoreBadAndNOResponses --> don't throw a error dialog if this command + // results in a NO or Bad response from the server..in other words the command + // is "exploratory" and we don't really care if it succeeds or fails. This + // value is typically FALSE for almost all cases. + virtual void ParseIMAPServerResponse(const char* aCurrentCommand, + bool aIgnoreBadAndNOResponses, + char* aGreetingWithCapability = NULL); + virtual void InitializeState(); + bool CommandFailed(); + void SetCommandFailed(bool failed); + bool UntaggedResponse(); + + enum eIMAPstate { kNonAuthenticated, kAuthenticated, kFolderSelected }; + + virtual eIMAPstate GetIMAPstate(); + virtual bool WaitingForMoreClientInput() { + return fWaitingForMoreClientInput; + } + const char* GetSelectedMailboxName(); // can be NULL + bool IsStdJunkNotJunkUseOk() { return fStdJunkNotJunkUseOk; } + + // if we get a PREAUTH greeting from the server, initialize the parser to + // begin in the kAuthenticated state + void PreauthSetAuthenticatedState(); + + // these functions represent the state of the currently selected + // folder + bool CurrentFolderReadOnly(); + int32_t NumberOfMessages(); + int32_t NumberOfRecentMessages(); + int32_t FolderUID(); + uint32_t CurrentResponseUID(); + uint32_t HighestRecordedUID(); + void ResetHighestRecordedUID(); + void SetCurrentResponseUID(uint32_t uid); + bool IsNumericString(const char* string); + uint32_t SizeOfMostRecentMessage(); + void SetTotalDownloadSize(int32_t newSize) { fTotalDownloadSize = newSize; } + + nsImapSearchResultIterator* CreateSearchResultIterator(); + void ResetSearchResultSequence() { fSearchResults->ResetSequence(); } + + // create a struct mailbox_spec from our info, used in + // libmsg c interface + already_AddRefed<nsImapMailboxSpec> CreateCurrentMailboxSpec( + const char* mailboxName = nullptr); + + // Resets the flags state. + void ResetFlagInfo(); + + // set this to false if you don't want to alert the user to server + // error messages + void SetReportingErrors(bool reportThem) { fReportingErrors = reportThem; } + bool GetReportingErrors() { return fReportingErrors; } + + eIMAPCapabilityFlags GetCapabilityFlag() { return fCapabilityFlag; } + void SetCapabilityFlag(eIMAPCapabilityFlags capability) { + fCapabilityFlag = capability; + } + bool ServerHasIMAP4Rev1Capability() { + return ((fCapabilityFlag & kIMAP4rev1Capability) != 0); + } + bool ServerHasACLCapability() { + return ((fCapabilityFlag & kACLCapability) != 0); + } + bool ServerHasNamespaceCapability() { + return ((fCapabilityFlag & kNamespaceCapability) != 0); + } + bool ServerIsNetscape3xServer() { return fServerIsNetscape3xServer; } + bool ServerHasServerInfo() { + return ((fCapabilityFlag & kXServerInfoCapability) != 0); + } + bool ServerIsAOLServer() { + return ((fCapabilityFlag & kAOLImapCapability) != 0); + } + void SetFetchingFlags(bool aFetchFlags) { fFetchingAllFlags = aFetchFlags; } + void ResetCapabilityFlag(); + + nsCString& GetMailAccountUrl() { return fMailAccountUrl; } + const char* GetXSenderInfo() { return fXSenderInfo; } + void FreeXSenderInfo() { PR_FREEIF(fXSenderInfo); } + nsCString& GetManageListsUrl() { return fManageListsUrl; } + nsCString& GetManageFiltersUrl() { return fManageFiltersUrl; } + const char* GetManageFolderUrl() { return fFolderAdminUrl; } + nsCString& GetServerID() { return fServerIdResponse; } + + // Call this when adding a pipelined command to the session + void IncrementNumberOfTaggedResponsesExpected(const char* newExpectedTag); + + // Interrupt a Fetch, without really Interrupting (through netlib) + bool GetLastFetchChunkReceived(); + void ClearLastFetchChunkReceived(); + int32_t GetNumBytesFetched(); + void ClearNumBytesFetched(); + virtual uint16_t SupportsUserFlags() { return fSupportsUserDefinedFlags; } + virtual uint16_t SettablePermanentFlags() { return fSettablePermanentFlags; } + void SetFlagState(nsIImapFlagAndUidState* state); + bool GetDownloadingHeaders(); + void SetHostSessionList(nsIImapHostSessionList* aHostSession); + char* fAuthChallenge; // the challenge returned by the server in + // response to authenticate using CRAM-MD5 or NTLM + bool fUtf8AcceptEnabled; + bool fUseModSeq; // can use mod seq for currently selected folder + uint64_t fHighestModSeq; + + protected: + virtual void flags(); + virtual void envelope_data(); + virtual void xaolenvelope_data(); + virtual void parse_address(nsAutoCString& addressLine); + virtual void internal_date(); + virtual nsresult BeginMessageDownload(const char* content_type); + + virtual void response_data(); + virtual void resp_text(); + virtual void resp_cond_state(bool isTagged); + virtual void text_mime2(); + virtual void text(); + virtual void parse_folder_flags(bool calledForFlags); + virtual void enable_data(); + virtual void language_data(); + virtual void authChallengeResponse_data(); + virtual void resp_text_code(); + virtual void response_done(); + virtual void response_tagged(); + virtual void response_fatal(); + virtual void resp_cond_bye(); + virtual void id_data(); + virtual void mailbox_data(); + virtual void numeric_mailbox_data(); + virtual void capability_data(); + virtual void xserverinfo_data(); + virtual void xmailboxinfo_data(); + virtual void namespace_data(); + virtual void myrights_data(bool unsolicited); + virtual void acl_data(); + virtual void mime_part_data(); + virtual void quota_data(); + virtual void msg_fetch(); + virtual void msg_obsolete(); + virtual void msg_fetch_headers(const char* partNum); + virtual void msg_fetch_content(bool chunk, int32_t origin, + const char* content_type); + virtual bool msg_fetch_quoted(); + virtual bool msg_fetch_literal(bool chunk, int32_t origin); + virtual void mailbox_list(bool discoveredFromLsub); + virtual void mailbox(nsImapMailboxSpec* boxSpec); + + virtual void ProcessOkCommand(const char* commandToken); + virtual void ProcessBadCommand(const char* commandToken); + virtual void PreProcessCommandToken(const char* commandToken, + const char* currentCommand); + virtual void PostProcessEndOfLine(); + + // Overridden from the nsImapGenericParser, to retrieve the next line + // from the open socket. + virtual bool GetNextLineForParser(char** nextLine) override; + // overridden to do logging + virtual void SetSyntaxError(bool error, const char* msg = nullptr) override; + + private: + bool fCurrentCommandFailed; + bool fUntaggedResponse; + bool fReportingErrors; + + bool fCurrentFolderReadOnly; + bool fCurrentLineContainedFlagInfo; + bool fFetchingAllFlags; + bool fWaitingForMoreClientInput; + // Is the server a Netscape 3.x Messaging Server? + bool fServerIsNetscape3xServer; + bool fDownloadingHeaders; + bool fCurrentCommandIsSingleMessageFetch; + bool fGotPermanentFlags; + bool fStdJunkNotJunkUseOk; + imapMessageFlagsType fSavedFlagInfo; + nsTArray<nsCString> fCustomFlags; + + uint16_t fSupportsUserDefinedFlags; + uint16_t fSettablePermanentFlags; + + int32_t fFolderUIDValidity; + int32_t fSeqNumOfFirstUnseenMsg; + int32_t fNumberOfExistingMessages; + int32_t fNumberOfRecentMessages; + uint32_t fCurrentResponseUID; + uint32_t fHighestRecordedUID; + // used to handle server that sends msg size after headers + uint32_t fReceivedHeaderOrSizeForUID; + int32_t fSizeOfMostRecentMessage; + int32_t fTotalDownloadSize; + + int32_t fStatusUnseenMessages; + int32_t fStatusRecentMessages; + uint32_t fStatusNextUID; + int32_t fStatusExistingMessages; + uint32_t fNextUID; + + int fNumberOfTaggedResponsesExpected; + + char* fCurrentCommandTag; + + nsCString fZeroLengthMessageUidString; + + char* fSelectedMailboxName; + + nsImapSearchResultSequence* fSearchResults; + + nsCOMPtr<nsIImapFlagAndUidState> + fFlagState; // NOT owned by us, it's a copy, do not destroy + + eIMAPstate fIMAPstate; + + eIMAPCapabilityFlags fCapabilityFlag; + nsCString fMailAccountUrl; + char* fNetscapeServerVersionString; + char* fXSenderInfo; /* changed per message download */ + char* fLastAlert; /* used to avoid displaying the same alert over and over */ + char* fMsgID; /* MessageID for Gmail only (X-GM-MSGID) */ + char* fThreadID; /* ThreadID for Gmail only (X-GM-THRID) */ + char* fLabels; /* Labels for Gmail only (X-GM-LABELS) [will include parens, + removed while passing to hashTable ]*/ + nsCString fManageListsUrl; + nsCString fManageFiltersUrl; + char* fFolderAdminUrl; + nsCString fServerIdResponse; // RFC + + int32_t fFetchResponseIndex; + + // used for aborting a fetch stream when we're pseudo-Interrupted + int32_t numberOfCharsInThisChunk; + int32_t charsReadSoFar; + bool fLastChunk; + + // Flags split of \r and \n between chunks in msg_fetch_literal(). + bool fNextChunkStartsWithNewline; + + // The connection object + nsImapProtocol& fServerConnection; + + RefPtr<nsIImapHostSessionList> fHostSessionList; + nsTArray<nsMsgKey> fCopyResponseKeyArray; +}; + +#endif diff --git a/comm/mailnews/imap/src/nsImapService.cpp b/comm/mailnews/imap/src/nsImapService.cpp new file mode 100644 index 0000000000..8c02d02dcd --- /dev/null +++ b/comm/mailnews/imap/src/nsImapService.cpp @@ -0,0 +1,3091 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" // precompiled header... +#include "nsImapService.h" +#include "nsImapCore.h" +#include "netCore.h" + +#include "nsImapUrl.h" +#include "nsCOMPtr.h" +#include "nsIMsgFolder.h" +#include "nsIMsgImapMailFolder.h" +#include "nsIImapIncomingServer.h" +#include "nsIImapMailFolderSink.h" +#include "nsIImapMessageSink.h" +#include "nsIImapServerSink.h" +#include "nsIImapMockChannel.h" +#include "nsImapUtils.h" +#include "nsImapNamespace.h" +#include "nsIDocShell.h" +#include "nsIProgressEventSink.h" +#include "nsIPrefBranch.h" +#include "nsIPrefService.h" +#include "nsILoadGroup.h" +#include "nsIMsgAccountManager.h" +#include "nsMsgFolderFlags.h" +#include "nsMailDirServiceDefs.h" +#include "nsIWebNavigation.h" +#include "nsImapStringBundle.h" +#include "plbase64.h" +#include "nsImapOfflineSync.h" +#include "nsIMsgHdr.h" +#include "nsMsgUtils.h" +#include "nsICacheStorage.h" +#include "nsICacheStorageService.h" +#include "nsIStreamListener.h" +#include "nsIUrlListener.h" +#include "nsNetCID.h" +#include "nsMsgI18N.h" +#include "nsIOutputStream.h" +#include "nsIInputStream.h" +#include "nsMsgLineBuffer.h" +#include "nsIMsgParseMailMsgState.h" +#include "nsIOutputStream.h" +#include "nsIDocShell.h" +#include "nsIMessengerWindowService.h" +#include "nsIWindowMediator.h" +#include "nsIPrompt.h" +#include "nsIWindowWatcher.h" +#include "nsIMsgMailSession.h" +#include "nsIStreamConverterService.h" +#include "nsIAutoSyncManager.h" +#include "nsThreadUtils.h" +#include "nsNetUtil.h" +#include "nsMsgMessageFlags.h" +#include "nsIMsgPluggableStore.h" +#include "../../base/src/MailnewsLoadContextInfo.h" +#include "nsDocShellLoadState.h" +#include "nsContentUtils.h" +#include "mozilla/LoadInfo.h" + +#define PREF_MAIL_ROOT_IMAP_REL "mail.root.imap-rel" +// old - for backward compatibility only +#define PREF_MAIL_ROOT_IMAP "mail.root.imap" + +#define NS_IMAPURL_CID \ + { \ + 0x21a89611, 0xdc0d, 0x11d2, { \ + 0x80, 0x6c, 0x0, 0x60, 0x8, 0x12, 0x8c, 0x4e \ + } \ + } +static NS_DEFINE_CID(kImapUrlCID, NS_IMAPURL_CID); + +#define NS_IMAPMOCKCHANNEL_CID \ + { \ + 0x4eca51df, 0x6734, 0x11d3, { \ + 0x98, 0x9a, 0x0, 0x10, 0x83, 0x1, 0xe, 0x9b \ + } \ + } +static NS_DEFINE_CID(kCImapMockChannel, NS_IMAPMOCKCHANNEL_CID); + +static const char sequenceString[] = "SEQUENCE"; +static const char uidString[] = "UID"; + +static bool gInitialized = false; + +NS_IMPL_ISUPPORTS(nsImapService, nsIImapService, nsIMsgMessageService, + nsIProtocolHandler, nsIMsgProtocolInfo, + nsIMsgMessageFetchPartService, nsIContentHandler) + +nsImapService::nsImapService() { + if (!gInitialized) { + nsresult rv; + + nsCOMPtr<nsIIOService> ioServ = do_GetIOService(); + ioServ->RegisterProtocolHandler( + "imap"_ns, this, + nsIProtocolHandler::URI_NORELATIVE | + nsIProtocolHandler::URI_FORBIDS_AUTOMATIC_DOCUMENT_REPLACEMENT | + nsIProtocolHandler::URI_DANGEROUS_TO_LOAD | + nsIProtocolHandler::ALLOWS_PROXY | + nsIProtocolHandler::URI_FORBIDS_COOKIE_ACCESS | + nsIProtocolHandler::ORIGIN_IS_FULL_SPEC, + nsIImapUrl::DEFAULT_IMAP_PORT); + + // initialize auto-sync service + nsCOMPtr<nsIAutoSyncManager> autoSyncMgr = + do_GetService(NS_AUTOSYNCMANAGER_CONTRACTID, &rv); + if (NS_SUCCEEDED(rv) && autoSyncMgr) { + // auto-sync manager initialization goes here + // assign new strategy objects here... + } + NS_ASSERTION(autoSyncMgr != nullptr, + "*** Cannot initialize nsAutoSyncManager service."); + + gInitialized = true; + } +} + +nsImapService::~nsImapService() {} + +char nsImapService::GetHierarchyDelimiter(nsIMsgFolder* aMsgFolder) { + char delimiter = '/'; + if (aMsgFolder) { + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(aMsgFolder); + if (imapFolder) imapFolder->GetHierarchyDelimiter(&delimiter); + } + return delimiter; +} + +// N.B., this returns an escaped folder name, appropriate for putting in a url. +nsresult nsImapService::GetFolderName(nsIMsgFolder* aImapFolder, + nsACString& aFolderName) { + nsresult rv; + nsCOMPtr<nsIMsgImapMailFolder> aFolder(do_QueryInterface(aImapFolder, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString onlineName; + // Online name is in MUTF-7 or UTF-8. + rv = aFolder->GetOnlineName(onlineName); + NS_ENSURE_SUCCESS(rv, rv); + if (onlineName.IsEmpty()) { + nsCString uri; + rv = aImapFolder->GetURI(uri); + NS_ENSURE_SUCCESS(rv, rv); + nsCString hostname; + rv = aImapFolder->GetHostname(hostname); + NS_ENSURE_SUCCESS(rv, rv); + rv = nsImapURI2FullName(kImapRootURI, hostname.get(), uri.get(), + getter_Copies(onlineName)); + } + // if the hierarchy delimiter is not '/', then we want to escape slashes; + // otherwise, we do want to escape slashes. + // we want to escape slashes and '^' first, otherwise, nsEscape will lose them + bool escapeSlashes = (GetHierarchyDelimiter(aImapFolder) != '/'); + if (escapeSlashes && !onlineName.IsEmpty()) { + char* escapedOnlineName; + rv = nsImapUrl::EscapeSlashes(onlineName.get(), &escapedOnlineName); + if (NS_SUCCEEDED(rv)) onlineName.Adopt(escapedOnlineName); + } + // need to escape everything else + MsgEscapeString(onlineName, nsINetUtil::ESCAPE_URL_PATH, aFolderName); + return rv; +} + +NS_IMETHODIMP nsImapService::SelectFolder(nsIMsgFolder* aImapMailFolder, + nsIUrlListener* aUrlListener, + nsIMsgWindow* aMsgWindow, + nsIURI** aURL) { + NS_ENSURE_ARG_POINTER(aImapMailFolder); + + if (WeAreOffline()) return NS_MSG_ERROR_OFFLINE; + + bool canOpenThisFolder = true; + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = + do_QueryInterface(aImapMailFolder); + if (imapFolder) imapFolder->GetCanOpenFolder(&canOpenThisFolder); + + if (!canOpenThisFolder) return NS_OK; + + nsresult rv; + nsCOMPtr<nsIImapUrl> imapUrl; + nsAutoCString urlSpec; + char hierarchyDelimiter = GetHierarchyDelimiter(aImapMailFolder); + rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl), + aImapMailFolder, aUrlListener, urlSpec, + hierarchyDelimiter); + + if (NS_SUCCEEDED(rv) && imapUrl) { + // nsImapUrl::SetSpec() will set the imap action properly + rv = imapUrl->SetImapAction(nsIImapUrl::nsImapSelectFolder); + + nsCOMPtr<nsIMsgMailNewsUrl> mailNewsUrl = do_QueryInterface(imapUrl); + // if no msg window, we won't put up error messages (this is almost + // certainly a biff-inspired get new msgs) + if (!aMsgWindow) mailNewsUrl->SetSuppressErrorMsgs(true); + mailNewsUrl->SetMsgWindow(aMsgWindow); + mailNewsUrl->SetUpdatingFolder(true); + rv = SetImapUrlSink(aImapMailFolder, imapUrl); + + if (NS_SUCCEEDED(rv)) { + nsAutoCString folderName; + GetFolderName(aImapMailFolder, folderName); + urlSpec.AppendLiteral("/select>"); + urlSpec.Append(hierarchyDelimiter); + urlSpec.Append(folderName); + rv = mailNewsUrl->SetSpecInternal(urlSpec); + if (NS_SUCCEEDED(rv)) + rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, aURL); + } + } // if we have a url to run.... + + return rv; +} + +// lite select, used to verify UIDVALIDITY while going on/offline +NS_IMETHODIMP nsImapService::LiteSelectFolder(nsIMsgFolder* aImapMailFolder, + nsIUrlListener* aUrlListener, + nsIMsgWindow* aMsgWindow, + nsIURI** aURL) { + NS_ENSURE_ARG_POINTER(aImapMailFolder); + + return FolderCommand(aImapMailFolder, aUrlListener, "/liteselect>", + nsIImapUrl::nsImapLiteSelectFolder, aMsgWindow, aURL); +} + +NS_IMETHODIMP nsImapService::GetUrlForUri(const nsACString& aMessageURI, + nsIMsgWindow* aMsgWindow, + nsIURI** aURL) { + nsAutoCString messageURI(aMessageURI); + + if (messageURI.Find("&type=application/x-message-display"_ns) != kNotFound) + return NS_NewURI(aURL, aMessageURI); + + nsCOMPtr<nsIMsgFolder> folder; + nsAutoCString msgKey; + nsresult rv = DecomposeImapURI(messageURI, getter_AddRefs(folder), msgKey); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIImapUrl> imapUrl; + nsAutoCString urlSpec; + char hierarchyDelimiter = GetHierarchyDelimiter(folder); + rv = CreateStartOfImapUrl(messageURI, getter_AddRefs(imapUrl), folder, + nullptr, urlSpec, hierarchyDelimiter); + NS_ENSURE_SUCCESS(rv, rv); + rv = SetImapUrlSink(folder, imapUrl); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(imapUrl); + bool useLocalCache = false; + folder->HasMsgOffline(strtoul(msgKey.get(), nullptr, 10), &useLocalCache); + mailnewsUrl->SetMsgIsInLocalCache(useLocalCache); + + nsCOMPtr<nsIURI> url = do_QueryInterface(imapUrl); + rv = url->GetSpec(urlSpec); + NS_ENSURE_SUCCESS(rv, rv); + urlSpec.AppendLiteral("fetch>UID>"); + urlSpec.Append(hierarchyDelimiter); + + nsAutoCString folderName; + GetFolderName(folder, folderName); + urlSpec.Append(folderName); + urlSpec.Append('>'); + urlSpec.Append(msgKey); + rv = mailnewsUrl->SetSpecInternal(urlSpec); + imapUrl->QueryInterface(NS_GET_IID(nsIURI), (void**)aURL); + } + + return rv; +} + +NS_IMETHODIMP nsImapService::FetchMimePart( + nsIURI* aURI, const nsACString& aMessageURI, nsISupports* aDisplayConsumer, + nsIMsgWindow* aMsgWindow, nsIUrlListener* aUrlListener, nsIURI** aURL) { + nsCOMPtr<nsIMsgFolder> folder; + nsAutoCString messageURI(aMessageURI); + nsAutoCString msgKey; + nsAutoCString mimePart; + nsAutoCString folderURI; + nsMsgKey key; + + nsresult rv = DecomposeImapURI(messageURI, getter_AddRefs(folder), msgKey); + NS_ENSURE_SUCCESS(rv, rv); + rv = nsParseImapMessageURI(aMessageURI, folderURI, &key, + getter_Copies(mimePart)); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIImapMessageSink> imapMessageSink( + do_QueryInterface(folder, &rv)); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIImapUrl> imapUrl = do_QueryInterface(aURI); + nsCOMPtr<nsIMsgMailNewsUrl> msgurl(do_QueryInterface(aURI, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + msgurl->SetMsgWindow(aMsgWindow); + msgurl->RegisterListener(aUrlListener); + + if (!mimePart.IsEmpty()) { + return FetchMimePart(imapUrl, nsIImapUrl::nsImapMsgFetch, folder, + imapMessageSink, aURL, aDisplayConsumer, msgKey, + mimePart); + } + } + } + return rv; +} + +NS_IMETHODIMP nsImapService::LoadMessage(const nsACString& aMessageURI, + nsISupports* aDisplayConsumer, + nsIMsgWindow* aMsgWindow, + nsIUrlListener* aUrlListener, + bool aAutodetectCharset) { + nsresult rv; + + nsCOMPtr<nsIMsgFolder> folder; + nsAutoCString msgKey; + nsAutoCString mimePart; + nsAutoCString folderURI; + nsMsgKey key; + nsAutoCString messageURI(aMessageURI); + + int32_t typeIndex = messageURI.Find("&type=application/x-message-display"); + if (typeIndex != kNotFound) { + // This happens with forward inline of a message/rfc822 attachment opened in + // a standalone msg window. + // So, just cut to the chase and call AsyncOpen on a channel. + nsCOMPtr<nsIURI> uri; + messageURI.Cut(typeIndex, + sizeof("&type=application/x-message-display") - 1); + rv = NS_NewURI(getter_AddRefs(uri), messageURI.get()); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIStreamListener> aStreamListener = + do_QueryInterface(aDisplayConsumer, &rv); + if (NS_SUCCEEDED(rv) && aStreamListener) { + nsCOMPtr<nsIChannel> aChannel; + nsCOMPtr<nsILoadGroup> aLoadGroup; + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(uri, &rv); + if (NS_SUCCEEDED(rv) && mailnewsUrl) + mailnewsUrl->GetLoadGroup(getter_AddRefs(aLoadGroup)); + + nsCOMPtr<nsILoadInfo> loadInfo = new mozilla::net::LoadInfo( + nsContentUtils::GetSystemPrincipal(), nullptr, nullptr, + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_OTHER); + rv = NewChannel(uri, loadInfo, getter_AddRefs(aChannel)); + NS_ENSURE_SUCCESS(rv, rv); + + // now try to open the channel passing in our display consumer as the + // listener + rv = aChannel->AsyncOpen(aStreamListener); + return rv; + } + } + + rv = DecomposeImapURI(messageURI, getter_AddRefs(folder), msgKey); + NS_ENSURE_SUCCESS(rv, rv); + if (msgKey.IsEmpty()) return NS_MSG_MESSAGE_NOT_FOUND; + + rv = nsParseImapMessageURI(aMessageURI, folderURI, &key, + getter_Copies(mimePart)); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIImapMessageSink> imapMessageSink( + do_QueryInterface(folder, &rv)); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIImapUrl> imapUrl; + nsAutoCString urlSpec; + char hierarchyDelimiter = GetHierarchyDelimiter(folder); + rv = CreateStartOfImapUrl(messageURI, getter_AddRefs(imapUrl), folder, + aUrlListener, urlSpec, hierarchyDelimiter); + NS_ENSURE_SUCCESS(rv, rv); + if (!mimePart.IsEmpty()) { + nsresult rv; + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl = do_QueryInterface(imapUrl); + + rv = AddImapFetchToUrl(mailnewsurl, folder, msgKey + mimePart, + EmptyCString()); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIURI> dummyURI; + return FetchMimePart(imapUrl, nsIImapUrl::nsImapMsgFetch, folder, + imapMessageSink, getter_AddRefs(dummyURI), + aDisplayConsumer, msgKey, mimePart); + } + + nsCOMPtr<nsIMsgMailNewsUrl> msgurl(do_QueryInterface(imapUrl)); + nsCOMPtr<nsIMsgI18NUrl> i18nurl(do_QueryInterface(imapUrl)); + i18nurl->SetAutodetectCharset(aAutodetectCharset); + + bool shouldStoreMsgOffline = false; + bool hasMsgOffline = false; + + msgurl->SetMsgWindow(aMsgWindow); + + if (folder) { + folder->ShouldStoreMsgOffline(key, &shouldStoreMsgOffline); + folder->HasMsgOffline(key, &hasMsgOffline); + } + imapUrl->SetStoreResultsOffline(shouldStoreMsgOffline); + + if (hasMsgOffline) msgurl->SetMsgIsInLocalCache(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 the \Seen + // flag on the msg, but a FETCH (BODY.PEEK[]) won't. + bool forcePeek = false; + if (NS_SUCCEEDED(rv) && prefBranch) { + nsAutoCString uriStr(aMessageURI); + int32_t dontMarkAsReadPos = uriStr.Find("&markRead=false"); + bool markReadAuto = true; + prefBranch->GetBoolPref("mailnews.mark_message_read.auto", + &markReadAuto); + bool markReadDelay = false; + prefBranch->GetBoolPref("mailnews.mark_message_read.delay", + &markReadDelay); + forcePeek = (!markReadAuto || markReadDelay || + (dontMarkAsReadPos != kNotFound)); + } + + if (!forcePeek) { + // If we're loading a message in an inactive docShell, don't let it + // be marked as read immediately. + nsCOMPtr<nsIDocShell> docShell = + do_QueryInterface(aDisplayConsumer, &rv); + if (NS_SUCCEEDED(rv) && docShell) { + auto* bc = docShell->GetBrowsingContext(); + forcePeek = !bc->IsActive(); + } + } + + nsCOMPtr<nsIURI> dummyURI; + rv = FetchMessage(imapUrl, + forcePeek ? nsIImapUrl::nsImapMsgFetchPeek + : nsIImapUrl::nsImapMsgFetch, + folder, imapMessageSink, aMsgWindow, aDisplayConsumer, + msgKey, false, getter_AddRefs(dummyURI)); + } + } + return rv; +} + +nsresult nsImapService::FetchMimePart( + nsIImapUrl* aImapUrl, nsImapAction aImapAction, + nsIMsgFolder* aImapMailFolder, nsIImapMessageSink* aImapMessage, + 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. + nsAutoCString urlSpec; + 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()) { + bool useLocalCache = false; + aImapMailFolder->HasMsgOffline( + strtoul(PromiseFlatCString(messageIdentifierList).get(), nullptr, 10), + &useLocalCache); + msgurl->SetMsgIsInLocalCache(useLocalCache); + } + rv = aImapUrl->SetImapMessageSink(aImapMessage); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIURI> url = do_QueryInterface(aImapUrl); + if (aURL) NS_IF_ADDREF(*aURL = url); + + rv = url->GetSpec(urlSpec); + NS_ENSURE_SUCCESS(rv, rv); + + rv = msgurl->SetSpecInternal(urlSpec); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aImapUrl->SetImapAction(actionToUse /* nsIImapUrl::nsImapMsgFetch */); + if (aImapMailFolder && aDisplayConsumer) { + nsCOMPtr<nsIMsgIncomingServer> aMsgIncomingServer; + rv = aImapMailFolder->GetServer(getter_AddRefs(aMsgIncomingServer)); + if (NS_SUCCEEDED(rv) && aMsgIncomingServer) { + bool interrupted; + nsCOMPtr<nsIImapIncomingServer> aImapServer( + do_QueryInterface(aMsgIncomingServer, &rv)); + if (NS_SUCCEEDED(rv) && aImapServer) + aImapServer->PseudoInterruptMsgLoad(aImapMailFolder, nullptr, + &interrupted); + } + } + // if the display consumer is a docshell, then we should run the url in the + // docshell. otherwise, it should be a stream listener....so open a channel + // using AsyncRead and the provided stream listener.... + + nsCOMPtr<nsIDocShell> docShell(do_QueryInterface(aDisplayConsumer, &rv)); + if (NS_SUCCEEDED(rv) && docShell) { + // DIRTY LITTLE HACK --> if we are opening an attachment we want the + // docshell to treat this load as if it were a user click event. Then the + // dispatching stuff will be much happier. + RefPtr<nsDocShellLoadState> loadState = new nsDocShellLoadState(url); + loadState->SetLoadFlags(aImapAction == nsImapUrl::nsImapOpenMimePart + ? nsIWebNavigation::LOAD_FLAGS_IS_LINK + : nsIWebNavigation::LOAD_FLAGS_NONE); + if (aImapAction == nsImapUrl::nsImapOpenMimePart) + loadState->SetLoadType(LOAD_LINK); + loadState->SetFirstParty(false); + loadState->SetTriggeringPrincipal(nsContentUtils::GetSystemPrincipal()); + rv = docShell->LoadURI(loadState, false); + } else { + nsCOMPtr<nsIStreamListener> aStreamListener = + do_QueryInterface(aDisplayConsumer, &rv); + if (NS_SUCCEEDED(rv) && aStreamListener) { + nsCOMPtr<nsIChannel> aChannel; + nsCOMPtr<nsILoadGroup> loadGroup; + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = + do_QueryInterface(aImapUrl, &rv); + if (NS_SUCCEEDED(rv) && mailnewsUrl) + mailnewsUrl->GetLoadGroup(getter_AddRefs(loadGroup)); + + nsCOMPtr<nsILoadInfo> loadInfo = new mozilla::net::LoadInfo( + nsContentUtils::GetSystemPrincipal(), nullptr, nullptr, + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_OTHER); + rv = NewChannel(url, loadInfo, getter_AddRefs(aChannel)); + NS_ENSURE_SUCCESS(rv, rv); + + // we need a load group to hold onto the channel. When the request is + // finished, it'll get removed from the load group, and the channel will + // go away, which will free the load group. + if (!loadGroup) loadGroup = do_CreateInstance(NS_LOADGROUP_CONTRACTID); + + aChannel->SetLoadGroup(loadGroup); + + // now try to open the channel passing in our display consumer as the + // listener + rv = aChannel->AsyncOpen(aStreamListener); + } else // do what we used to do before + { + // I'd like to get rid of this code as I believe that we always get a + // docshell or stream listener passed into us in this method but i'm not + // sure yet... I'm going to use an assert for now to figure out if this + // is ever getting called +#if defined(DEBUG_mscott) || defined(DEBUG_bienvenu) + NS_ERROR("oops...someone still is reaching this part of the code"); +#endif + rv = GetImapConnectionAndLoadUrl(aImapUrl, aDisplayConsumer, aURL); + } + } + } + return rv; +} + +NS_IMETHODIMP nsImapService::CopyMessage(const nsACString& aSrcMailboxURI, + nsIStreamListener* aMailboxCopy, + bool moveMessage, + nsIUrlListener* aUrlListener, + nsIMsgWindow* aMsgWindow) { + NS_ENSURE_ARG_POINTER(aMailboxCopy); + + nsresult rv; + nsCOMPtr<nsIMsgFolder> folder; + nsAutoCString msgKey; + rv = DecomposeImapURI(aSrcMailboxURI, getter_AddRefs(folder), msgKey); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIImapMessageSink> imapMessageSink( + do_QueryInterface(folder, &rv)); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIImapUrl> imapUrl; + nsAutoCString urlSpec; + char hierarchyDelimiter = GetHierarchyDelimiter(folder); + bool hasMsgOffline = false; + nsMsgKey key = strtoul(msgKey.get(), nullptr, 10); + + rv = CreateStartOfImapUrl(aSrcMailboxURI, getter_AddRefs(imapUrl), folder, + aUrlListener, urlSpec, hierarchyDelimiter); + if (folder) { + nsCOMPtr<nsIMsgMailNewsUrl> msgurl(do_QueryInterface(imapUrl)); + folder->HasMsgOffline(key, &hasMsgOffline); + if (msgurl) msgurl->SetMsgIsInLocalCache(hasMsgOffline); + } + // now try to download the message + nsImapAction imapAction = nsIImapUrl::nsImapOnlineToOfflineCopy; + if (moveMessage) imapAction = nsIImapUrl::nsImapOnlineToOfflineMove; + nsCOMPtr<nsIURI> dummyURI; + rv = + FetchMessage(imapUrl, imapAction, folder, imapMessageSink, aMsgWindow, + aMailboxCopy, msgKey, false, getter_AddRefs(dummyURI)); + } // if we got an imap message sink + } // if we decomposed the imap message + return rv; +} + +NS_IMETHODIMP nsImapService::CopyMessages( + const nsTArray<nsMsgKey>& aKeys, nsIMsgFolder* srcFolder, + nsIStreamListener* aMailboxCopy, bool moveMessage, + nsIUrlListener* aUrlListener, nsIMsgWindow* aMsgWindow, nsIURI** aURL) { + NS_ENSURE_ARG_POINTER(aMailboxCopy); + NS_ENSURE_TRUE(!aKeys.IsEmpty(), NS_ERROR_INVALID_ARG); + + nsresult rv; + nsCOMPtr<nsIMsgFolder> folder = srcFolder; + nsCOMPtr<nsIImapMessageSink> imapMessageSink(do_QueryInterface(folder, &rv)); + if (NS_SUCCEEDED(rv)) { + // we generate the uri for the first message so that way on down the line, + // GetMessage in nsCopyMessageStreamListener will get an unescaped + // username and be able to find the msg hdr. See bug 259656 for details + nsCString uri; + srcFolder->GenerateMessageURI(aKeys[0], uri); + + nsCString messageIds; + // TODO: AllocateImapUidString() maxes out at 950 keys or so... it + // updates the numKeys passed in, but here the resulting value is + // ignored. Does this need sorting out? + uint32_t numKeys = aKeys.Length(); + AllocateImapUidString(aKeys.Elements(), numKeys, nullptr, messageIds); + nsCOMPtr<nsIImapUrl> imapUrl; + nsAutoCString urlSpec; + char hierarchyDelimiter = GetHierarchyDelimiter(folder); + rv = CreateStartOfImapUrl(uri, getter_AddRefs(imapUrl), folder, + aUrlListener, urlSpec, hierarchyDelimiter); + nsImapAction action; + if (moveMessage) // don't use ?: syntax here, it seems to break the Mac. + action = nsIImapUrl::nsImapOnlineToOfflineMove; + else + action = nsIImapUrl::nsImapOnlineToOfflineCopy; + imapUrl->SetCopyState(aMailboxCopy); + // now try to display the message + rv = FetchMessage(imapUrl, action, folder, imapMessageSink, aMsgWindow, + aMailboxCopy, messageIds, false, aURL); + // ### end of copy operation should know how to do the delete.if this is a + // move + + } // if we got an imap message sink + return rv; +} + +NS_IMETHODIMP nsImapService::Search(nsIMsgSearchSession* aSearchSession, + nsIMsgWindow* aMsgWindow, + nsIMsgFolder* aMsgFolder, + const nsACString& aSearchUri) { + NS_ENSURE_ARG_POINTER(aMsgFolder); + nsresult rv; + + nsCOMPtr<nsIImapUrl> imapUrl; + nsCOMPtr<nsIUrlListener> urlListener = do_QueryInterface(aSearchSession, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString urlSpec; + char hierarchyDelimiter = GetHierarchyDelimiter(aMsgFolder); + rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl), aMsgFolder, + urlListener, urlSpec, hierarchyDelimiter); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgMailNewsUrl> msgurl(do_QueryInterface(imapUrl)); + + msgurl->SetMsgWindow(aMsgWindow); + msgurl->SetSearchSession(aSearchSession); + rv = SetImapUrlSink(aMsgFolder, imapUrl); + + if (NS_SUCCEEDED(rv)) { + nsCString folderName; + GetFolderName(aMsgFolder, folderName); + + nsCOMPtr<nsIMsgMailNewsUrl> mailNewsUrl = do_QueryInterface(imapUrl); + if (!aMsgWindow) mailNewsUrl->SetSuppressErrorMsgs(true); + + urlSpec.AppendLiteral("/search>UID>"); + urlSpec.Append(hierarchyDelimiter); + urlSpec.Append(folderName); + urlSpec.Append('>'); + // escape aSearchUri so that IMAP special characters (i.e. '\') + // won't be replaced with '/' in NECKO. + // it will be unescaped in nsImapUrl::ParseUrl(). + nsCString escapedSearchUri; + + MsgEscapeString(aSearchUri, nsINetUtil::ESCAPE_XALPHAS, escapedSearchUri); + urlSpec.Append(escapedSearchUri); + rv = mailNewsUrl->SetSpecInternal(urlSpec); + if (NS_SUCCEEDED(rv)) + rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, nullptr); + } + return rv; +} + +// just a helper method to break down imap message URIs.... +nsresult nsImapService::DecomposeImapURI(const nsACString& aMessageURI, + nsIMsgFolder** aFolder, + nsACString& aMsgKey) { + nsMsgKey msgKey; + nsresult rv = DecomposeImapURI(aMessageURI, aFolder, &msgKey); + NS_ENSURE_SUCCESS(rv, rv); + + if (msgKey) { + nsAutoCString messageIdString; + messageIdString.AppendInt(msgKey); + aMsgKey = messageIdString; + } + + return rv; +} + +// just a helper method to break down imap message URIs.... +nsresult nsImapService::DecomposeImapURI(const nsACString& aMessageURI, + nsIMsgFolder** aFolder, + nsMsgKey* aMsgKey) { + NS_ENSURE_ARG_POINTER(aFolder); + NS_ENSURE_ARG_POINTER(aMsgKey); + + nsAutoCString folderURI; + nsresult rv = nsParseImapMessageURI(aMessageURI, folderURI, aMsgKey, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgFolder> folder; + rv = GetOrCreateFolder(folderURI, aFolder); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP nsImapService::SaveMessageToDisk( + const nsACString& aMessageURI, nsIFile* aFile, bool aAddDummyEnvelope, + nsIUrlListener* aUrlListener, nsIURI** aURL, bool canonicalLineEnding, + nsIMsgWindow* aMsgWindow) { + nsCOMPtr<nsIMsgFolder> folder; + nsCOMPtr<nsIImapUrl> imapUrl; + nsAutoCString msgKey; + + nsresult rv = DecomposeImapURI(aMessageURI, getter_AddRefs(folder), msgKey); + NS_ENSURE_SUCCESS(rv, rv); + + bool hasMsgOffline = false; + + if (folder) + folder->HasMsgOffline(strtoul(msgKey.get(), nullptr, 10), &hasMsgOffline); + + nsAutoCString urlSpec; + char hierarchyDelimiter = GetHierarchyDelimiter(folder); + rv = CreateStartOfImapUrl(aMessageURI, getter_AddRefs(imapUrl), folder, + aUrlListener, urlSpec, hierarchyDelimiter); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIImapMessageSink> imapMessageSink( + do_QueryInterface(folder, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgMessageUrl> msgUrl = do_QueryInterface(imapUrl, &rv); + NS_ENSURE_SUCCESS(rv, rv); + msgUrl->SetMessageFile(aFile); + msgUrl->SetAddDummyEnvelope(aAddDummyEnvelope); + msgUrl->SetCanonicalLineEnding(canonicalLineEnding); + + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(msgUrl); + if (mailnewsUrl) mailnewsUrl->SetMsgIsInLocalCache(hasMsgOffline); + + nsCOMPtr<nsIStreamListener> saveAsListener; + mailnewsUrl->GetSaveAsListener(aAddDummyEnvelope, aFile, + getter_AddRefs(saveAsListener)); + + return FetchMessage(imapUrl, nsIImapUrl::nsImapSaveMessageToDisk, folder, + imapMessageSink, aMsgWindow, saveAsListener, msgKey, + false, aURL); + } + return rv; +} + +/* fetching RFC822 messages */ +/* imap4://HOST>fetch><UID>>MAILBOXPATH>x */ +/* 'x' is the message UID */ +/* will set the 'SEEN' flag */ +NS_IMETHODIMP nsImapService::AddImapFetchToUrl( + nsIMsgMailNewsUrl* aUrl, nsIMsgFolder* aImapMailFolder, + const nsACString& aMessageIdentifierList, + const nsACString& aAdditionalHeader) { + NS_ENSURE_ARG_POINTER(aUrl); + + nsAutoCString urlSpec; + nsresult rv = aUrl->GetSpec(urlSpec); + NS_ENSURE_SUCCESS(rv, rv); + + char hierarchyDelimiter = GetHierarchyDelimiter(aImapMailFolder); + + urlSpec.AppendLiteral("fetch>UID>"); + urlSpec.Append(hierarchyDelimiter); + + nsAutoCString folderName; + GetFolderName(aImapMailFolder, folderName); + urlSpec.Append(folderName); + + urlSpec.Append('>'); + urlSpec.Append(aMessageIdentifierList); + + if (!aAdditionalHeader.IsEmpty()) { + urlSpec.AppendLiteral("?header="); + urlSpec.Append(aAdditionalHeader); + } + + return aUrl->SetSpecInternal(urlSpec); +} + +NS_IMETHODIMP nsImapService::FetchMessage( + nsIImapUrl* aImapUrl, nsImapAction aImapAction, + nsIMsgFolder* aImapMailFolder, nsIImapMessageSink* aImapMessage, + nsIMsgWindow* aMsgWindow, nsISupports* aDisplayConsumer, + const nsACString& messageIdentifierList, bool aConvertDataToText, + nsIURI** aURL) { + NS_ENSURE_ARG_POINTER(aImapUrl); + NS_ENSURE_ARG_POINTER(aImapMailFolder); + NS_ENSURE_ARG_POINTER(aImapMessage); + + nsresult rv; + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl = do_QueryInterface(aImapUrl); + + rv = AddImapFetchToUrl(mailnewsurl, aImapMailFolder, messageIdentifierList, + ""_ns); + NS_ENSURE_SUCCESS(rv, rv); + + if (WeAreOffline()) { + bool msgIsInCache = false; + nsCOMPtr<nsIMsgMailNewsUrl> msgUrl(do_QueryInterface(aImapUrl)); + msgUrl->GetMsgIsInLocalCache(&msgIsInCache); + if (!msgIsInCache) + IsMsgInMemCache(mailnewsurl, aImapMailFolder, &msgIsInCache); + + // Display the "offline" message if we didn't find it in the memory cache + // either + if (!msgIsInCache) { + return NS_ERROR_OFFLINE; + } + } + + if (aURL) mailnewsurl.forget(aURL); + + return GetMessageFromUrl(aImapUrl, aImapAction, aImapMailFolder, aImapMessage, + aMsgWindow, aDisplayConsumer, aConvertDataToText, + aURL); +} + +nsresult nsImapService::GetMessageFromUrl( + nsIImapUrl* aImapUrl, nsImapAction aImapAction, + nsIMsgFolder* aImapMailFolder, nsIImapMessageSink* aImapMessage, + nsIMsgWindow* aMsgWindow, nsISupports* aDisplayConsumer, + bool aConvertDataToText, nsIURI** aURL) { + nsresult rv = SetImapUrlSink(aImapMailFolder, aImapUrl); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aImapUrl->SetImapMessageSink(aImapMessage); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aImapUrl->SetImapAction(aImapAction); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIURI> url(do_QueryInterface(aImapUrl)); + + // if the display consumer is a docshell, then we should run the url in the + // docshell. otherwise, it should be a stream listener....so open a channel + // using AsyncRead and the provided stream listener.... + + nsCOMPtr<nsIDocShell> docShell(do_QueryInterface(aDisplayConsumer, &rv)); + if (aImapMailFolder && docShell) { + nsCOMPtr<nsIMsgIncomingServer> aMsgIncomingServer; + rv = aImapMailFolder->GetServer(getter_AddRefs(aMsgIncomingServer)); + if (NS_SUCCEEDED(rv) && aMsgIncomingServer) { + bool interrupted; + nsCOMPtr<nsIImapIncomingServer> aImapServer( + do_QueryInterface(aMsgIncomingServer, &rv)); + if (NS_SUCCEEDED(rv) && aImapServer) + aImapServer->PseudoInterruptMsgLoad(aImapMailFolder, aMsgWindow, + &interrupted); + } + } + if (NS_SUCCEEDED(rv) && docShell) { + NS_ASSERTION(!aConvertDataToText, + "can't convert to text when using docshell"); + RefPtr<nsDocShellLoadState> loadState = new nsDocShellLoadState(url); + loadState->SetLoadFlags(nsIWebNavigation::LOAD_FLAGS_NONE); + loadState->SetFirstParty(false); + loadState->SetTriggeringPrincipal(nsContentUtils::GetSystemPrincipal()); + rv = docShell->LoadURI(loadState, false); + } else { + nsCOMPtr<nsIStreamListener> streamListener = + do_QueryInterface(aDisplayConsumer, &rv); + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(aImapUrl, &rv); + if (aMsgWindow && mailnewsUrl) mailnewsUrl->SetMsgWindow(aMsgWindow); + if (NS_SUCCEEDED(rv) && streamListener) { + nsCOMPtr<nsIChannel> channel; + nsCOMPtr<nsILoadGroup> loadGroup; + if (NS_SUCCEEDED(rv) && mailnewsUrl) + mailnewsUrl->GetLoadGroup(getter_AddRefs(loadGroup)); + + nsCOMPtr<nsILoadInfo> loadInfo = new mozilla::net::LoadInfo( + nsContentUtils::GetSystemPrincipal(), nullptr, nullptr, + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_OTHER); + rv = NewChannel(url, loadInfo, getter_AddRefs(channel)); + NS_ENSURE_SUCCESS(rv, rv); + + // we need a load group to hold onto the channel. When the request is + // finished, it'll get removed from the load group, and the channel will + // go away, which will free the load group. + if (!loadGroup) loadGroup = do_CreateInstance(NS_LOADGROUP_CONTRACTID); + + rv = channel->SetLoadGroup(loadGroup); + NS_ENSURE_SUCCESS(rv, rv); + + if (aConvertDataToText) { + nsCOMPtr<nsIStreamListener> conversionListener; + nsCOMPtr<nsIStreamConverterService> streamConverter = + do_GetService("@mozilla.org/streamConverters;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = streamConverter->AsyncConvertData( + "message/rfc822", "*/*", streamListener, channel, + getter_AddRefs(conversionListener)); + NS_ENSURE_SUCCESS(rv, rv); + streamListener = conversionListener; // this is our new listener. + } + + // now try to open the channel passing in our display consumer as the + // listener + rv = channel->AsyncOpen(streamListener); + } else // do what we used to do before + { + // I'd like to get rid of this code as I believe that we always get a + // docshell or stream listener passed into us in this method but i'm not + // sure yet... I'm going to use an assert for now to figure out if this is + // ever getting called +#if defined(DEBUG_mscott) || defined(DEBUG_bienvenu) + NS_ERROR("oops...someone still is reaching this part of the code"); +#endif + rv = GetImapConnectionAndLoadUrl(aImapUrl, aDisplayConsumer, aURL); + } + } + return rv; +} + +// this method streams a message to the passed in consumer, with an optional +// stream converter and additional header (e.g., "header=filter") +NS_IMETHODIMP nsImapService::StreamMessage( + const nsACString& aMessageURI, nsISupports* aConsumer, + nsIMsgWindow* aMsgWindow, nsIUrlListener* aUrlListener, bool aConvertData, + const nsACString& aAdditionalHeader, bool aLocalOnly, nsIURI** aURL) { + nsCOMPtr<nsIMsgFolder> folder; + nsAutoCString msgKey; + nsAutoCString mimePart; + nsAutoCString folderURI; + nsMsgKey key; + nsAutoCString messageURI(aMessageURI); + + int32_t typeIndex = messageURI.Find("&type=application/x-message-display"); + if (typeIndex != kNotFound) { + // This happens with forward inline of a message/rfc822 attachment opened in + // a standalone msg window. + // So, just cut to the chase and call AsyncOpen on a channel. + nsCOMPtr<nsIURI> uri; + messageURI.Cut(typeIndex, + sizeof("&type=application/x-message-display") - 1); + nsresult rv = NS_NewURI(getter_AddRefs(uri), messageURI.get()); + NS_ENSURE_SUCCESS(rv, rv); + if (aURL) NS_IF_ADDREF(*aURL = uri); + nsCOMPtr<nsIStreamListener> aStreamListener = + do_QueryInterface(aConsumer, &rv); + if (NS_SUCCEEDED(rv) && aStreamListener) { + nsCOMPtr<nsIChannel> aChannel; + nsCOMPtr<nsILoadGroup> aLoadGroup; + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(uri, &rv); + if (NS_SUCCEEDED(rv) && mailnewsUrl) + mailnewsUrl->GetLoadGroup(getter_AddRefs(aLoadGroup)); + + nsCOMPtr<nsILoadInfo> loadInfo = new mozilla::net::LoadInfo( + nsContentUtils::GetSystemPrincipal(), nullptr, nullptr, + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_OTHER); + rv = NewChannel(uri, loadInfo, getter_AddRefs(aChannel)); + NS_ENSURE_SUCCESS(rv, rv); + + // now try to open the channel passing in our display consumer as the + // listener + rv = aChannel->AsyncOpen(aStreamListener); + return rv; + } + } + + nsresult rv = DecomposeImapURI(aMessageURI, getter_AddRefs(folder), msgKey); + NS_ENSURE_SUCCESS(rv, rv); + + if (msgKey.IsEmpty()) return NS_MSG_MESSAGE_NOT_FOUND; + rv = nsParseImapMessageURI(aMessageURI, folderURI, &key, + getter_Copies(mimePart)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIImapMessageSink> imapMessageSink(do_QueryInterface(folder, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIImapUrl> imapUrl; + nsAutoCString urlSpec; + char hierarchyDelimiter = GetHierarchyDelimiter(folder); + rv = CreateStartOfImapUrl(aMessageURI, getter_AddRefs(imapUrl), folder, + aUrlListener, urlSpec, hierarchyDelimiter); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl(do_QueryInterface(imapUrl)); + + // This option is used by the JS Mime Emitter, in case we want a cheap + // streaming, for example, if we just want a quick look at some header, + // without having to download all the attachments... + + // We need to add the fetch command here for the cache lookup to behave + // correctly + nsAutoCString additionalHeader(aAdditionalHeader); + rv = AddImapFetchToUrl(mailnewsurl, folder, msgKey, additionalHeader); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgIncomingServer> aMsgIncomingServer; + + mailnewsurl->SetMsgWindow(aMsgWindow); + rv = mailnewsurl->GetServer(getter_AddRefs(aMsgIncomingServer)); + + // Try to check if the message is offline + bool hasMsgOffline = false; + folder->HasMsgOffline(key, &hasMsgOffline); + mailnewsurl->SetMsgIsInLocalCache(hasMsgOffline); + imapUrl->SetLocalFetchOnly(aLocalOnly); + + // If we don't have the message available locally, and we can't get it + // over the network, return with an error + if (aLocalOnly || WeAreOffline()) { + bool isMsgInMemCache = false; + if (!hasMsgOffline) { + rv = IsMsgInMemCache(mailnewsurl, folder, &isMsgInMemCache); + NS_ENSURE_SUCCESS(rv, rv); + + if (!isMsgInMemCache) return NS_ERROR_FAILURE; + } + } + + bool shouldStoreMsgOffline = false; + folder->ShouldStoreMsgOffline(key, &shouldStoreMsgOffline); + imapUrl->SetStoreResultsOffline(shouldStoreMsgOffline); + rv = GetMessageFromUrl(imapUrl, nsIImapUrl::nsImapMsgFetchPeek, folder, + imapMessageSink, aMsgWindow, aConsumer, aConvertData, + aURL); + return rv; +} + +// this method streams a message's headers to the passed in consumer. +NS_IMETHODIMP nsImapService::StreamHeaders(const nsACString& aMessageURI, + nsIStreamListener* aConsumer, + nsIUrlListener* aUrlListener, + bool aLocalOnly, nsIURI** aURL) { + NS_ENSURE_ARG_POINTER(aConsumer); + nsCOMPtr<nsIMsgFolder> folder; + nsAutoCString msgKey; + nsAutoCString folderURI; + nsCString mimePart; + nsMsgKey key; + + nsresult rv = DecomposeImapURI(aMessageURI, getter_AddRefs(folder), msgKey); + NS_ENSURE_SUCCESS(rv, rv); + + if (msgKey.IsEmpty()) return NS_MSG_MESSAGE_NOT_FOUND; + rv = nsParseImapMessageURI(aMessageURI, folderURI, &key, + getter_Copies(mimePart)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIInputStream> inputStream; + bool hasMsgOffline = false; + folder->HasMsgOffline(key, &hasMsgOffline); + if (hasMsgOffline) { + nsCOMPtr<nsIMsgDBHdr> hdr; + rv = folder->GetMessageHeader(key, getter_AddRefs(hdr)); + NS_ENSURE_SUCCESS(rv, rv); + rv = folder->GetLocalMsgStream(hdr, getter_AddRefs(inputStream)); + NS_ENSURE_SUCCESS(rv, rv); + return MsgStreamMsgHeaders(inputStream, aConsumer); + } + + if (aLocalOnly) return NS_ERROR_FAILURE; + return NS_OK; +} + +NS_IMETHODIMP nsImapService::IsMsgInMemCache(nsIURI* aUrl, + nsIMsgFolder* aImapMailFolder, + bool* aResult) { + NS_ENSURE_ARG_POINTER(aUrl); + NS_ENSURE_ARG_POINTER(aImapMailFolder); + *aResult = false; + + // Poke around in the memory cache + if (mCacheStorage) { + nsAutoCString urlSpec; + aUrl->GetSpec(urlSpec); + + // Strip any query qualifiers. + bool truncated = false; + int32_t ind = urlSpec.FindChar('?'); + if (ind != kNotFound) { + urlSpec.SetLength(ind); + truncated = true; + } + ind = urlSpec.Find("/;"); + if (ind != kNotFound) { + urlSpec.SetLength(ind); + truncated = true; + } + + nsresult rv; + nsCOMPtr<nsIImapMailFolderSink> folderSink( + do_QueryInterface(aImapMailFolder, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t uidValidity = -1; + folderSink->GetUidValidity(&uidValidity); + // stick the uid validity in front of the url, so that if the uid validity + // changes, we won't re-use the wrong cache entries. + nsAutoCString extension; + extension.AppendInt(uidValidity, 16); + + bool exists; + if (truncated) { + nsCOMPtr<nsIURI> newUri; + rv = NS_NewURI(getter_AddRefs(newUri), urlSpec); + NS_ENSURE_SUCCESS(rv, rv); + rv = mCacheStorage->Exists(newUri, extension, &exists); + } else { + rv = mCacheStorage->Exists(aUrl, extension, &exists); + } + if (NS_SUCCEEDED(rv) && exists) { + *aResult = true; + } + } + + return NS_OK; +} + +nsresult nsImapService::CreateStartOfImapUrl(const nsACString& aImapURI, + nsIImapUrl** imapUrl, + nsIMsgFolder* aImapMailFolder, + nsIUrlListener* aUrlListener, + nsACString& urlSpec, + char& hierarchyDelimiter) { + NS_ENSURE_ARG_POINTER(aImapMailFolder); + + nsCString hostname; + nsCString username; + nsCString escapedUsername; + + nsresult rv = aImapMailFolder->GetHostname(hostname); + NS_ENSURE_SUCCESS(rv, rv); + rv = aImapMailFolder->GetUsername(username); + NS_ENSURE_SUCCESS(rv, rv); + if (!username.IsEmpty()) + MsgEscapeString(username, nsINetUtil::ESCAPE_XALPHAS, escapedUsername); + + int32_t port = nsIImapUrl::DEFAULT_IMAP_PORT; + nsCOMPtr<nsIMsgIncomingServer> server; + rv = aImapMailFolder->GetServer(getter_AddRefs(server)); + if (NS_SUCCEEDED(rv)) { + server->GetPort(&port); + if (port == -1 || port == 0) port = nsIImapUrl::DEFAULT_IMAP_PORT; + } + + // now we need to create an imap url to load into the connection. The url + // needs to represent a select folder action. + rv = CallCreateInstance(kImapUrlCID, imapUrl); + if (NS_SUCCEEDED(rv) && *imapUrl) { + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(*imapUrl, &rv); + if (NS_SUCCEEDED(rv) && mailnewsUrl && aUrlListener) + mailnewsUrl->RegisterListener(aUrlListener); + nsCOMPtr<nsIMsgMessageUrl> msgurl(do_QueryInterface(*imapUrl)); + (*imapUrl)->SetExternalLinkUrl(false); + msgurl->SetUri(aImapURI); + + urlSpec = "imap://"; + urlSpec.Append(escapedUsername); + urlSpec.Append('@'); + urlSpec.Append(hostname); + urlSpec.Append(':'); + + nsAutoCString portStr; + portStr.AppendInt(port); + urlSpec.Append(portStr); + + // *** jefft - force to parse the urlSpec in order to search for + // the correct incoming server + rv = mailnewsUrl->SetSpecInternal(urlSpec); + NS_ENSURE_SUCCESS(rv, rv); + + hierarchyDelimiter = kOnlineHierarchySeparatorUnknown; + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = + do_QueryInterface(aImapMailFolder); + if (imapFolder) imapFolder->GetHierarchyDelimiter(&hierarchyDelimiter); + } + return rv; +} + +/* fetching the headers of RFC822 messages */ +/* imap4://HOST>header><UID/SEQUENCE>>MAILBOXPATH>x */ +/* 'x' is the message UID or sequence number list */ +/* will not affect the 'SEEN' flag */ +NS_IMETHODIMP nsImapService::GetHeaders(nsIMsgFolder* aImapMailFolder, + nsIUrlListener* aUrlListener, + nsIURI** aURL, + const nsACString& messageIdentifierList, + bool messageIdsAreUID) { + // 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. + NS_ENSURE_ARG_POINTER(aImapMailFolder); + + nsCOMPtr<nsIImapUrl> imapUrl; + nsAutoCString urlSpec; + char hierarchyDelimiter = GetHierarchyDelimiter(aImapMailFolder); + + nsresult rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl), + aImapMailFolder, aUrlListener, urlSpec, + hierarchyDelimiter); + if (NS_SUCCEEDED(rv) && imapUrl) { + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(imapUrl); + + rv = imapUrl->SetImapAction(nsIImapUrl::nsImapMsgFetch); + rv = SetImapUrlSink(aImapMailFolder, imapUrl); + + if (NS_SUCCEEDED(rv)) { + urlSpec.AppendLiteral("/header>"); + urlSpec.Append(messageIdsAreUID ? uidString : sequenceString); + urlSpec.Append('>'); + urlSpec.Append(char(hierarchyDelimiter)); + + nsCString folderName; + + GetFolderName(aImapMailFolder, folderName); + urlSpec.Append(folderName); + urlSpec.Append('>'); + urlSpec.Append(messageIdentifierList); + rv = mailnewsUrl->SetSpecInternal(urlSpec); + + if (NS_SUCCEEDED(rv)) + rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, aURL); + } + } + return rv; +} + +/* peeking at the start of msg bodies */ +/* imap4://HOST>header><UID>>MAILBOXPATH>x>n */ +/* 'x' is the message UID */ +/* 'n' is the number of bytes to fetch */ +/* will not affect the 'SEEN' flag */ +NS_IMETHODIMP nsImapService::GetBodyStart( + nsIMsgFolder* aImapMailFolder, nsIUrlListener* aUrlListener, + const nsACString& messageIdentifierList, int32_t numBytes, nsIURI** aURL) { + NS_ENSURE_ARG_POINTER(aImapMailFolder); + + nsresult rv; + nsCOMPtr<nsIImapUrl> imapUrl; + nsAutoCString urlSpec; + + char hierarchyDelimiter = GetHierarchyDelimiter(aImapMailFolder); + rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl), + aImapMailFolder, aUrlListener, urlSpec, + hierarchyDelimiter); + if (NS_SUCCEEDED(rv) && imapUrl) { + rv = imapUrl->SetImapAction(nsIImapUrl::nsImapMsgPreview); + rv = SetImapUrlSink(aImapMailFolder, imapUrl); + + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(imapUrl); + + urlSpec.AppendLiteral("/previewBody>"); + urlSpec.Append(uidString); + urlSpec.Append('>'); + urlSpec.Append(hierarchyDelimiter); + + nsCString folderName; + GetFolderName(aImapMailFolder, folderName); + urlSpec.Append(folderName); + urlSpec.Append('>'); + urlSpec.Append(messageIdentifierList); + urlSpec.Append('>'); + urlSpec.AppendInt(numBytes); + rv = mailnewsUrl->SetSpecInternal(urlSpec); + if (NS_SUCCEEDED(rv)) + rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, aURL); + } + } + return rv; +} + +nsresult nsImapService::FolderCommand(nsIMsgFolder* imapMailFolder, + nsIUrlListener* urlListener, + const char* aCommand, + nsImapAction imapAction, + nsIMsgWindow* msgWindow, nsIURI** url) { + NS_ENSURE_ARG_POINTER(imapMailFolder); + + nsCOMPtr<nsIImapUrl> imapUrl; + nsAutoCString urlSpec; + + char hierarchyDelimiter = GetHierarchyDelimiter(imapMailFolder); + nsresult rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl), + imapMailFolder, urlListener, urlSpec, + hierarchyDelimiter); + if (NS_SUCCEEDED(rv) && imapUrl) { + rv = imapUrl->SetImapAction(imapAction); + rv = SetImapUrlSink(imapMailFolder, imapUrl); + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl = do_QueryInterface(imapUrl); + if (mailnewsurl) mailnewsurl->SetMsgWindow(msgWindow); + + if (NS_SUCCEEDED(rv)) { + urlSpec.Append(aCommand); + urlSpec.Append(hierarchyDelimiter); + + nsCString folderName; + GetFolderName(imapMailFolder, folderName); + urlSpec.Append(folderName); + rv = mailnewsurl->SetSpecInternal(urlSpec); + if (NS_SUCCEEDED(rv)) + rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, url); + } + } + return rv; +} + +NS_IMETHODIMP +nsImapService::VerifyLogon(nsIMsgFolder* aFolder, nsIUrlListener* aUrlListener, + nsIMsgWindow* aMsgWindow, nsIURI** aURL) { + nsCOMPtr<nsIImapUrl> imapUrl; + nsAutoCString urlSpec; + + char delimiter = '/'; // shouldn't matter what is is. + nsresult rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl), + aFolder, aUrlListener, urlSpec, delimiter); + if (NS_SUCCEEDED(rv) && imapUrl) { + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl = do_QueryInterface(imapUrl); + + nsCOMPtr<nsIMsgMailNewsUrl> mailNewsUrl = do_QueryInterface(imapUrl); + mailNewsUrl->SetSuppressErrorMsgs(true); + mailNewsUrl->SetMsgWindow(aMsgWindow); + rv = SetImapUrlSink(aFolder, imapUrl); + urlSpec.AppendLiteral("/verifyLogon"); + rv = mailnewsurl->SetSpecInternal(urlSpec); + if (NS_SUCCEEDED(rv)) + rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, nullptr); + if (aURL) mailnewsurl.forget(aURL); + } + return rv; +} + +// Noop, used to update a folder (causes server to send changes). +NS_IMETHODIMP nsImapService::Noop(nsIMsgFolder* aImapMailFolder, + nsIUrlListener* aUrlListener, nsIURI** aURL) { + NS_ENSURE_ARG_POINTER(aImapMailFolder); + + return FolderCommand(aImapMailFolder, aUrlListener, "/selectnoop>", + nsIImapUrl::nsImapSelectNoopFolder, nullptr, aURL); +} + +// FolderStatus, used to update message counts +NS_IMETHODIMP nsImapService::UpdateFolderStatus(nsIMsgFolder* aImapMailFolder, + nsIUrlListener* aUrlListener, + nsIURI** aURL) { + NS_ENSURE_ARG_POINTER(aImapMailFolder); + + return FolderCommand(aImapMailFolder, aUrlListener, "/folderstatus>", + nsIImapUrl::nsImapFolderStatus, nullptr, aURL); +} + +// Expunge, used to "compress" an imap folder,removes deleted messages. +NS_IMETHODIMP nsImapService::Expunge(nsIMsgFolder* aImapMailFolder, + nsIUrlListener* aUrlListener, + nsIMsgWindow* aMsgWindow) { + NS_ENSURE_ARG_POINTER(aImapMailFolder); + + return FolderCommand(aImapMailFolder, aUrlListener, "/Expunge>", + nsIImapUrl::nsImapExpungeFolder, aMsgWindow, nullptr); +} + +/* old-stle biff that doesn't download headers */ +NS_IMETHODIMP nsImapService::Biff(nsIMsgFolder* aImapMailFolder, + nsIUrlListener* aUrlListener, nsIURI** aURL, + uint32_t uidHighWater) { + NS_ENSURE_ARG_POINTER(aImapMailFolder); + + // static const char *formatString = "biff>%c%s>%ld"; + nsCOMPtr<nsIImapUrl> imapUrl; + nsAutoCString urlSpec; + + char hierarchyDelimiter = GetHierarchyDelimiter(aImapMailFolder); + nsresult rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl), + aImapMailFolder, aUrlListener, urlSpec, + hierarchyDelimiter); + if (NS_SUCCEEDED(rv) && imapUrl) { + rv = imapUrl->SetImapAction(nsIImapUrl::nsImapExpungeFolder); + rv = SetImapUrlSink(aImapMailFolder, imapUrl); + + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl = do_QueryInterface(imapUrl); + if (NS_SUCCEEDED(rv)) { + urlSpec.AppendLiteral("/Biff>"); + urlSpec.Append(hierarchyDelimiter); + + nsCString folderName; + GetFolderName(aImapMailFolder, folderName); + urlSpec.Append(folderName); + urlSpec.Append('>'); + urlSpec.AppendInt(uidHighWater); + rv = mailnewsurl->SetSpecInternal(urlSpec); + if (NS_SUCCEEDED(rv)) + rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, aURL); + } + } + return rv; +} + +NS_IMETHODIMP nsImapService::DeleteFolder(nsIMsgFolder* aImapMailFolder, + nsIUrlListener* aUrlListener, + nsIMsgWindow* aMsgWindow) { + NS_ENSURE_ARG_POINTER(aImapMailFolder); + + // If it's an aol server then use 'deletefolder' url to + // remove all msgs first and then remove the folder itself. + bool removeFolderAndMsgs = false; + nsCOMPtr<nsIMsgIncomingServer> server; + if (NS_SUCCEEDED(aImapMailFolder->GetServer(getter_AddRefs(server))) && + server) { + nsCOMPtr<nsIImapIncomingServer> imapServer = do_QueryInterface(server); + if (imapServer) imapServer->GetIsAOLServer(&removeFolderAndMsgs); + } + + return FolderCommand(aImapMailFolder, aUrlListener, + removeFolderAndMsgs ? "/deletefolder>" : "/delete>", + nsIImapUrl::nsImapDeleteFolder, aMsgWindow, nullptr); +} + +NS_IMETHODIMP nsImapService::DeleteMessages( + nsIMsgFolder* aImapMailFolder, nsIUrlListener* aUrlListener, nsIURI** aURL, + const nsACString& messageIdentifierList, bool messageIdsAreUID) { + NS_ENSURE_ARG_POINTER(aImapMailFolder); + + // 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; + nsCOMPtr<nsIImapUrl> imapUrl; + nsAutoCString urlSpec; + + char hierarchyDelimiter = GetHierarchyDelimiter(aImapMailFolder); + rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl), + aImapMailFolder, aUrlListener, urlSpec, + hierarchyDelimiter); + if (NS_SUCCEEDED(rv) && imapUrl) { + rv = imapUrl->SetImapAction(nsIImapUrl::nsImapMsgFetch); + rv = SetImapUrlSink(aImapMailFolder, imapUrl); + + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl = do_QueryInterface(imapUrl); + + urlSpec.AppendLiteral("/deletemsg>"); + urlSpec.Append(messageIdsAreUID ? uidString : sequenceString); + urlSpec.Append('>'); + urlSpec.Append(hierarchyDelimiter); + + nsCString folderName; + GetFolderName(aImapMailFolder, folderName); + urlSpec.Append(folderName); + urlSpec.Append('>'); + urlSpec.Append(messageIdentifierList); + rv = mailnewsurl->SetSpecInternal(urlSpec); + if (NS_SUCCEEDED(rv)) + rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, aURL); + } + } + return rv; +} + +// Delete all messages in a folder, used to empty trash +NS_IMETHODIMP nsImapService::DeleteAllMessages(nsIMsgFolder* aImapMailFolder, + nsIUrlListener* aUrlListener) { + NS_ENSURE_ARG_POINTER(aImapMailFolder); + + return FolderCommand(aImapMailFolder, aUrlListener, "/deleteallmsgs>", + nsIImapUrl::nsImapSelectNoopFolder, nullptr, nullptr); +} + +NS_IMETHODIMP nsImapService::AddMessageFlags( + nsIMsgFolder* aImapMailFolder, nsIUrlListener* aUrlListener, + const nsACString& messageIdentifierList, imapMessageFlagsType flags, + bool messageIdsAreUID) { + NS_ENSURE_ARG_POINTER(aImapMailFolder); + + return DiddleFlags(aImapMailFolder, aUrlListener, nullptr, + messageIdentifierList, "addmsgflags", flags, + messageIdsAreUID); +} + +NS_IMETHODIMP nsImapService::SubtractMessageFlags( + nsIMsgFolder* aImapMailFolder, nsIUrlListener* aUrlListener, + const nsACString& messageIdentifierList, imapMessageFlagsType flags, + bool messageIdsAreUID) { + NS_ENSURE_ARG_POINTER(aImapMailFolder); + + return DiddleFlags(aImapMailFolder, aUrlListener, nullptr, + messageIdentifierList, "subtractmsgflags", flags, + messageIdsAreUID); +} + +NS_IMETHODIMP nsImapService::SetMessageFlags( + nsIMsgFolder* aImapMailFolder, nsIUrlListener* aUrlListener, nsIURI** aURL, + const nsACString& messageIdentifierList, imapMessageFlagsType flags, + bool messageIdsAreUID) { + NS_ENSURE_ARG_POINTER(aImapMailFolder); + + return DiddleFlags(aImapMailFolder, aUrlListener, aURL, messageIdentifierList, + "setmsgflags", flags, messageIdsAreUID); +} + +nsresult nsImapService::DiddleFlags(nsIMsgFolder* aImapMailFolder, + nsIUrlListener* aUrlListener, nsIURI** aURL, + const nsACString& messageIdentifierList, + const char* howToDiddle, + imapMessageFlagsType flags, + bool messageIdsAreUID) { + NS_ENSURE_ARG_POINTER(aImapMailFolder); + + // 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. + nsCOMPtr<nsIImapUrl> imapUrl; + nsAutoCString urlSpec; + + char hierarchyDelimiter = GetHierarchyDelimiter(aImapMailFolder); + nsresult rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl), + aImapMailFolder, aUrlListener, urlSpec, + hierarchyDelimiter); + if (NS_SUCCEEDED(rv) && imapUrl) { + rv = imapUrl->SetImapAction(nsIImapUrl::nsImapMsgFetch); + rv = SetImapUrlSink(aImapMailFolder, imapUrl); + + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl = do_QueryInterface(imapUrl); + + urlSpec.Append('/'); + urlSpec.Append(howToDiddle); + urlSpec.Append('>'); + urlSpec.Append(messageIdsAreUID ? uidString : sequenceString); + urlSpec.Append('>'); + urlSpec.Append(hierarchyDelimiter); + nsCString folderName; + GetFolderName(aImapMailFolder, folderName); + urlSpec.Append(folderName); + urlSpec.Append('>'); + urlSpec.Append(messageIdentifierList); + urlSpec.Append('>'); + urlSpec.AppendInt(flags); + rv = mailnewsurl->SetSpecInternal(urlSpec); + if (NS_SUCCEEDED(rv)) + rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, aURL); + } + } + return rv; +} + +nsresult nsImapService::SetImapUrlSink(nsIMsgFolder* aMsgFolder, + nsIImapUrl* aImapUrl) { + NS_ENSURE_ARG_POINTER(aMsgFolder); + NS_ENSURE_ARG_POINTER(aImapUrl); + + nsresult rv; + nsCOMPtr<nsIMsgIncomingServer> incomingServer; + nsCOMPtr<nsIImapServerSink> imapServerSink; + + rv = aMsgFolder->GetServer(getter_AddRefs(incomingServer)); + if (NS_SUCCEEDED(rv) && incomingServer) { + imapServerSink = do_QueryInterface(incomingServer); + if (imapServerSink) aImapUrl->SetImapServerSink(imapServerSink); + } + + nsCOMPtr<nsIImapMailFolderSink> imapMailFolderSink = + do_QueryInterface(aMsgFolder); + if (NS_SUCCEEDED(rv) && imapMailFolderSink) + aImapUrl->SetImapMailFolderSink(imapMailFolderSink); + + nsCOMPtr<nsIImapMessageSink> imapMessageSink = do_QueryInterface(aMsgFolder); + if (NS_SUCCEEDED(rv) && imapMessageSink) + aImapUrl->SetImapMessageSink(imapMessageSink); + + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(aImapUrl); + mailnewsUrl->SetFolder(aMsgFolder); + + return NS_OK; +} + +NS_IMETHODIMP nsImapService::DiscoverAllFolders(nsIMsgFolder* aImapMailFolder, + nsIUrlListener* aUrlListener, + nsIMsgWindow* aMsgWindow) { + NS_ENSURE_ARG_POINTER(aImapMailFolder); + + nsCOMPtr<nsIImapUrl> imapUrl; + nsAutoCString urlSpec; + + char hierarchyDelimiter = GetHierarchyDelimiter(aImapMailFolder); + nsresult rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl), + aImapMailFolder, aUrlListener, urlSpec, + hierarchyDelimiter); + if (NS_SUCCEEDED(rv)) { + rv = SetImapUrlSink(aImapMailFolder, imapUrl); + + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl = do_QueryInterface(imapUrl); + mailnewsurl->SetMsgWindow(aMsgWindow); + urlSpec.AppendLiteral("/discoverallboxes"); + rv = mailnewsurl->SetSpecInternal(urlSpec); + if (NS_SUCCEEDED(rv)) + rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, nullptr); + } + } + return rv; +} + +NS_IMETHODIMP nsImapService::DiscoverAllAndSubscribedFolders( + nsIMsgFolder* aImapMailFolder, nsIUrlListener* aUrlListener, + nsIMsgWindow* aMsgWindow) { + NS_ENSURE_ARG_POINTER(aImapMailFolder); + + nsCOMPtr<nsIImapUrl> aImapUrl; + nsAutoCString urlSpec; + + char hierarchyDelimiter = GetHierarchyDelimiter(aImapMailFolder); + nsresult rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(aImapUrl), + aImapMailFolder, aUrlListener, urlSpec, + hierarchyDelimiter); + if (NS_SUCCEEDED(rv) && aImapUrl) { + rv = SetImapUrlSink(aImapMailFolder, aImapUrl); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl = do_QueryInterface(aImapUrl); + urlSpec.AppendLiteral("/discoverallandsubscribedboxes"); + rv = mailnewsurl->SetSpecInternal(urlSpec); + + if (aMsgWindow) mailnewsurl->SetMsgWindow(aMsgWindow); + + if (NS_SUCCEEDED(rv)) + rv = GetImapConnectionAndLoadUrl(aImapUrl, nullptr, nullptr); + } + } + return rv; +} + +NS_IMETHODIMP nsImapService::DiscoverChildren(nsIMsgFolder* aImapMailFolder, + nsIUrlListener* aUrlListener, + const nsACString& folderPath) { + NS_ENSURE_ARG_POINTER(aImapMailFolder); + + nsCOMPtr<nsIImapUrl> aImapUrl; + nsAutoCString urlSpec; + + char hierarchyDelimiter = GetHierarchyDelimiter(aImapMailFolder); + nsresult rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(aImapUrl), + aImapMailFolder, aUrlListener, urlSpec, + hierarchyDelimiter); + if (NS_SUCCEEDED(rv)) { + rv = SetImapUrlSink(aImapMailFolder, aImapUrl); + if (NS_SUCCEEDED(rv)) { + if (!folderPath.IsEmpty()) { + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl = do_QueryInterface(aImapUrl); + urlSpec.AppendLiteral("/discoverchildren>"); + urlSpec.Append(hierarchyDelimiter); + urlSpec.Append(folderPath); + rv = mailnewsurl->SetSpecInternal(urlSpec); + + // Make sure the uri has the same hierarchy separator as the one in msg + // folder obj if it's not kOnlineHierarchySeparatorUnknown (ie, '^'). + char uriDelimiter; + nsresult rv1 = aImapUrl->GetOnlineSubDirSeparator(&uriDelimiter); + if (NS_SUCCEEDED(rv1) && + hierarchyDelimiter != kOnlineHierarchySeparatorUnknown && + uriDelimiter != hierarchyDelimiter) + aImapUrl->SetOnlineSubDirSeparator(hierarchyDelimiter); + + if (NS_SUCCEEDED(rv)) + rv = GetImapConnectionAndLoadUrl(aImapUrl, nullptr, nullptr); + } else + rv = NS_ERROR_FAILURE; + } + } + return rv; +} + +NS_IMETHODIMP nsImapService::OnlineMessageCopy( + nsIMsgFolder* aSrcFolder, const nsACString& messageIds, + nsIMsgFolder* aDstFolder, bool idsAreUids, bool isMove, + nsIUrlListener* aUrlListener, nsIURI** aURL, nsISupports* copyState, + nsIMsgWindow* aMsgWindow) { + NS_ENSURE_ARG_POINTER(aSrcFolder); + NS_ENSURE_ARG_POINTER(aDstFolder); + + nsresult rv; + nsCOMPtr<nsIMsgIncomingServer> srcServer; + nsCOMPtr<nsIMsgIncomingServer> dstServer; + + rv = aSrcFolder->GetServer(getter_AddRefs(srcServer)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aDstFolder->GetServer(getter_AddRefs(dstServer)); + NS_ENSURE_SUCCESS(rv, rv); + + bool sameServer; + rv = dstServer->Equals(srcServer, &sameServer); + NS_ENSURE_SUCCESS(rv, rv); + + if (!sameServer) { + NS_ASSERTION(false, "can't use this method to copy across servers"); + // *** can only take message from the same imap host and user accnt + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIImapUrl> imapUrl; + nsAutoCString urlSpec; + + char hierarchyDelimiter = GetHierarchyDelimiter(aSrcFolder); + rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl), aSrcFolder, + aUrlListener, urlSpec, hierarchyDelimiter); + if (NS_SUCCEEDED(rv)) { + SetImapUrlSink(aSrcFolder, imapUrl); + imapUrl->SetCopyState(copyState); + + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl(do_QueryInterface(imapUrl)); + mailnewsurl->SetMsgWindow(aMsgWindow); + + if (isMove) + urlSpec.AppendLiteral("/onlinemove>"); + else + urlSpec.AppendLiteral("/onlinecopy>"); + if (idsAreUids) + urlSpec.Append(uidString); + else + urlSpec.Append(sequenceString); + urlSpec.Append('>'); + urlSpec.Append(hierarchyDelimiter); + + nsCString folderName; + GetFolderName(aSrcFolder, folderName); + urlSpec.Append(folderName); + urlSpec.Append('>'); + urlSpec.Append(messageIds); + urlSpec.Append('>'); + urlSpec.Append(hierarchyDelimiter); + folderName.Adopt(strdup("")); + GetFolderName(aDstFolder, folderName); + urlSpec.Append(folderName); + + rv = mailnewsurl->SetSpecInternal(urlSpec); + if (NS_SUCCEEDED(rv)) + rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, aURL); + } + return rv; +} + +nsresult nsImapService::OfflineAppendFromFile( + nsIFile* aFile, nsIURI* aUrl, nsIMsgFolder* aDstFolder, + const nsACString& messageId, // to be replaced + bool inSelectedState, // needs to be in + nsIUrlListener* aListener, nsIURI** aURL, nsISupports* aCopyState) { + nsCOMPtr<nsIMsgDatabase> destDB; + nsresult rv = aDstFolder->GetMsgDatabase(getter_AddRefs(destDB)); + // ### might need to send some notifications instead of just returning + + bool isLocked; + aDstFolder->GetLocked(&isLocked); + if (isLocked) return NS_MSG_FOLDER_BUSY; + + if (NS_SUCCEEDED(rv) && destDB) { + nsMsgKey fakeKey; + destDB->GetNextFakeOfflineMsgKey(&fakeKey); + + nsCOMPtr<nsIMsgOfflineImapOperation> op; + nsCOMPtr<nsIMsgOfflineOpsDatabase> opsDb = do_QueryInterface(destDB, &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = opsDb->GetOfflineOpForKey(fakeKey, true, getter_AddRefs(op)); + if (NS_SUCCEEDED(rv) && op) { + nsCString destFolderUri; + aDstFolder->GetURI(destFolderUri); + op->SetOperation( + nsIMsgOfflineImapOperation::kAppendDraft); // ### do we care if it's + // a template? + op->SetDestinationFolderURI(destFolderUri); + nsCOMPtr<nsIOutputStream> outputStream; + nsCOMPtr<nsIMsgPluggableStore> msgStore; + nsCOMPtr<nsIMsgIncomingServer> dstServer; + nsCOMPtr<nsIMsgDBHdr> newMsgHdr; + + aDstFolder->GetServer(getter_AddRefs(dstServer)); + rv = dstServer->GetMsgStore(getter_AddRefs(msgStore)); + NS_ENSURE_SUCCESS(rv, rv); + rv = destDB->CreateNewHdr(fakeKey, getter_AddRefs(newMsgHdr)); + NS_ENSURE_SUCCESS(rv, rv); + rv = aDstFolder->GetOfflineStoreOutputStream( + newMsgHdr, getter_AddRefs(outputStream)); + + if (NS_SUCCEEDED(rv) && outputStream) { + nsCOMPtr<nsIInputStream> inputStream; + nsCOMPtr<nsIMsgParseMailMsgState> msgParser = do_CreateInstance( + "@mozilla.org/messenger/messagestateparser;1", &rv); + msgParser->SetMailDB(destDB); + + rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream), aFile); + if (NS_SUCCEEDED(rv) && inputStream) { + // now, copy the temp file to the offline store for the dest folder. + RefPtr<nsMsgLineStreamBuffer> inputStreamBuffer = + new nsMsgLineStreamBuffer( + FILE_IO_BUFFER_SIZE, + true, // allocate new lines + false); // leave CRLFs on the returned string + int64_t fileSize; + aFile->GetFileSize(&fileSize); + uint32_t bytesWritten; + rv = NS_OK; + // rv = inputStream->Read(inputBuffer, inputBufferSize, &bytesRead); + // if (NS_SUCCEEDED(rv) && bytesRead > 0) + msgParser->SetState(nsIMsgParseMailMsgState::ParseHeadersState); + msgParser->SetNewMsgHdr(newMsgHdr); + // set the new key to fake key so the msg hdr will have that for a key + msgParser->SetNewKey(fakeKey); + bool needMoreData = false; + char* newLine = nullptr; + uint32_t numBytesInLine = 0; + do { + newLine = inputStreamBuffer->ReadNextLine( + inputStream, numBytesInLine, needMoreData); + if (newLine) { + msgParser->ParseAFolderLine(newLine, numBytesInLine); + rv = outputStream->Write(newLine, numBytesInLine, &bytesWritten); + free(newLine); + } + } while (newLine); + msgParser->FinishHeader(); + + if (NS_SUCCEEDED(rv)) { + uint32_t resultFlags; + newMsgHdr->OrFlags( + nsMsgMessageFlags::Offline | nsMsgMessageFlags::Read, + &resultFlags); + newMsgHdr->SetOfflineMessageSize(fileSize); + destDB->AddNewHdrToDB(newMsgHdr, true /* notify */); + aDstFolder->SetFlag(nsMsgFolderFlags::OfflineEvents); + if (msgStore) msgStore->FinishNewMessage(outputStream, newMsgHdr); + } + // tell the listener we're done. + inputStream->Close(); + inputStream = nullptr; + aListener->OnStopRunningUrl(aUrl, NS_OK); + } + outputStream->Close(); + } + } + } + + if (destDB) destDB->Close(true); + return rv; +} + +/* append message from file url */ +/* imap://HOST>appendmsgfromfile>DESTINATIONMAILBOXPATH */ +/* imap://HOST>appenddraftfromfile>DESTINATIONMAILBOXPATH>UID>messageId */ +NS_IMETHODIMP nsImapService::AppendMessageFromFile( + nsIFile* aFile, nsIMsgFolder* aDstFolder, + const nsACString& messageId, // to be replaced + bool idsAreUids, + bool inSelectedState, // needs to be in + nsIUrlListener* aListener, nsISupports* aCopyState, + nsIMsgWindow* aMsgWindow) { + NS_ENSURE_ARG_POINTER(aFile); + NS_ENSURE_ARG_POINTER(aDstFolder); + + nsresult rv; + nsCOMPtr<nsIImapUrl> imapUrl; + nsAutoCString urlSpec; + + char hierarchyDelimiter = GetHierarchyDelimiter(aDstFolder); + rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl), aDstFolder, + aListener, urlSpec, hierarchyDelimiter); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIMsgMailNewsUrl> msgUrl = do_QueryInterface(imapUrl); + if (msgUrl && aMsgWindow) { + // we get the loadGroup from msgWindow + msgUrl->SetMsgWindow(aMsgWindow); + } + + SetImapUrlSink(aDstFolder, imapUrl); + imapUrl->SetMsgFile(aFile); + imapUrl->SetCopyState(aCopyState); + + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl = do_QueryInterface(imapUrl); + + if (inSelectedState) + urlSpec.AppendLiteral("/appenddraftfromfile>"); + else + urlSpec.AppendLiteral("/appendmsgfromfile>"); + + urlSpec.Append(hierarchyDelimiter); + + nsCString folderName; + GetFolderName(aDstFolder, folderName); + urlSpec.Append(folderName); + + if (inSelectedState) { + urlSpec.Append('>'); + if (idsAreUids) + urlSpec.Append(uidString); + else + urlSpec.Append(sequenceString); + urlSpec.Append('>'); + if (!messageId.IsEmpty()) urlSpec.Append(messageId); + } + + rv = mailnewsurl->SetSpecInternal(urlSpec); + if (WeAreOffline()) { + // handle offline append to drafts or templates folder here. + return OfflineAppendFromFile(aFile, mailnewsurl, aDstFolder, messageId, + inSelectedState, aListener, nullptr, + aCopyState); + } + if (NS_SUCCEEDED(rv)) + rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, nullptr); + } + return rv; +} + +nsresult nsImapService::GetImapConnectionAndLoadUrl(nsIImapUrl* aImapUrl, + nsISupports* aConsumer, + nsIURI** aURL) { + NS_ENSURE_ARG_POINTER(aImapUrl); + + bool isValidUrl; + aImapUrl->GetValidUrl(&isValidUrl); + if (!isValidUrl) return NS_ERROR_FAILURE; + + if (WeAreOffline()) { + nsImapAction imapAction; + + // the only thing we can do offline is fetch messages. + // ### TODO - need to look at msg copy, save attachment, etc. when we + // have offline message bodies. + aImapUrl->GetImapAction(&imapAction); + if (imapAction != nsIImapUrl::nsImapMsgFetch && + imapAction != nsIImapUrl::nsImapSaveMessageToDisk) + return NS_MSG_ERROR_OFFLINE; + } + + nsCOMPtr<nsIMsgIncomingServer> aMsgIncomingServer; + nsCOMPtr<nsIMsgMailNewsUrl> msgUrl = do_QueryInterface(aImapUrl); + nsresult rv = msgUrl->GetServer(getter_AddRefs(aMsgIncomingServer)); + + if (aURL) { + msgUrl.forget(aURL); + } + + if (NS_SUCCEEDED(rv) && aMsgIncomingServer) { + nsCOMPtr<nsIImapIncomingServer> aImapServer( + do_QueryInterface(aMsgIncomingServer, &rv)); + if (NS_SUCCEEDED(rv) && aImapServer) + rv = aImapServer->GetImapConnectionAndLoadUrl(aImapUrl, aConsumer); + } + return rv; +} + +NS_IMETHODIMP nsImapService::MoveFolder(nsIMsgFolder* srcFolder, + nsIMsgFolder* dstFolder, + nsIUrlListener* urlListener, + nsIMsgWindow* msgWindow) { + NS_ENSURE_ARG_POINTER(srcFolder); + NS_ENSURE_ARG_POINTER(dstFolder); + + nsCOMPtr<nsIImapUrl> imapUrl; + nsAutoCString urlSpec; + nsresult rv; + + char default_hierarchyDelimiter = GetHierarchyDelimiter(dstFolder); + rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl), dstFolder, + urlListener, urlSpec, default_hierarchyDelimiter); + if (NS_SUCCEEDED(rv) && imapUrl) { + rv = SetImapUrlSink(dstFolder, imapUrl); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIMsgMailNewsUrl> mailNewsUrl = do_QueryInterface(imapUrl); + if (mailNewsUrl) mailNewsUrl->SetMsgWindow(msgWindow); + char hierarchyDelimiter = kOnlineHierarchySeparatorUnknown; + nsCString folderName; + + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl = do_QueryInterface(imapUrl); + GetFolderName(srcFolder, folderName); + urlSpec.AppendLiteral("/movefolderhierarchy>"); + urlSpec.Append(hierarchyDelimiter); + urlSpec.Append(folderName); + urlSpec.Append('>'); + GetFolderName(dstFolder, folderName); + if (!folderName.IsEmpty()) { + urlSpec.Append(hierarchyDelimiter); + urlSpec.Append(folderName); + } + rv = mailnewsurl->SetSpecInternal(urlSpec); + if (NS_SUCCEEDED(rv)) { + GetFolderName(srcFolder, folderName); + rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, nullptr); + } + } + } + return rv; +} + +NS_IMETHODIMP nsImapService::RenameLeaf(nsIMsgFolder* srcFolder, + const nsAString& newLeafName, + nsIUrlListener* urlListener, + nsIMsgWindow* msgWindow) { + NS_ENSURE_ARG_POINTER(srcFolder); + + nsCOMPtr<nsIImapUrl> imapUrl; + nsAutoCString urlSpec; + + char hierarchyDelimiter = GetHierarchyDelimiter(srcFolder); + nsresult rv = + CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl), srcFolder, + urlListener, urlSpec, hierarchyDelimiter); + if (NS_SUCCEEDED(rv)) { + rv = SetImapUrlSink(srcFolder, imapUrl); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIMsgMailNewsUrl> mailNewsUrl = do_QueryInterface(imapUrl); + mailNewsUrl->SetMsgWindow(msgWindow); + nsCString folderName; + GetFolderName(srcFolder, folderName); + urlSpec.AppendLiteral("/rename>"); + urlSpec.Append(hierarchyDelimiter); + urlSpec.Append(folderName); + urlSpec.Append('>'); + urlSpec.Append(hierarchyDelimiter); + nsAutoCString cStrFolderName; + // Unescape the name before looking for parent path + MsgUnescapeString(folderName, 0, cStrFolderName); + int32_t leafNameStart = cStrFolderName.RFindChar(hierarchyDelimiter); + if (leafNameStart != -1) { + cStrFolderName.SetLength(leafNameStart + 1); + urlSpec.Append(cStrFolderName); + } + + nsAutoCString utfNewName; + bool utf8AcceptEnabled; + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(srcFolder); + rv = imapFolder->GetShouldUseUtf8FolderName(&utf8AcceptEnabled); + NS_ENSURE_SUCCESS(rv, rv); + if (utf8AcceptEnabled) { + CopyUTF16toUTF8(newLeafName, utfNewName); + } else { + CopyUTF16toMUTF7(newLeafName, utfNewName); + } + nsCString escapedNewName; + MsgEscapeString(utfNewName, nsINetUtil::ESCAPE_URL_PATH, escapedNewName); + nsCString escapedSlashName; + rv = nsImapUrl::EscapeSlashes(escapedNewName.get(), + getter_Copies(escapedSlashName)); + NS_ENSURE_SUCCESS(rv, rv); + urlSpec.Append(escapedSlashName); + + rv = mailNewsUrl->SetSpecInternal(urlSpec); + if (NS_SUCCEEDED(rv)) + rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, nullptr); + } + } + return rv; +} + +NS_IMETHODIMP nsImapService::CreateFolder(nsIMsgFolder* parent, + const nsAString& newFolderName, + nsIUrlListener* urlListener, + nsIURI** url) { + NS_ENSURE_ARG_POINTER(parent); + + nsCOMPtr<nsIImapUrl> imapUrl; + nsAutoCString urlSpec; + nsresult rv; + + char hierarchyDelimiter = GetHierarchyDelimiter(parent); + rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl), parent, + urlListener, urlSpec, hierarchyDelimiter); + if (NS_SUCCEEDED(rv) && imapUrl) { + rv = SetImapUrlSink(parent, imapUrl); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl = do_QueryInterface(imapUrl); + + nsCString folderName; + GetFolderName(parent, folderName); + urlSpec.AppendLiteral("/create>"); + urlSpec.Append(hierarchyDelimiter); + if (!folderName.IsEmpty()) { + nsCString canonicalName; + nsImapUrl::ConvertToCanonicalFormat( + folderName.get(), hierarchyDelimiter, getter_Copies(canonicalName)); + urlSpec.Append(canonicalName); + urlSpec.Append(hierarchyDelimiter); + } + + nsAutoCString utfNewName; + bool utf8AcceptEnabled; + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(parent); + rv = imapFolder->GetShouldUseUtf8FolderName(&utf8AcceptEnabled); + NS_ENSURE_SUCCESS(rv, rv); + if (utf8AcceptEnabled) { + CopyUTF16toUTF8(newFolderName, utfNewName); + } else { + CopyUTF16toMUTF7(newFolderName, utfNewName); + } + nsCString escapedFolderName; + MsgEscapeString(utfNewName, nsINetUtil::ESCAPE_URL_PATH, + escapedFolderName); + urlSpec.Append(escapedFolderName); + + rv = mailnewsurl->SetSpecInternal(urlSpec); + if (NS_SUCCEEDED(rv)) + rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, url); + } + } + return rv; +} + +NS_IMETHODIMP nsImapService::EnsureFolderExists(nsIMsgFolder* parent, + const nsAString& newFolderName, + nsIMsgWindow* msgWindow, + nsIUrlListener* urlListener) { + NS_ENSURE_ARG_POINTER(parent); + + nsCOMPtr<nsIImapUrl> imapUrl; + nsAutoCString urlSpec; + nsresult rv; + + char hierarchyDelimiter = GetHierarchyDelimiter(parent); + rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl), parent, + urlListener, urlSpec, hierarchyDelimiter); + if (NS_SUCCEEDED(rv) && imapUrl) { + rv = SetImapUrlSink(parent, imapUrl); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl = do_QueryInterface(imapUrl); + + nsCString folderName; + GetFolderName(parent, folderName); + urlSpec.AppendLiteral("/ensureExists>"); + urlSpec.Append(hierarchyDelimiter); + if (!folderName.IsEmpty()) { + urlSpec.Append(folderName); + urlSpec.Append(hierarchyDelimiter); + } + nsAutoCString utfNewName; + bool utf8AcceptEnabled; + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(parent); + rv = imapFolder->GetShouldUseUtf8FolderName(&utf8AcceptEnabled); + NS_ENSURE_SUCCESS(rv, rv); + if (utf8AcceptEnabled) { + CopyUTF16toUTF8(newFolderName, utfNewName); + } else { + CopyUTF16toMUTF7(newFolderName, utfNewName); + } + nsCString escapedFolderName; + MsgEscapeString(utfNewName, nsINetUtil::ESCAPE_URL_PATH, + escapedFolderName); + urlSpec.Append(escapedFolderName); + + rv = mailnewsurl->SetSpecInternal(urlSpec); + + if (msgWindow) mailnewsurl->SetMsgWindow(msgWindow); + + if (NS_SUCCEEDED(rv)) + rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, nullptr); + } + } + return rv; +} + +NS_IMETHODIMP nsImapService::ListFolder(nsIMsgFolder* aImapMailFolder, + nsIUrlListener* aUrlListener) { + NS_ENSURE_ARG_POINTER(aImapMailFolder); + + return FolderCommand(aImapMailFolder, aUrlListener, "/listfolder>", + nsIImapUrl::nsImapListFolder, nullptr, nullptr); +} + +NS_IMETHODIMP nsImapService::GetScheme(nsACString& aScheme) { + aScheme.AssignLiteral("imap"); + return NS_OK; +} + +NS_IMETHODIMP nsImapService::AllowPort(int32_t port, const char* scheme, + bool* aRetVal) { + // allow imap to run on any port + *aRetVal = true; + return NS_OK; +} + +NS_IMETHODIMP nsImapService::GetDefaultDoBiff(bool* aDoBiff) { + NS_ENSURE_ARG_POINTER(aDoBiff); + // by default, do biff for IMAP servers + *aDoBiff = true; + return NS_OK; +} + +NS_IMETHODIMP nsImapService::GetDefaultServerPort(bool isSecure, + int32_t* aDefaultPort) { + // Return Secure IMAP Port if secure option chosen i.e., if isSecure is TRUE + if (isSecure) + *aDefaultPort = nsIImapUrl::DEFAULT_IMAPS_PORT; + else + *aDefaultPort = nsIImapUrl::DEFAULT_IMAP_PORT; + + return NS_OK; +} + +// this method first tries to find an exact username and hostname match with the +// given url then, tries to find any account on the passed in imap host in case +// this is a url to a shared imap folder. +nsresult nsImapService::GetServerFromUrl(nsIImapUrl* aImapUrl, + nsIMsgIncomingServer** aServer) { + nsresult rv; + nsCString folderName; + nsAutoCString userPass; + nsAutoCString hostName; + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(aImapUrl); + + // if we can't get a folder name out of the url then I think this is an error + aImapUrl->CreateCanonicalSourceFolderPathString(getter_Copies(folderName)); + if (folderName.IsEmpty()) { + rv = mailnewsUrl->GetFileName(folderName); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr<nsIMsgAccountManager> accountManager = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = accountManager->FindServerByURI(mailnewsUrl, aServer); + + // look for server with any user name, in case we're trying to subscribe + // to a folder with some one else's user name like the following + // "IMAP://userSharingFolder@server1/SharedFolderName" + if (NS_FAILED(rv) || !aServer) { + nsAutoCString turl; + rv = mailnewsUrl->GetSpec(turl); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIURL> url; + rv = NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID) + .SetSpec(turl) + .SetUserPass(EmptyCString()) + .Finalize(url); + NS_ENSURE_SUCCESS(rv, rv); + + rv = accountManager->FindServerByURI(url, aServer); + if (*aServer) aImapUrl->SetExternalLinkUrl(true); + } + + // if we can't extract the imap server from this url then give up!!! + NS_ENSURE_TRUE(*aServer, NS_ERROR_FAILURE); + return rv; +} + +nsresult nsImapService::NewURI(const nsACString& aSpec, + const char* aOriginCharset, // ignored + nsIURI* aBaseURI, nsIURI** aRetVal) { + NS_ENSURE_ARG_POINTER(aRetVal); + + nsresult rv; + nsCOMPtr<nsIImapUrl> aImapUrl = do_CreateInstance(kImapUrlCID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // now extract lots of fun information... + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(aImapUrl); + // nsAutoCString unescapedSpec(aSpec); + // nsUnescape(unescapedSpec.BeginWriting()); + + // set the spec + if (aBaseURI) { + nsAutoCString newSpec; + aBaseURI->Resolve(aSpec, newSpec); + rv = mailnewsUrl->SetSpecInternal(newSpec); + } else { + rv = mailnewsUrl->SetSpecInternal(aSpec); + } + + NS_ENSURE_SUCCESS(rv, rv); + + nsCString folderName; + // if we can't get a folder name out of the url then I think this is an error + aImapUrl->CreateCanonicalSourceFolderPathString(getter_Copies(folderName)); + if (folderName.IsEmpty()) { + rv = mailnewsUrl->GetFileName(folderName); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr<nsIMsgIncomingServer> server; + rv = GetServerFromUrl(aImapUrl, getter_AddRefs(server)); + // if we can't extract the imap server from this url then give up!!! + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(server, NS_ERROR_FAILURE); + + // now try to get the folder in question... + nsCOMPtr<nsIMsgFolder> rootFolder; + server->GetRootFolder(getter_AddRefs(rootFolder)); + bool ready; + if (rootFolder && !folderName.IsEmpty() && + // Skip folder processing if folder names aren't ready yet. + // They may not be available during early initialization. + // XXX TODO: This hack can be removed when the localization system gets + // initialized in M-C code before, for example, the permission manager + // which creates all sorts of URIs incl. imap: URIs. + NS_SUCCEEDED(rootFolder->FolderNamesReady(&ready)) && ready) { + nsCOMPtr<nsIMsgFolder> folder; + nsCOMPtr<nsIMsgImapMailFolder> imapRoot = do_QueryInterface(rootFolder); + nsCOMPtr<nsIMsgImapMailFolder> subFolder; + if (imapRoot) { + imapRoot->FindOnlineSubFolder(folderName, getter_AddRefs(subFolder)); + folder = do_QueryInterface(subFolder); + } + + // If we can't find the folder, we can still create the URI + // in this low-level service. Cloning URIs where the folder + // isn't found is common when folders are renamed or moved. + // We also ignore return statuses here. + if (folder) { + nsCOMPtr<nsIImapMessageSink> msgSink = do_QueryInterface(folder); + (void)aImapUrl->SetImapMessageSink(msgSink); + + (void)SetImapUrlSink(folder, aImapUrl); + + nsCString messageIdString; + aImapUrl->GetListOfMessageIds(messageIdString); + if (!messageIdString.IsEmpty()) { + bool useLocalCache = false; + folder->HasMsgOffline(strtoul(messageIdString.get(), nullptr, 10), + &useLocalCache); + mailnewsUrl->SetMsgIsInLocalCache(useLocalCache); + } + } + } + + // we got an imap url, so be sure to return it... + nsCOMPtr<nsIURI> imapUri = do_QueryInterface(aImapUrl); + + imapUri.forget(aRetVal); + + return rv; +} + +NS_IMETHODIMP nsImapService::NewChannel(nsIURI* aURI, nsILoadInfo* aLoadInfo, + nsIChannel** aRetVal) { + NS_ENSURE_ARG_POINTER(aURI); + NS_ENSURE_ARG_POINTER(aRetVal); + MOZ_ASSERT(aLoadInfo); + *aRetVal = nullptr; + + nsresult rv; + nsCOMPtr<nsIImapUrl> imapUrl = do_QueryInterface(aURI, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(imapUrl, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // imap can't open and return a channel right away...the url needs to go in + // the imap url queue until we find a connection which can run the url..in + // order to satisfy necko, we're going to return a mock imap channel.... + nsCOMPtr<nsIImapMockChannel> channel = + do_CreateInstance(kCImapMockChannel, &rv); + NS_ENSURE_SUCCESS(rv, rv); + channel->SetURI(aURI); + + rv = channel->SetLoadInfo(aLoadInfo); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString spec; + rv = aURI->GetSpec(spec); + NS_ENSURE_SUCCESS(rv, rv); + + // Add the attachment disposition. This forces docShell to open the + // attachment instead of displaying it. Content types we have special + // handlers for are white-listed. This white list also exists in + // nsMailboxService::NewChannel and nsNntpService::NewChannel, so if you're + // changing this, update those too. + if (spec.Find("part=") >= 0 && spec.Find("type=message/rfc822") < 0 && + spec.Find("type=application/x-message-display") < 0 && + spec.Find("type=application/pdf") < 0) { + rv = channel->SetContentDisposition(nsIChannel::DISPOSITION_ATTACHMENT); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr<nsIMsgWindow> msgWindow; + mailnewsUrl->GetMsgWindow(getter_AddRefs(msgWindow)); + if (msgWindow) { + nsCOMPtr<nsIDocShell> msgDocShell; + msgWindow->GetRootDocShell(getter_AddRefs(msgDocShell)); + if (msgDocShell) { + nsCOMPtr<nsIProgressEventSink> prevEventSink; + channel->GetProgressEventSink(getter_AddRefs(prevEventSink)); + nsCOMPtr<nsIInterfaceRequestor> docIR(do_QueryInterface(msgDocShell)); + channel->SetNotificationCallbacks(docIR); + // we want to use our existing event sink. + if (prevEventSink) channel->SetProgressEventSink(prevEventSink); + } + } else { + // This might not be a call resulting from user action (e.g. we might be + // getting a new message via nsImapMailFolder::OnNewIdleMessages(), or via + // nsAutoSyncManager, etc). In this case, try to retrieve the top-most + // message window to update its status feedback. + nsCOMPtr<nsIMsgMailSession> mailSession = + do_GetService("@mozilla.org/messenger/services/session;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgWindow> msgWindow; + rv = mailSession->GetTopmostMsgWindow(getter_AddRefs(msgWindow)); + if (NS_SUCCEEDED(rv) && msgWindow) { + // If we could retrieve a window, get its nsIMsgStatusFeedback and set it + // to the URL so that other components interacting with it can correctly + // feed status updates to the UI. + nsCOMPtr<nsIMsgStatusFeedback> statusFeedback; + msgWindow->GetStatusFeedback(getter_AddRefs(statusFeedback)); + mailnewsUrl->SetStatusFeedback(statusFeedback); + // We also need to set the status feedback as the channel's progress event + // sink, since that's how nsImapProtocol feeds some of the progress + // changes (e.g. downloading incoming messages) to the UI. + nsCOMPtr<nsIProgressEventSink> eventSink = + do_QueryInterface(statusFeedback); + channel->SetProgressEventSink(eventSink); + } + + // This function ends by checking the final value of rv and deciding whether + // to set aRetVal to our channel according to it. We don't want this to be + // impacted if we fail to retrieve a window (which might not work if we're + // being called through the command line, or through a test), so let's just + // reset rv to an OK value. + rv = NS_OK; + } + + // the imap url holds a weak reference so we can pass the channel into the + // imap protocol when we actually run the url. + imapUrl->SetMockChannel(channel); + + bool externalLinkUrl; + imapUrl->GetExternalLinkUrl(&externalLinkUrl); + + // Only external imap links with no action are supported. Ignore links that + // attempt to cause an effect such as fetching a mime part. This avoids + // spurious prompts to subscribe to folders due to "imap://...Fetch..." links + // residing in legacy emails residing in an imap mailbox. + if (externalLinkUrl) { + nsImapAction imapAction; + imapUrl->GetImapAction(&imapAction); + if (imapAction != 0) externalLinkUrl = false; + } + + if (externalLinkUrl) { + // Everything after here is to handle clicking on an external link. We only + // want to do this if we didn't run the url through the various + // nsImapService methods, which we can tell by seeing if the sinks have been + // setup on the url or not. + nsCOMPtr<nsIMsgIncomingServer> server; + rv = GetServerFromUrl(imapUrl, getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + nsCString folderName; + imapUrl->CreateCanonicalSourceFolderPathString(getter_Copies(folderName)); + if (folderName.IsEmpty()) { + nsCString escapedFolderName; + rv = mailnewsUrl->GetFileName(escapedFolderName); + if (!escapedFolderName.IsEmpty()) { + MsgUnescapeString(escapedFolderName, 0, folderName); + } + } + // if the parent is null, then the folder doesn't really exist, so see if + // the user wants to subscribe to it./ + nsCOMPtr<nsIMsgFolder> urlFolder; + // now try to get the folder in question... + nsCOMPtr<nsIMsgFolder> rootFolder; + server->GetRootFolder(getter_AddRefs(rootFolder)); + nsCOMPtr<nsIMsgImapMailFolder> imapRoot = do_QueryInterface(rootFolder); + nsCOMPtr<nsIMsgImapMailFolder> subFolder; + if (imapRoot) { + imapRoot->FindOnlineSubFolder(folderName, getter_AddRefs(subFolder)); + urlFolder = do_QueryInterface(subFolder); + } + nsCOMPtr<nsIMsgFolder> parent; + if (urlFolder) urlFolder->GetParent(getter_AddRefs(parent)); + nsCString serverKey; + nsAutoCString userPass; + rv = mailnewsUrl->GetUserPass(userPass); + server->GetKey(serverKey); + nsCString fullFolderName; + if (parent) fullFolderName = folderName; + if (!parent && !folderName.IsEmpty() && imapRoot) { + // Check if this folder is another user's folder. + fullFolderName = + nsImapNamespaceList::GenerateFullFolderNameWithDefaultNamespace( + serverKey.get(), folderName.get(), userPass.get(), + kOtherUsersNamespace, nullptr); + // if this is another user's folder, let's see if we're already subscribed + // to it. + rv = imapRoot->FindOnlineSubFolder(fullFolderName, + getter_AddRefs(subFolder)); + urlFolder = do_QueryInterface(subFolder); + if (urlFolder) urlFolder->GetParent(getter_AddRefs(parent)); + } + // if we couldn't get the fullFolderName, then we probably couldn't find + // the other user's namespace, in which case, we shouldn't try to subscribe + // to it. + if (!parent && !folderName.IsEmpty() && !fullFolderName.IsEmpty()) { + // this folder doesn't exist - check if the user wants to subscribe to + // this folder. + nsCOMPtr<nsIPrompt> dialog; + nsCOMPtr<nsIWindowWatcher> wwatch( + do_GetService(NS_WINDOWWATCHER_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + wwatch->GetNewPrompter(nullptr, getter_AddRefs(dialog)); + + nsString statusString, confirmText; + nsCOMPtr<nsIStringBundle> bundle; + rv = IMAPGetStringBundle(getter_AddRefs(bundle)); + NS_ENSURE_SUCCESS(rv, rv); + // Need to convert folder name, can be MUTF-7 or UTF-8 depending on the + // server. + nsAutoString unescapedName; + if (NS_FAILED(CopyFolderNameToUTF16(fullFolderName, unescapedName))) + CopyASCIItoUTF16(fullFolderName, unescapedName); + AutoTArray<nsString, 1> formatStrings = {unescapedName}; + + rv = bundle->FormatStringFromName("imapSubscribePrompt", formatStrings, + confirmText); + NS_ENSURE_SUCCESS(rv, rv); + + bool confirmResult = false; + rv = dialog->Confirm(nullptr, confirmText.get(), &confirmResult); + NS_ENSURE_SUCCESS(rv, rv); + + if (confirmResult) { + nsCOMPtr<nsIImapIncomingServer> imapServer = do_QueryInterface(server); + if (imapServer) { + nsCOMPtr<nsIURI> subscribeURI; + // Now we have the real folder name to try to subscribe to. Let's try + // running a subscribe url and returning that as the uri we've + // created. We need to convert this to unicode because that's what + // subscribe wants. + nsAutoString unicodeName; + CopyFolderNameToUTF16(fullFolderName, unicodeName); + rv = imapServer->SubscribeToFolder(unicodeName, true, + getter_AddRefs(subscribeURI)); + if (NS_SUCCEEDED(rv) && subscribeURI) { + nsCOMPtr<nsIImapUrl> imapSubscribeUrl = + do_QueryInterface(subscribeURI); + if (imapSubscribeUrl) imapSubscribeUrl->SetExternalLinkUrl(true); + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = + do_QueryInterface(subscribeURI); + if (mailnewsUrl) { + nsCOMPtr<nsIMsgMailSession> mailSession = do_GetService( + "@mozilla.org/messenger/services/session;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgWindow> msgWindow; + rv = mailSession->GetTopmostMsgWindow(getter_AddRefs(msgWindow)); + if (NS_SUCCEEDED(rv) && msgWindow) { + mailnewsUrl->SetMsgWindow(msgWindow); + nsCOMPtr<nsIUrlListener> listener = + do_QueryInterface(rootFolder); + if (listener) mailnewsUrl->RegisterListener(listener); + } + } + } + } + } + // error out this channel, so it'll stop trying to run the url. + rv = NS_ERROR_FAILURE; + *aRetVal = nullptr; + } + // this folder exists - check if this is a click on a link to the folder + // in which case, we'll select it. + else if (!fullFolderName.IsEmpty()) { + nsCOMPtr<nsIMsgFolder> imapFolder; + mailnewsUrl->GetFolder(getter_AddRefs(imapFolder)); + NS_ASSERTION( + imapFolder, + nsPrintfCString("No folder for imap url: %s", spec.get()).get()); + + nsCOMPtr<nsIMsgMailSession> mailSession = + do_GetService("@mozilla.org/messenger/services/session;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgWindow> msgWindow; + rv = mailSession->GetTopmostMsgWindow(getter_AddRefs(msgWindow)); + if (NS_SUCCEEDED(rv) && msgWindow) { + // Clicked IMAP folder URL in the window. + nsCOMPtr<nsIObserverService> obsServ = + mozilla::services::GetObserverService(); + obsServ->NotifyObservers(imapFolder, "folder-attention", nullptr); + // null out this channel, so it'll stop trying to run the url. + *aRetVal = nullptr; + rv = NS_OK; + } else { + // Got IMAP folder URL from command line (most likely). + // Set action to nsImapSelectFolder (x-application-imapfolder), so + // ::HandleContent will handle it. + imapUrl->SetImapAction(nsIImapUrl::nsImapSelectFolder); + HandleContent("x-application-imapfolder", nullptr, channel); + } + } + } + if (NS_SUCCEEDED(rv)) channel.forget(aRetVal); + return rv; +} + +NS_IMETHODIMP nsImapService::SetDefaultLocalPath(nsIFile* aPath) { + NS_ENSURE_ARG_POINTER(aPath); + + return NS_SetPersistentFile(PREF_MAIL_ROOT_IMAP_REL, PREF_MAIL_ROOT_IMAP, + aPath); +} + +NS_IMETHODIMP nsImapService::GetDefaultLocalPath(nsIFile** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + *aResult = nullptr; + + bool havePref; + nsCOMPtr<nsIFile> localFile; + nsresult rv = NS_GetPersistentFile( + PREF_MAIL_ROOT_IMAP_REL, PREF_MAIL_ROOT_IMAP, NS_APP_IMAP_MAIL_50_DIR, + havePref, getter_AddRefs(localFile)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(localFile, NS_ERROR_FAILURE); + + bool exists; + rv = localFile->Exists(&exists); + if (NS_SUCCEEDED(rv) && !exists) + rv = localFile->Create(nsIFile::DIRECTORY_TYPE, 0775); + NS_ENSURE_SUCCESS(rv, rv); + + if (!havePref || !exists) { + rv = NS_SetPersistentFile(PREF_MAIL_ROOT_IMAP_REL, PREF_MAIL_ROOT_IMAP, + localFile); + NS_ASSERTION(NS_SUCCEEDED(rv), "Failed to set root dir pref."); + } + + localFile.forget(aResult); + return NS_OK; +} + +NS_IMETHODIMP nsImapService::GetServerIID(nsIID** aServerIID) { + *aServerIID = new nsIID(NS_GET_IID(nsIImapIncomingServer)); + return NS_OK; +} + +NS_IMETHODIMP nsImapService::GetRequiresUsername(bool* aRequiresUsername) { + NS_ENSURE_ARG_POINTER(aRequiresUsername); + + *aRequiresUsername = true; + return NS_OK; +} + +NS_IMETHODIMP nsImapService::GetPreflightPrettyNameWithEmailAddress( + bool* aPreflightPrettyNameWithEmailAddress) { + NS_ENSURE_ARG_POINTER(aPreflightPrettyNameWithEmailAddress); + + *aPreflightPrettyNameWithEmailAddress = true; + return NS_OK; +} + +NS_IMETHODIMP nsImapService::GetCanLoginAtStartUp(bool* aCanLoginAtStartUp) { + NS_ENSURE_ARG_POINTER(aCanLoginAtStartUp); + *aCanLoginAtStartUp = true; + return NS_OK; +} + +NS_IMETHODIMP nsImapService::GetCanDelete(bool* aCanDelete) { + NS_ENSURE_ARG_POINTER(aCanDelete); + *aCanDelete = true; + return NS_OK; +} + +NS_IMETHODIMP nsImapService::GetCanDuplicate(bool* aCanDuplicate) { + NS_ENSURE_ARG_POINTER(aCanDuplicate); + *aCanDuplicate = true; + return NS_OK; +} + +NS_IMETHODIMP nsImapService::GetCanGetMessages(bool* aCanGetMessages) { + NS_ENSURE_ARG_POINTER(aCanGetMessages); + *aCanGetMessages = true; + return NS_OK; +} + +NS_IMETHODIMP nsImapService::GetCanGetIncomingMessages( + bool* aCanGetIncomingMessages) { + NS_ENSURE_ARG_POINTER(aCanGetIncomingMessages); + *aCanGetIncomingMessages = true; + return NS_OK; +} + +NS_IMETHODIMP nsImapService::GetShowComposeMsgLink(bool* showComposeMsgLink) { + NS_ENSURE_ARG_POINTER(showComposeMsgLink); + *showComposeMsgLink = true; + return NS_OK; +} + +NS_IMETHODIMP nsImapService::GetFoldersCreatedAsync(bool* aAsyncCreation) { + NS_ENSURE_ARG_POINTER(aAsyncCreation); + *aAsyncCreation = true; + return NS_OK; +} + +NS_IMETHODIMP nsImapService::GetListOfFoldersWithPath( + nsIImapIncomingServer* aServer, nsIMsgWindow* aMsgWindow, + const nsACString& folderPath) { + nsresult rv; + nsCOMPtr<nsIMsgIncomingServer> server = do_QueryInterface(aServer); + if (!server) return NS_ERROR_FAILURE; + + nsCOMPtr<nsIMsgFolder> rootMsgFolder; + rv = server->GetRootMsgFolder(getter_AddRefs(rootMsgFolder)); + + NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && rootMsgFolder, NS_ERROR_FAILURE); + + nsCOMPtr<nsIUrlListener> listener = do_QueryInterface(aServer, &rv); + NS_ENSURE_SUCCESS(rv, rv); + if (!listener) return NS_ERROR_FAILURE; + + // Locate the folder so that the correct hierarchical delimiter is used in the + // folder pathnames, otherwise root's (ie, '^') is used and this is wrong. + nsCOMPtr<nsIMsgFolder> msgFolder; + if (rootMsgFolder && !folderPath.IsEmpty()) { + // If the folder path contains 'INBOX' of any forms, we need to convert it + // to uppercase before finding it under the root folder. We do the same in + // PossibleImapMailbox(). + nsAutoCString tempFolderName(folderPath); + nsAutoCString tokenStr, remStr, changedStr; + int32_t slashPos = tempFolderName.FindChar('/'); + if (slashPos > 0) { + tokenStr = StringHead(tempFolderName, slashPos); + remStr = Substring(tempFolderName, slashPos); + } else + tokenStr.Assign(tempFolderName); + + if (tokenStr.LowerCaseEqualsLiteral("inbox") && + !tokenStr.EqualsLiteral("INBOX")) + changedStr.AppendLiteral("INBOX"); + else + changedStr.Append(tokenStr); + + if (slashPos > 0) changedStr.Append(remStr); + + rv = rootMsgFolder->FindSubFolder(changedStr, getter_AddRefs(msgFolder)); + } + return DiscoverChildren(msgFolder, listener, folderPath); +} + +NS_IMETHODIMP nsImapService::GetListOfFoldersOnServer( + nsIImapIncomingServer* aServer, nsIMsgWindow* aMsgWindow) { + nsresult rv; + + nsCOMPtr<nsIMsgIncomingServer> server = do_QueryInterface(aServer); + if (!server) return NS_ERROR_FAILURE; + + nsCOMPtr<nsIMsgFolder> rootMsgFolder; + rv = server->GetRootMsgFolder(getter_AddRefs(rootMsgFolder)); + + NS_ENSURE_SUCCESS(rv, rv); + if (!rootMsgFolder) return NS_ERROR_FAILURE; + + nsCOMPtr<nsIUrlListener> listener = do_QueryInterface(aServer, &rv); + NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && listener, NS_ERROR_FAILURE); + + return DiscoverAllAndSubscribedFolders(rootMsgFolder, listener, aMsgWindow); +} + +NS_IMETHODIMP nsImapService::SubscribeFolder(nsIMsgFolder* aFolder, + const nsAString& aFolderName, + nsIUrlListener* urlListener, + nsIURI** url) { + return ChangeFolderSubscription(aFolder, aFolderName, "/subscribe>", + urlListener, url); +} + +nsresult nsImapService::ChangeFolderSubscription(nsIMsgFolder* folder, + const nsAString& folderName, + const char* command, + nsIUrlListener* urlListener, + nsIURI** url) { + NS_ENSURE_ARG_POINTER(folder); + + nsCOMPtr<nsIImapUrl> imapUrl; + nsAutoCString urlSpec; + nsresult rv; + char hierarchyDelimiter = GetHierarchyDelimiter(folder); + rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl), folder, + urlListener, urlSpec, hierarchyDelimiter); + if (NS_SUCCEEDED(rv) && imapUrl) { + rv = SetImapUrlSink(folder, imapUrl); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsurl = do_QueryInterface(imapUrl); + urlSpec.Append(command); + urlSpec.Append(hierarchyDelimiter); + // `folderName` contains MUFT-7 or UTF-8 as required by the server here. + NS_ConvertUTF16toUTF8 utfFolderName(folderName); + nsCString escapedFolderName; + MsgEscapeString(utfFolderName, nsINetUtil::ESCAPE_URL_PATH, + escapedFolderName); + urlSpec.Append(escapedFolderName); + rv = mailnewsurl->SetSpecInternal(urlSpec); + if (NS_SUCCEEDED(rv)) + rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, url); + } + } + return rv; +} + +NS_IMETHODIMP nsImapService::UnsubscribeFolder(nsIMsgFolder* aFolder, + const nsAString& aFolderName, + nsIUrlListener* aUrlListener, + nsIURI** aUrl) { + return ChangeFolderSubscription(aFolder, aFolderName, "/unsubscribe>", + aUrlListener, aUrl); +} + +NS_IMETHODIMP nsImapService::GetFolderAdminUrl(nsIMsgFolder* aImapMailFolder, + nsIMsgWindow* aMsgWindow, + nsIUrlListener* aUrlListener, + nsIURI** aURL) { + NS_ENSURE_ARG_POINTER(aImapMailFolder); + + return FolderCommand(aImapMailFolder, aUrlListener, "/refreshfolderurls>", + nsIImapUrl::nsImapRefreshFolderUrls, aMsgWindow, aURL); +} + +NS_IMETHODIMP nsImapService::IssueCommandOnMsgs(nsIMsgFolder* anImapFolder, + nsIMsgWindow* aMsgWindow, + const nsACString& aCommand, + const nsACString& uids, + nsIURI** aURL) { + NS_ENSURE_ARG_POINTER(anImapFolder); + NS_ENSURE_ARG_POINTER(aMsgWindow); + + nsCOMPtr<nsIImapUrl> imapUrl; + nsAutoCString urlSpec; + nsresult rv; + char hierarchyDelimiter = GetHierarchyDelimiter(anImapFolder); + rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl), + anImapFolder, nullptr, urlSpec, hierarchyDelimiter); + + if (NS_SUCCEEDED(rv) && imapUrl) { + // nsImapUrl::SetSpec() will set the imap action properly + rv = imapUrl->SetImapAction(nsIImapUrl::nsImapUserDefinedMsgCommand); + + nsCOMPtr<nsIMsgMailNewsUrl> mailNewsUrl = do_QueryInterface(imapUrl); + mailNewsUrl->SetMsgWindow(aMsgWindow); + mailNewsUrl->SetUpdatingFolder(true); + rv = SetImapUrlSink(anImapFolder, imapUrl); + + if (NS_SUCCEEDED(rv)) { + nsCString folderName; + GetFolderName(anImapFolder, folderName); + urlSpec.Append('/'); + urlSpec.Append(aCommand); + urlSpec.Append('>'); + urlSpec.Append(uidString); + urlSpec.Append('>'); + urlSpec.Append(hierarchyDelimiter); + urlSpec.Append(folderName); + urlSpec.Append('>'); + urlSpec.Append(uids); + rv = mailNewsUrl->SetSpecInternal(urlSpec); + if (NS_SUCCEEDED(rv)) + rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, aURL); + } + } // if we have a url to run.... + + return rv; +} + +NS_IMETHODIMP nsImapService::FetchCustomMsgAttribute( + nsIMsgFolder* anImapFolder, nsIMsgWindow* aMsgWindow, + const nsACString& aAttribute, const nsACString& uids, nsIURI** aURL) { + NS_ENSURE_ARG_POINTER(anImapFolder); + NS_ENSURE_ARG_POINTER(aMsgWindow); + + nsCOMPtr<nsIImapUrl> imapUrl; + nsAutoCString urlSpec; + nsresult rv; + char hierarchyDelimiter = GetHierarchyDelimiter(anImapFolder); + rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl), + anImapFolder, nullptr, urlSpec, hierarchyDelimiter); + if (NS_SUCCEEDED(rv) && imapUrl) { + // nsImapUrl::SetSpec() will set the imap action properly + rv = imapUrl->SetImapAction(nsIImapUrl::nsImapUserDefinedFetchAttribute); + + nsCOMPtr<nsIMsgMailNewsUrl> mailNewsUrl = do_QueryInterface(imapUrl); + mailNewsUrl->SetMsgWindow(aMsgWindow); + mailNewsUrl->SetUpdatingFolder(true); + rv = SetImapUrlSink(anImapFolder, imapUrl); + + if (NS_SUCCEEDED(rv)) { + nsCString folderName; + GetFolderName(anImapFolder, folderName); + urlSpec.AppendLiteral("/customFetch>UID>"); + urlSpec.Append(hierarchyDelimiter); + urlSpec.Append(folderName); + urlSpec.Append('>'); + urlSpec.Append(uids); + urlSpec.Append('>'); + urlSpec.Append(aAttribute); + rv = mailNewsUrl->SetSpecInternal(urlSpec); + if (NS_SUCCEEDED(rv)) + rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, aURL); + } + } // if we have a url to run.... + + return rv; +} + +NS_IMETHODIMP nsImapService::StoreCustomKeywords( + nsIMsgFolder* anImapFolder, nsIMsgWindow* aMsgWindow, + const nsACString& flagsToAdd, const nsACString& flagsToSubtract, + const nsACString& uids, nsIURI** aURL) { + NS_ENSURE_ARG_POINTER(anImapFolder); + + nsCOMPtr<nsIImapUrl> imapUrl; + nsAutoCString urlSpec; + nsresult rv; + char hierarchyDelimiter = GetHierarchyDelimiter(anImapFolder); + rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl), + anImapFolder, nullptr, urlSpec, hierarchyDelimiter); + + if (NS_SUCCEEDED(rv) && imapUrl) { + // nsImapUrl::SetSpec() will set the imap action properly + rv = imapUrl->SetImapAction(nsIImapUrl::nsImapMsgStoreCustomKeywords); + + nsCOMPtr<nsIMsgMailNewsUrl> mailNewsUrl = do_QueryInterface(imapUrl); + mailNewsUrl->SetMsgWindow(aMsgWindow); + mailNewsUrl->SetUpdatingFolder(true); + rv = SetImapUrlSink(anImapFolder, imapUrl); + + if (NS_SUCCEEDED(rv)) { + nsCString folderName; + GetFolderName(anImapFolder, folderName); + urlSpec.AppendLiteral("/customKeywords>UID>"); + urlSpec.Append(hierarchyDelimiter); + urlSpec.Append(folderName); + urlSpec.Append('>'); + urlSpec.Append(uids); + urlSpec.Append('>'); + urlSpec.Append(flagsToAdd); + urlSpec.Append('>'); + urlSpec.Append(flagsToSubtract); + rv = mailNewsUrl->SetSpecInternal(urlSpec); + if (NS_SUCCEEDED(rv)) + rv = GetImapConnectionAndLoadUrl(imapUrl, nullptr, aURL); + } + } // if we have a url to run.... + + return rv; +} + +NS_IMETHODIMP nsImapService::DownloadMessagesForOffline( + const nsACString& messageIds, nsIMsgFolder* aFolder, + nsIUrlListener* aUrlListener, nsIMsgWindow* aMsgWindow) { + NS_ENSURE_ARG_POINTER(aFolder); + + nsCOMPtr<nsIImapUrl> imapUrl; + nsAutoCString urlSpec; + nsresult rv; + char hierarchyDelimiter = GetHierarchyDelimiter(aFolder); + rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl), aFolder, + nullptr, 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)); + rv = FetchMessage(imapUrl, nsImapUrl::nsImapMsgDownloadForOffline, aFolder, + imapMessageSink, aMsgWindow, nullptr, messageIds, false, + 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(true); + } + } + return rv; +} + +NS_IMETHODIMP nsImapService::MessageURIToMsgHdr(const nsACString& uri, + nsIMsgDBHdr** aRetVal) { + NS_ENSURE_ARG_POINTER(aRetVal); + + nsCOMPtr<nsIMsgFolder> folder; + nsMsgKey msgKey; + nsresult rv = DecomposeImapURI(uri, getter_AddRefs(folder), &msgKey); + NS_ENSURE_SUCCESS(rv, rv); + rv = folder->GetMessageHeader(msgKey, aRetVal); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP nsImapService::PlaybackAllOfflineOperations( + nsIMsgWindow* aMsgWindow, nsIUrlListener* aListener, + nsISupports** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + nsresult rv; + nsImapOfflineSync* goOnline = new nsImapOfflineSync(); + goOnline->Init(aMsgWindow, aListener, nullptr, false); + rv = goOnline->QueryInterface(NS_GET_IID(nsISupports), (void**)aResult); + NS_ENSURE_SUCCESS(rv, rv); + if (NS_SUCCEEDED(rv) && *aResult) return goOnline->ProcessNextOperation(); + return NS_ERROR_OUT_OF_MEMORY; +} + +NS_IMETHODIMP nsImapService::DownloadAllOffineImapFolders( + nsIMsgWindow* aMsgWindow, nsIUrlListener* aListener) { + RefPtr<nsImapOfflineDownloader> downloadForOffline = + new nsImapOfflineDownloader(aMsgWindow, aListener); + if (downloadForOffline) { + // hold reference to this so it won't get deleted out from under itself. + nsresult rv = downloadForOffline->ProcessNextOperation(); + return rv; + } + return NS_ERROR_OUT_OF_MEMORY; +} + +NS_IMETHODIMP nsImapService::GetCacheStorage(nsICacheStorage** result) { + nsresult rv = NS_OK; + if (!mCacheStorage) { + nsCOMPtr<nsICacheStorageService> cacheStorageService = + do_GetService("@mozilla.org/netwerk/cache-storage-service;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<MailnewsLoadContextInfo> lci = + new MailnewsLoadContextInfo(false, false, mozilla::OriginAttributes()); + + // Determine if disk cache or memory cache is in use. + // Note: This is mozilla system cache, not offline storage (mbox, maildir) + // which is also sometimes referred to as cache at places in the code. + if (mozilla::Preferences::GetBool("mail.imap.use_disk_cache2", true)) + rv = cacheStorageService->DiskCacheStorage(lci, + getter_AddRefs(mCacheStorage)); + else + rv = cacheStorageService->MemoryCacheStorage( + lci, getter_AddRefs(mCacheStorage)); + NS_ENSURE_SUCCESS(rv, rv); + } + + NS_IF_ADDREF(*result = mCacheStorage); + return rv; +} + +NS_IMETHODIMP nsImapService::HandleContent( + const char* aContentType, nsIInterfaceRequestor* aWindowContext, + nsIRequest* request) { + NS_ENSURE_ARG_POINTER(request); + + nsresult rv; + nsCOMPtr<nsIChannel> aChannel = do_QueryInterface(request, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + if (PL_strcasecmp(aContentType, "x-application-imapfolder") == 0) { + nsCOMPtr<nsIURI> uri; + rv = aChannel->GetURI(getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + + if (uri) { + request->Cancel(NS_BINDING_ABORTED); + nsCOMPtr<nsIWindowMediator> mediator( + do_GetService(NS_WINDOWMEDIATOR_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString uriStr; + rv = uri->GetSpec(uriStr); + NS_ENSURE_SUCCESS(rv, rv); + + // imap uri's are unescaped, so unescape the url. + nsCString unescapedUriStr; + MsgUnescapeString(uriStr, 0, unescapedUriStr); + nsCOMPtr<nsIMessengerWindowService> messengerWindowService = + do_GetService("@mozilla.org/messenger/windowservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = messengerWindowService->OpenMessengerWindowWithUri( + "mail:3pane", unescapedUriStr, nsMsgKey_None); + NS_ENSURE_SUCCESS(rv, rv); + } + } else { + // The content-type was not x-application-imapfolder + return NS_ERROR_WONT_HANDLE_CONTENT; + } + + return rv; +} diff --git a/comm/mailnews/imap/src/nsImapService.h b/comm/mailnews/imap/src/nsImapService.h new file mode 100644 index 0000000000..86d38aa949 --- /dev/null +++ b/comm/mailnews/imap/src/nsImapService.h @@ -0,0 +1,122 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsImapService_h___ +#define nsImapService_h___ + +#include "nsIImapService.h" +#include "nsIMsgMessageService.h" +#include "nsCOMPtr.h" +#include "nsIFile.h" +#include "nsString.h" +#include "nsIProtocolHandler.h" +#include "nsIMsgProtocolInfo.h" +#include "nsIContentHandler.h" +#include "nsICacheStorage.h" + +class nsIImapUrl; +class nsIMsgFolder; +class nsIMsgIncomingServer; + +/** + * nsImapService implements the IMAP protocol. + * So, whenever someone opens an "imap://" url, the resultant nsIChannel + * is created here (via newChannel()). + * + * It also provides a bunch of methods to provide more egonomic ways to + * initiate IMAP operations, rather than manually composing an "imap://..." + * URL. See nsIImapService for these. + */ +class nsImapService : public nsIImapService, + public nsIMsgMessageService, + public nsIMsgMessageFetchPartService, + public nsIProtocolHandler, + public nsIMsgProtocolInfo, + public nsIContentHandler { + public: + nsImapService(); + static nsresult NewURI(const nsACString& aSpec, + const char* aOriginCharset, // ignored + nsIURI* aBaseURI, nsIURI** aRetVal); + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIMSGPROTOCOLINFO + NS_DECL_NSIIMAPSERVICE + NS_DECL_NSIMSGMESSAGESERVICE + NS_DECL_NSIPROTOCOLHANDLER + NS_DECL_NSIMSGMESSAGEFETCHPARTSERVICE + NS_DECL_NSICONTENTHANDLER + + protected: + virtual ~nsImapService(); + char GetHierarchyDelimiter(nsIMsgFolder* aMsgFolder); + + nsresult GetFolderName(nsIMsgFolder* aImapFolder, nsACString& aFolderName); + + // This is called by both FetchMessage and StreamMessage + nsresult GetMessageFromUrl(nsIImapUrl* aImapUrl, nsImapAction aImapAction, + nsIMsgFolder* aImapMailFolder, + nsIImapMessageSink* aImapMessage, + nsIMsgWindow* aMsgWindow, + nsISupports* aDisplayConsumer, + bool aConvertDataToText, nsIURI** aURL); + + nsresult CreateStartOfImapUrl( + const nsACString& + aImapURI, // a RDF URI for the current message/folder, can be empty + nsIImapUrl** imapUrl, nsIMsgFolder* aImapFolder, + nsIUrlListener* aUrlListener, nsACString& urlSpec, + char& hierarchyDelimiter); + + nsresult GetImapConnectionAndLoadUrl(nsIImapUrl* aImapUrl, + nsISupports* aConsumer, nsIURI** aURL); + + static nsresult SetImapUrlSink(nsIMsgFolder* aMsgFolder, + nsIImapUrl* aImapUrl); + + nsresult FetchMimePart(nsIImapUrl* aImapUrl, nsImapAction aImapAction, + nsIMsgFolder* aImapMailFolder, + nsIImapMessageSink* aImapMessage, nsIURI** aURL, + nsISupports* aDisplayConsumer, + const nsACString& messageIdentifierList, + const nsACString& mimePart); + + nsresult FolderCommand(nsIMsgFolder* imapMailFolder, + nsIUrlListener* urlListener, const char* aCommand, + nsImapAction imapAction, nsIMsgWindow* msgWindow, + nsIURI** url); + + nsresult ChangeFolderSubscription(nsIMsgFolder* folder, + const nsAString& folderName, + const char* aCommand, + nsIUrlListener* urlListener, nsIURI** url); + + nsresult DiddleFlags(nsIMsgFolder* aImapMailFolder, + nsIUrlListener* aUrlListener, nsIURI** aURL, + const nsACString& messageIdentifierList, + const char* howToDiddle, imapMessageFlagsType flags, + bool messageIdsAreUID); + + nsresult OfflineAppendFromFile(nsIFile* aFile, nsIURI* aUrl, + nsIMsgFolder* aDstFolder, + const nsACString& messageId, // to be replaced + bool inSelectedState, // needs to be in + nsIUrlListener* aListener, nsIURI** aURL, + nsISupports* aCopyState); + + static nsresult GetServerFromUrl(nsIImapUrl* aImapUrl, + nsIMsgIncomingServer** aServer); + + // just a little helper method...maybe it should be a macro? which helps break + // down a imap message uri into the folder and message key equivalents + nsresult DecomposeImapURI(const nsACString& aMessageURI, + nsIMsgFolder** aFolder, nsACString& msgKey); + nsresult DecomposeImapURI(const nsACString& aMessageURI, + nsIMsgFolder** aFolder, nsMsgKey* msgKey); + + nsCOMPtr<nsICacheStorage> mCacheStorage; +}; + +#endif /* nsImapService_h___ */ diff --git a/comm/mailnews/imap/src/nsImapStringBundle.cpp b/comm/mailnews/imap/src/nsImapStringBundle.cpp new file mode 100644 index 0000000000..ab20f6e20f --- /dev/null +++ b/comm/mailnews/imap/src/nsImapStringBundle.cpp @@ -0,0 +1,37 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#include "prprf.h" +#include "prmem.h" +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsIStringBundle.h" +#include "nsImapStringBundle.h" +#include "nsServiceManagerUtils.h" +#include "mozilla/Components.h" + +#define IMAP_MSGS_URL "chrome://messenger/locale/imapMsgs.properties" + +extern "C" nsresult IMAPGetStringByName(const char* stringName, + char16_t** aString) { + nsCOMPtr<nsIStringBundle> sBundle; + nsresult rv = IMAPGetStringBundle(getter_AddRefs(sBundle)); + if (NS_SUCCEEDED(rv) && sBundle) { + nsAutoString string; + rv = sBundle->GetStringFromName(stringName, string); + *aString = ToNewUnicode(string); + } + return rv; +} + +nsresult IMAPGetStringBundle(nsIStringBundle** aBundle) { + nsresult rv = NS_OK; + nsCOMPtr<nsIStringBundleService> stringService = + mozilla::components::StringBundle::Service(); + if (!stringService) return NS_ERROR_NULL_POINTER; + nsCOMPtr<nsIStringBundle> stringBundle; + rv = stringService->CreateBundle(IMAP_MSGS_URL, getter_AddRefs(stringBundle)); + stringBundle.forget(aBundle); + return rv; +} diff --git a/comm/mailnews/imap/src/nsImapStringBundle.h b/comm/mailnews/imap/src/nsImapStringBundle.h new file mode 100644 index 0000000000..a929391e43 --- /dev/null +++ b/comm/mailnews/imap/src/nsImapStringBundle.h @@ -0,0 +1,17 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef _nsImapStringBundle_H__ +#define _nsImapStringBundle_H__ + +#include "nsIStringBundle.h" + +PR_BEGIN_EXTERN_C + +nsresult IMAPGetStringByName(const char* stringName, char16_t** aString); +nsresult IMAPGetStringBundle(nsIStringBundle** aBundle); + +PR_END_EXTERN_C + +#endif /* _nsImapStringBundle_H__ */ diff --git a/comm/mailnews/imap/src/nsImapUndoTxn.cpp b/comm/mailnews/imap/src/nsImapUndoTxn.cpp new file mode 100644 index 0000000000..04fa74c90a --- /dev/null +++ b/comm/mailnews/imap/src/nsImapUndoTxn.cpp @@ -0,0 +1,647 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" // for precompiled headers +#include "nsIMsgHdr.h" +#include "nsImapUndoTxn.h" +#include "nsIMsgIncomingServer.h" +#include "nsImapMailFolder.h" +#include "nsIImapService.h" +#include "nsIDBFolderInfo.h" +#include "nsIMsgDatabase.h" +#include "nsMsgUtils.h" +#include "nsThreadUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsComponentManagerUtils.h" + +nsImapMoveCopyMsgTxn::nsImapMoveCopyMsgTxn() + : m_idsAreUids(false), m_isMove(false), m_srcIsPop3(false) {} + +nsresult nsImapMoveCopyMsgTxn::Init(nsIMsgFolder* srcFolder, + nsTArray<nsMsgKey>* srcKeyArray, + const char* srcMsgIdString, + nsIMsgFolder* dstFolder, bool idsAreUids, + bool isMove) { + m_srcMsgIdString = srcMsgIdString; + m_idsAreUids = idsAreUids; + m_isMove = isMove; + m_srcFolder = do_GetWeakReference(srcFolder); + m_dstFolder = do_GetWeakReference(dstFolder); + m_srcKeyArray = srcKeyArray->Clone(); + m_dupKeyArray = srcKeyArray->Clone(); + nsCString uri; + nsresult rv = srcFolder->GetURI(uri); + nsCString protocolType(uri); + protocolType.SetLength(protocolType.FindChar(':')); + nsCOMPtr<nsIMsgDatabase> srcDB; + rv = srcFolder->GetMsgDatabase(getter_AddRefs(srcDB)); + NS_ENSURE_SUCCESS(rv, rv); + uint32_t i, count = m_srcKeyArray.Length(); + nsCOMPtr<nsIMsgDBHdr> srcHdr; + nsCOMPtr<nsIMsgDBHdr> copySrcHdr; + nsCString messageId; + + for (i = 0; i < count; i++) { + rv = srcDB->GetMsgHdrForKey(m_srcKeyArray[i], getter_AddRefs(srcHdr)); + if (NS_SUCCEEDED(rv)) { + // ** jt -- only do this for mailbox protocol + if (protocolType.LowerCaseEqualsLiteral("mailbox")) { + m_srcIsPop3 = true; + uint32_t msgSize; + rv = srcHdr->GetMessageSize(&msgSize); + if (NS_SUCCEEDED(rv)) m_srcSizeArray.AppendElement(msgSize); + if (isMove) { + rv = srcDB->CopyHdrFromExistingHdr(nsMsgKey_None, srcHdr, false, + getter_AddRefs(copySrcHdr)); + nsMsgKey pseudoKey = nsMsgKey_None; + if (NS_SUCCEEDED(rv)) { + copySrcHdr->GetMessageKey(&pseudoKey); + m_srcHdrs.AppendObject(copySrcHdr); + } + m_dupKeyArray[i] = pseudoKey; + } + } + srcHdr->GetMessageId(getter_Copies(messageId)); + m_srcMessageIds.AppendElement(messageId); + } + } + return nsMsgTxn::Init(); +} + +nsImapMoveCopyMsgTxn::~nsImapMoveCopyMsgTxn() {} + +NS_IMPL_ISUPPORTS_INHERITED(nsImapMoveCopyMsgTxn, nsMsgTxn, nsIUrlListener) + +NS_IMETHODIMP +nsImapMoveCopyMsgTxn::UndoTransaction(void) { + nsresult rv; + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + bool finishInOnStopRunningUrl = false; + + if (m_isMove || !m_dstFolder) { + if (m_srcIsPop3) { + rv = UndoMailboxDelete(); + NS_ENSURE_SUCCESS(rv, rv); + } else { + nsCOMPtr<nsIMsgFolder> srcFolder = do_QueryReferent(m_srcFolder, &rv); + if (NS_FAILED(rv) || !srcFolder) return rv; + nsCOMPtr<nsIUrlListener> srcListener = do_QueryInterface(srcFolder, &rv); + if (NS_FAILED(rv)) return rv; + m_onStopListener = do_GetWeakReference(srcListener); + + // ** make sure we are in the selected state; use lite select + // folder so we won't hit performance hard + nsCOMPtr<nsIURI> outUri; + rv = imapService->LiteSelectFolder(srcFolder, srcListener, nullptr, + getter_AddRefs(outUri)); + if (NS_FAILED(rv)) return rv; + bool deletedMsgs = true; // default is true unless imapDelete model + nsMsgImapDeleteModel deleteModel; + rv = GetImapDeleteModel(srcFolder, &deleteModel); + + // protect against a bogus undo txn without any source keys + // see bug #179856 for details + NS_ASSERTION(!m_srcKeyArray.IsEmpty(), "no source keys"); + if (m_srcKeyArray.IsEmpty()) return NS_ERROR_UNEXPECTED; + + if (!m_srcMsgIdString.IsEmpty()) { + if (NS_SUCCEEDED(rv) && + deleteModel == nsMsgImapDeleteModels::IMAPDelete) + CheckForToggleDelete(srcFolder, m_srcKeyArray[0], &deletedMsgs); + + if (deletedMsgs) + rv = imapService->SubtractMessageFlags( + srcFolder, this, m_srcMsgIdString, kImapMsgDeletedFlag, + m_idsAreUids); + else + rv = imapService->AddMessageFlags(srcFolder, srcListener, + m_srcMsgIdString, + kImapMsgDeletedFlag, m_idsAreUids); + if (NS_FAILED(rv)) return rv; + + finishInOnStopRunningUrl = true; + if (deleteModel != nsMsgImapDeleteModels::IMAPDelete) + nsCOMPtr<nsIURI> outUri; + rv = imapService->GetHeaders(srcFolder, srcListener, + getter_AddRefs(outUri), m_srcMsgIdString, + true); + } + } + } + if (!finishInOnStopRunningUrl && !m_dstMsgIdString.IsEmpty()) { + nsCOMPtr<nsIMsgFolder> dstFolder = do_QueryReferent(m_dstFolder, &rv); + if (NS_FAILED(rv) || !dstFolder) return rv; + + nsCOMPtr<nsIUrlListener> dstListener; + + dstListener = do_QueryInterface(dstFolder, &rv); + NS_ENSURE_SUCCESS(rv, rv); + // ** make sure we are in the selected state; use lite select folder + // so we won't potentially download a bunch of headers. + nsCOMPtr<nsIURI> outUri; + rv = imapService->LiteSelectFolder(dstFolder, dstListener, nullptr, + getter_AddRefs(outUri)); + NS_ENSURE_SUCCESS(rv, rv); + rv = imapService->AddMessageFlags(dstFolder, dstListener, m_dstMsgIdString, + kImapMsgDeletedFlag, m_idsAreUids); + } + return rv; +} + +NS_IMETHODIMP +nsImapMoveCopyMsgTxn::RedoTransaction(void) { + nsresult rv; + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + if (m_isMove || !m_dstFolder) { + if (m_srcIsPop3) { + rv = RedoMailboxDelete(); + if (NS_FAILED(rv)) return rv; + } else if (!m_srcMsgIdString.IsEmpty()) { + nsCOMPtr<nsIMsgFolder> srcFolder = do_QueryReferent(m_srcFolder, &rv); + if (NS_FAILED(rv) || !srcFolder) return rv; + nsCOMPtr<nsIUrlListener> srcListener = do_QueryInterface(srcFolder, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + bool deletedMsgs = false; // default will be false unless + // imapDeleteModel; + nsMsgImapDeleteModel deleteModel; + rv = GetImapDeleteModel(srcFolder, &deleteModel); + + // protect against a bogus undo txn without any source keys + // see bug #179856 for details + NS_ASSERTION(!m_srcKeyArray.IsEmpty(), "no source keys"); + if (m_srcKeyArray.IsEmpty()) return NS_ERROR_UNEXPECTED; + + if (NS_SUCCEEDED(rv) && deleteModel == nsMsgImapDeleteModels::IMAPDelete) + rv = CheckForToggleDelete(srcFolder, m_srcKeyArray[0], &deletedMsgs); + + // Make sure we are in the selected state; use lite select + // folder so performance won't suffer. + nsCOMPtr<nsIURI> outUri; + rv = imapService->LiteSelectFolder(srcFolder, srcListener, nullptr, + getter_AddRefs(outUri)); + NS_ENSURE_SUCCESS(rv, rv); + if (deletedMsgs) { + rv = imapService->SubtractMessageFlags( + srcFolder, srcListener, m_srcMsgIdString, kImapMsgDeletedFlag, + m_idsAreUids); + } else { + rv = imapService->AddMessageFlags(srcFolder, srcListener, + m_srcMsgIdString, kImapMsgDeletedFlag, + m_idsAreUids); + } + } + } + if (!m_dstMsgIdString.IsEmpty()) { + nsCOMPtr<nsIMsgFolder> dstFolder = do_QueryReferent(m_dstFolder, &rv); + if (NS_FAILED(rv) || !dstFolder) return rv; + + nsCOMPtr<nsIUrlListener> dstListener; + + dstListener = do_QueryInterface(dstFolder, &rv); + NS_ENSURE_SUCCESS(rv, rv); + // ** make sure we are in the selected state; use lite select + // folder so we won't hit performance hard + nsCOMPtr<nsIURI> outUri; + rv = imapService->LiteSelectFolder(dstFolder, dstListener, nullptr, + getter_AddRefs(outUri)); + NS_ENSURE_SUCCESS(rv, rv); + rv = imapService->SubtractMessageFlags(dstFolder, dstListener, + m_dstMsgIdString, + kImapMsgDeletedFlag, m_idsAreUids); + NS_ENSURE_SUCCESS(rv, rv); + nsMsgImapDeleteModel deleteModel; + rv = GetImapDeleteModel(dstFolder, &deleteModel); + if (NS_FAILED(rv) || deleteModel == nsMsgImapDeleteModels::MoveToTrash) { + rv = imapService->GetHeaders(dstFolder, dstListener, nullptr, + m_dstMsgIdString, true); + } + } + return rv; +} + +nsresult nsImapMoveCopyMsgTxn::SetCopyResponseUid(const char* aMsgIdString) { + if (!aMsgIdString) return NS_ERROR_NULL_POINTER; + m_dstMsgIdString = aMsgIdString; + if (m_dstMsgIdString.Last() == ']') { + int32_t len = m_dstMsgIdString.Length(); + m_dstMsgIdString.SetLength(len - 1); + } + return NS_OK; +} + +nsresult nsImapMoveCopyMsgTxn::GetSrcKeyArray(nsTArray<nsMsgKey>& srcKeyArray) { + srcKeyArray = m_srcKeyArray.Clone(); + return NS_OK; +} + +nsresult nsImapMoveCopyMsgTxn::AddDstKey(nsMsgKey aKey) { + if (!m_dstMsgIdString.IsEmpty()) m_dstMsgIdString.Append(','); + m_dstMsgIdString.AppendInt((int32_t)aKey); + return NS_OK; +} + +nsresult nsImapMoveCopyMsgTxn::UndoMailboxDelete() { + nsresult rv = NS_ERROR_FAILURE; + // ** jt -- only do this for mailbox protocol + if (m_srcIsPop3) { + nsCOMPtr<nsIMsgFolder> srcFolder = do_QueryReferent(m_srcFolder, &rv); + if (NS_FAILED(rv) || !srcFolder) return rv; + + nsCOMPtr<nsIMsgFolder> dstFolder = do_QueryReferent(m_dstFolder, &rv); + if (NS_FAILED(rv) || !dstFolder) return rv; + + nsCOMPtr<nsIMsgDatabase> srcDB; + nsCOMPtr<nsIMsgDatabase> dstDB; + rv = srcFolder->GetMsgDatabase(getter_AddRefs(srcDB)); + if (NS_FAILED(rv)) return rv; + rv = dstFolder->GetMsgDatabase(getter_AddRefs(dstDB)); + if (NS_FAILED(rv)) return rv; + + uint32_t count = m_srcKeyArray.Length(); + uint32_t i; + nsCOMPtr<nsIMsgDBHdr> oldHdr; + nsCOMPtr<nsIMsgDBHdr> newHdr; + for (i = 0; i < count; i++) { + oldHdr = m_srcHdrs[i]; + NS_ASSERTION(oldHdr, "fatal ... cannot get old msg header"); + rv = srcDB->CopyHdrFromExistingHdr(m_srcKeyArray[i], oldHdr, true, + getter_AddRefs(newHdr)); + NS_ASSERTION(newHdr, "fatal ... cannot create new header"); + + if (NS_SUCCEEDED(rv) && newHdr) { + if (i < m_srcSizeArray.Length()) + newHdr->SetMessageSize(m_srcSizeArray[i]); + srcDB->UndoDelete(newHdr); + } + } + srcDB->SetSummaryValid(true); + return NS_OK; // always return NS_OK + } + return NS_ERROR_FAILURE; +} + +nsresult nsImapMoveCopyMsgTxn::RedoMailboxDelete() { + nsresult rv = NS_ERROR_FAILURE; + if (m_srcIsPop3) { + nsCOMPtr<nsIMsgDatabase> srcDB; + nsCOMPtr<nsIMsgFolder> srcFolder = do_QueryReferent(m_srcFolder, &rv); + if (NS_FAILED(rv) || !srcFolder) return rv; + rv = srcFolder->GetMsgDatabase(getter_AddRefs(srcDB)); + if (NS_SUCCEEDED(rv)) { + srcDB->DeleteMessages(m_srcKeyArray, nullptr); + srcDB->SetSummaryValid(true); + } + return NS_OK; // always return NS_OK + } + return NS_ERROR_FAILURE; +} + +nsresult nsImapMoveCopyMsgTxn::GetImapDeleteModel( + nsIMsgFolder* aFolder, nsMsgImapDeleteModel* aDeleteModel) { + nsresult rv; + nsCOMPtr<nsIMsgIncomingServer> server; + if (!aFolder) return NS_ERROR_NULL_POINTER; + rv = aFolder->GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIImapIncomingServer> imapServer = do_QueryInterface(server, &rv); + if (NS_SUCCEEDED(rv) && imapServer) + rv = imapServer->GetDeleteModel(aDeleteModel); + return rv; +} + +NS_IMETHODIMP nsImapMoveCopyMsgTxn::OnStartRunningUrl(nsIURI* aUrl) { + return NS_OK; +} + +NS_IMETHODIMP nsImapMoveCopyMsgTxn::OnStopRunningUrl(nsIURI* aUrl, + nsresult aExitCode) { + nsCOMPtr<nsIUrlListener> urlListener = do_QueryReferent(m_onStopListener); + if (urlListener) urlListener->OnStopRunningUrl(aUrl, aExitCode); + + nsCOMPtr<nsIImapUrl> imapUrl = do_QueryInterface(aUrl); + if (imapUrl) { + nsresult rv; + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsImapAction imapAction; + imapUrl->GetImapAction(&imapAction); + nsCOMPtr<nsIMsgFolder> dstFolder = do_QueryReferent(m_dstFolder, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgFolder> srcFolder = do_QueryReferent(m_srcFolder, &rv); + NS_ENSURE_SUCCESS(rv, rv); + if (imapAction == nsIImapUrl::nsImapSubtractMsgFlags) { + int32_t extraStatus; + imapUrl->GetExtraStatus(&extraStatus); + if (extraStatus != nsIImapUrl::ImapStatusNone) { + // If subtracting the deleted flag didn't work, try + // moving the message back from the target folder to the src folder + if (!m_dstMsgIdString.IsEmpty()) + imapService->OnlineMessageCopy(dstFolder, m_dstMsgIdString, srcFolder, + true, true, nullptr, /* listener */ + nullptr, nullptr, nullptr); + else { + // server doesn't support COPYUID, so we're going to update the dest + // folder, and when that's done, use the db to find the messages + // to move back, looking them up by message-id. + nsCOMPtr<nsIMsgImapMailFolder> imapDest = + do_QueryInterface(dstFolder); + if (imapDest) imapDest->UpdateFolderWithListener(nullptr, this); + } + } else if (!m_dstMsgIdString.IsEmpty()) { + nsCOMPtr<nsIUrlListener> dstListener; + + dstListener = do_QueryInterface(dstFolder, &rv); + NS_ENSURE_SUCCESS(rv, rv); + // ** make sure we are in the selected state; use lite select folder + // so we won't potentially download a bunch of headers. + nsCOMPtr<nsIURI> outUri; + rv = imapService->LiteSelectFolder(dstFolder, dstListener, nullptr, + getter_AddRefs(outUri)); + NS_ENSURE_SUCCESS(rv, rv); + rv = imapService->AddMessageFlags(dstFolder, dstListener, + m_dstMsgIdString, kImapMsgDeletedFlag, + m_idsAreUids); + } + } else if (imapAction == nsIImapUrl::nsImapSelectFolder) { + // Now we should have the headers from the dest folder. + // Look them up and move them back to the source folder. + uint32_t count = m_srcMessageIds.Length(); + uint32_t i; + nsCString messageId; + nsTArray<nsMsgKey> dstKeys; + nsCOMPtr<nsIMsgDatabase> destDB; + nsCOMPtr<nsIMsgDBHdr> dstHdr; + + rv = dstFolder->GetMsgDatabase(getter_AddRefs(destDB)); + NS_ENSURE_SUCCESS(rv, rv); + for (i = 0; i < count; i++) { + rv = destDB->GetMsgHdrForMessageID(m_srcMessageIds[i].get(), + getter_AddRefs(dstHdr)); + if (NS_SUCCEEDED(rv) && dstHdr) { + nsMsgKey dstKey; + dstHdr->GetMessageKey(&dstKey); + dstKeys.AppendElement(dstKey); + } + } + if (dstKeys.Length()) { + nsAutoCString uids; + nsImapMailFolder::AllocateUidStringFromKeys(dstKeys, uids); + rv = imapService->OnlineMessageCopy(dstFolder, uids, srcFolder, true, + true, nullptr, nullptr, nullptr, + nullptr); + } + } + } + return NS_OK; +} + +nsImapOfflineTxn::nsImapOfflineTxn(nsIMsgFolder* srcFolder, + nsTArray<nsMsgKey>* srcKeyArray, + const char* srcMsgIdString, + nsIMsgFolder* dstFolder, bool isMove, + nsOfflineImapOperationType opType, + nsCOMArray<nsIMsgDBHdr>& srcHdrs) { + Init(srcFolder, srcKeyArray, srcMsgIdString, dstFolder, true, isMove); + + m_opType = opType; + m_flags = 0; + m_addFlags = false; + if (opType == nsIMsgOfflineImapOperation::kDeletedMsg) { + nsCOMPtr<nsIMsgDatabase> srcDB; + nsCOMPtr<nsIDBFolderInfo> folderInfo; + + nsresult rv = srcFolder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), + getter_AddRefs(srcDB)); + if (NS_SUCCEEDED(rv) && srcDB) { + nsMsgKey pseudoKey; + nsCOMPtr<nsIMsgDBHdr> copySrcHdr; + + // Imap protocols have conflated key/UUID so we cannot use + // auto key with them. + nsCString protocolType; + srcFolder->GetURI(protocolType); + protocolType.SetLength(protocolType.FindChar(':')); + for (int32_t i = 0; i < srcHdrs.Count(); i++) { + if (protocolType.EqualsLiteral("imap")) { + srcDB->GetNextPseudoMsgKey(&pseudoKey); + pseudoKey--; + } else { + pseudoKey = nsMsgKey_None; + } + rv = srcDB->CopyHdrFromExistingHdr(pseudoKey, srcHdrs[i], false, + getter_AddRefs(copySrcHdr)); + if (NS_SUCCEEDED(rv)) { + copySrcHdr->GetMessageKey(&pseudoKey); + m_srcHdrs.AppendObject(copySrcHdr); + } + m_dupKeyArray[i] = pseudoKey; + } + } + } else + m_srcHdrs.AppendObjects(srcHdrs); +} + +nsImapOfflineTxn::~nsImapOfflineTxn() {} + +// Open the database and find the key for the offline operation that we want to +// undo, then remove it from the database, we also hold on to this +// data for a redo operation. +NS_IMETHODIMP nsImapOfflineTxn::UndoTransaction(void) { + nsresult rv; + + nsCOMPtr<nsIMsgFolder> srcFolder = do_QueryReferent(m_srcFolder, &rv); + if (NS_FAILED(rv) || !srcFolder) return rv; + nsCOMPtr<nsIMsgOfflineImapOperation> op; + nsCOMPtr<nsIDBFolderInfo> folderInfo; + nsCOMPtr<nsIMsgDatabase> srcDB; + nsCOMPtr<nsIMsgDatabase> destDB; + + rv = srcFolder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), + getter_AddRefs(srcDB)); + NS_ENSURE_SUCCESS(rv, rv); + + switch (m_opType) { + case nsIMsgOfflineImapOperation::kMsgMoved: + case nsIMsgOfflineImapOperation::kMsgCopy: + case nsIMsgOfflineImapOperation::kAddedHeader: + case nsIMsgOfflineImapOperation::kFlagsChanged: + case nsIMsgOfflineImapOperation::kDeletedMsg: { + if (m_srcHdrs.IsEmpty()) { + NS_ASSERTION(false, "No msg header to apply undo."); + break; + } + nsCOMPtr<nsIMsgDBHdr> firstHdr = m_srcHdrs[0]; + nsMsgKey hdrKey; + firstHdr->GetMessageKey(&hdrKey); + nsCOMPtr<nsIMsgOfflineOpsDatabase> opsDb = do_QueryInterface(srcDB, &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = opsDb->GetOfflineOpForKey(hdrKey, false, getter_AddRefs(op)); + bool offlineOpPlayedBack = true; + if (NS_SUCCEEDED(rv) && op) { + op->GetPlayingBack(&offlineOpPlayedBack); + opsDb->RemoveOfflineOp(op); + op = nullptr; + } + if (!WeAreOffline() && offlineOpPlayedBack) { + // couldn't find offline op - it must have been played back already + // so we should undo the transaction online. + return nsImapMoveCopyMsgTxn::UndoTransaction(); + } + + if (!firstHdr) break; + nsMsgKey msgKey; + if (m_opType == nsIMsgOfflineImapOperation::kAddedHeader) { + for (int32_t i = 0; i < m_srcHdrs.Count(); i++) { + m_srcHdrs[i]->GetMessageKey(&msgKey); + nsCOMPtr<nsIMsgDBHdr> mailHdr; + rv = srcDB->GetMsgHdrForKey(msgKey, getter_AddRefs(mailHdr)); + if (mailHdr) srcDB->DeleteHeader(mailHdr, nullptr, false, false); + } + srcDB->Commit(true); + } else if (m_opType == nsIMsgOfflineImapOperation::kDeletedMsg) { + for (int32_t i = 0; i < m_srcHdrs.Count(); i++) { + nsCOMPtr<nsIMsgDBHdr> undeletedHdr = m_srcHdrs[i]; + m_srcHdrs[i]->GetMessageKey(&msgKey); + if (undeletedHdr) { + nsCOMPtr<nsIMsgDBHdr> newHdr; + srcDB->CopyHdrFromExistingHdr(msgKey, undeletedHdr, true, + getter_AddRefs(newHdr)); + } + } + srcDB->Close(true); + srcFolder->SummaryChanged(); + } + break; + } + case nsIMsgOfflineImapOperation::kMsgMarkedDeleted: { + nsMsgKey msgKey; + for (int32_t i = 0; i < m_srcHdrs.Count(); i++) { + m_srcHdrs[i]->GetMessageKey(&msgKey); + srcDB->MarkImapDeleted(msgKey, false, nullptr); + } + } break; + default: + break; + } + srcDB->Close(true); + srcFolder->SummaryChanged(); + return NS_OK; +} + +NS_IMETHODIMP nsImapOfflineTxn::RedoTransaction(void) { + nsresult rv; + + nsCOMPtr<nsIMsgFolder> srcFolder = do_QueryReferent(m_srcFolder, &rv); + if (NS_FAILED(rv) || !srcFolder) return rv; + nsCOMPtr<nsIMsgOfflineImapOperation> op; + nsCOMPtr<nsIDBFolderInfo> folderInfo; + nsCOMPtr<nsIMsgDatabase> srcDB; + nsCOMPtr<nsIMsgDatabase> destDB; + rv = srcFolder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), + getter_AddRefs(srcDB)); + NS_ENSURE_SUCCESS(rv, rv); + + switch (m_opType) { + case nsIMsgOfflineImapOperation::kMsgMoved: + case nsIMsgOfflineImapOperation::kMsgCopy: { + nsCOMPtr<nsIMsgOfflineOpsDatabase> opsDb = do_QueryInterface(srcDB, &rv); + NS_ENSURE_SUCCESS(rv, rv); + for (int32_t i = 0; i < m_srcHdrs.Count(); i++) { + nsMsgKey hdrKey; + m_srcHdrs[i]->GetMessageKey(&hdrKey); + rv = opsDb->GetOfflineOpForKey(hdrKey, false, getter_AddRefs(op)); + if (NS_SUCCEEDED(rv) && op) { + nsCOMPtr<nsIMsgFolder> dstFolder = do_QueryReferent(m_dstFolder, &rv); + if (dstFolder) { + nsCString folderURI; + dstFolder->GetURI(folderURI); + + if (m_opType == nsIMsgOfflineImapOperation::kMsgMoved) + op->SetDestinationFolderURI(folderURI); // offline move + if (m_opType == nsIMsgOfflineImapOperation::kMsgCopy) { + op->SetOperation(nsIMsgOfflineImapOperation::kMsgMoved); + op->AddMessageCopyOperation(folderURI); // offline copy + } + dstFolder->SummaryChanged(); + } + } else if (!WeAreOffline()) { + // couldn't find offline op - it must have been played back already + // so we should redo the transaction online. + return nsImapMoveCopyMsgTxn::RedoTransaction(); + } + } + break; + } + case nsIMsgOfflineImapOperation::kAddedHeader: { + nsCOMPtr<nsIMsgFolder> dstFolder = do_QueryReferent(m_dstFolder, &rv); + rv = srcFolder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), + getter_AddRefs(destDB)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgOfflineOpsDatabase> opsDb = do_QueryInterface(destDB, &rv); + NS_ENSURE_SUCCESS(rv, rv); + for (int32_t i = 0; i < m_srcHdrs.Count(); i++) { + nsCOMPtr<nsIMsgDBHdr> restoreHdr; + nsMsgKey msgKey; + m_srcHdrs[i]->GetMessageKey(&msgKey); + destDB->CopyHdrFromExistingHdr(msgKey, m_srcHdrs[i], true, + getter_AddRefs(restoreHdr)); + rv = opsDb->GetOfflineOpForKey(msgKey, true, getter_AddRefs(op)); + if (NS_SUCCEEDED(rv) && op) { + nsCString folderURI; + srcFolder->GetURI(folderURI); + op->SetSourceFolderURI(folderURI); + } + } + dstFolder->SummaryChanged(); + destDB->Close(true); + } break; + case nsIMsgOfflineImapOperation::kDeletedMsg: + for (int32_t i = 0; i < m_srcHdrs.Count(); i++) { + nsMsgKey msgKey; + m_srcHdrs[i]->GetMessageKey(&msgKey); + srcDB->DeleteMessage(msgKey, nullptr, true); + } + break; + case nsIMsgOfflineImapOperation::kMsgMarkedDeleted: + for (int32_t i = 0; i < m_srcHdrs.Count(); i++) { + nsMsgKey msgKey; + m_srcHdrs[i]->GetMessageKey(&msgKey); + srcDB->MarkImapDeleted(msgKey, true, nullptr); + } + break; + case nsIMsgOfflineImapOperation::kFlagsChanged: { + nsCOMPtr<nsIMsgOfflineOpsDatabase> opsDb = do_QueryInterface(srcDB, &rv); + NS_ENSURE_SUCCESS(rv, rv); + for (int32_t i = 0; i < m_srcHdrs.Count(); i++) { + nsMsgKey msgKey; + m_srcHdrs[i]->GetMessageKey(&msgKey); + rv = opsDb->GetOfflineOpForKey(msgKey, true, getter_AddRefs(op)); + if (NS_SUCCEEDED(rv) && op) { + imapMessageFlagsType newMsgFlags; + op->GetNewFlags(&newMsgFlags); + if (m_addFlags) + op->SetFlagOperation(newMsgFlags | m_flags); + else + op->SetFlagOperation(newMsgFlags & ~m_flags); + } + } + break; + } + default: + break; + } + srcDB->Close(true); + srcDB = nullptr; + srcFolder->SummaryChanged(); + return NS_OK; +} diff --git a/comm/mailnews/imap/src/nsImapUndoTxn.h b/comm/mailnews/imap/src/nsImapUndoTxn.h new file mode 100644 index 0000000000..9b0f2b30f9 --- /dev/null +++ b/comm/mailnews/imap/src/nsImapUndoTxn.h @@ -0,0 +1,87 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsImapUndoTxn_h__ +#define nsImapUndoTxn_h__ + +#include "mozilla/Attributes.h" +#include "nsIMsgFolder.h" +#include "nsIImapIncomingServer.h" +#include "nsIUrlListener.h" +#include "nsMsgTxn.h" +#include "MailNewsTypes.h" +#include "nsTArray.h" +#include "nsIMsgOfflineImapOperation.h" +#include "nsCOMPtr.h" +#include "nsIWeakReferenceUtils.h" +#include "nsCOMArray.h" + +class nsImapMoveCopyMsgTxn : public nsMsgTxn, nsIUrlListener { + public: + nsImapMoveCopyMsgTxn(); + nsImapMoveCopyMsgTxn(nsIMsgFolder* srcFolder, nsTArray<nsMsgKey>* srcKeyArray, + const char* srcMsgIdString, nsIMsgFolder* dstFolder, + bool isMove); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIURLLISTENER + + NS_IMETHOD UndoTransaction(void) override; + NS_IMETHOD RedoTransaction(void) override; + + // helper + nsresult SetCopyResponseUid(const char* msgIdString); + nsresult GetSrcKeyArray(nsTArray<nsMsgKey>& srcKeyArray); + void GetSrcMsgIds(nsCString& srcMsgIds) { srcMsgIds = m_srcMsgIdString; } + nsresult AddDstKey(nsMsgKey aKey); + nsresult UndoMailboxDelete(); + nsresult RedoMailboxDelete(); + nsresult Init(nsIMsgFolder* srcFolder, nsTArray<nsMsgKey>* srcKeyArray, + const char* srcMsgIdString, nsIMsgFolder* dstFolder, + bool idsAreUids, bool isMove); + + protected: + virtual ~nsImapMoveCopyMsgTxn(); + + nsWeakPtr m_srcFolder; + nsCOMArray<nsIMsgDBHdr> m_srcHdrs; + nsTArray<nsMsgKey> m_dupKeyArray; + nsTArray<nsMsgKey> m_srcKeyArray; + nsTArray<nsCString> m_srcMessageIds; + nsCString m_srcMsgIdString; + nsWeakPtr m_dstFolder; + nsCString m_dstMsgIdString; + bool m_idsAreUids; + bool m_isMove; + bool m_srcIsPop3; + nsTArray<uint32_t> m_srcSizeArray; + // this is used when we chain urls for imap undo, since "this" needs + // to be the listener, but the folder may need to also be notified. + nsWeakPtr m_onStopListener; + + nsresult GetImapDeleteModel(nsIMsgFolder* aFolder, + nsMsgImapDeleteModel* aDeleteModel); +}; + +class nsImapOfflineTxn : public nsImapMoveCopyMsgTxn { + public: + nsImapOfflineTxn(nsIMsgFolder* srcFolder, nsTArray<nsMsgKey>* srcKeyArray, + const char* srcMsgIdString, nsIMsgFolder* dstFolder, + bool isMove, nsOfflineImapOperationType opType, + nsCOMArray<nsIMsgDBHdr>& srcHdrs); + + NS_IMETHOD UndoTransaction(void) override; + NS_IMETHOD RedoTransaction(void) override; + void SetAddFlags(bool addFlags) { m_addFlags = addFlags; } + void SetFlags(uint32_t flags) { m_flags = flags; } + + protected: + virtual ~nsImapOfflineTxn(); + nsOfflineImapOperationType m_opType; + // these two are used to undo flag changes, which we don't currently do. + bool m_addFlags; + uint32_t m_flags; +}; +#endif diff --git a/comm/mailnews/imap/src/nsImapUrl.cpp b/comm/mailnews/imap/src/nsImapUrl.cpp new file mode 100644 index 0000000000..4460b5ab05 --- /dev/null +++ b/comm/mailnews/imap/src/nsImapUrl.cpp @@ -0,0 +1,1276 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" // precompiled header... + +#include "nsImapUrl.h" +#include "../public/nsIImapHostSessionList.h" +#include "nsThreadUtils.h" +#include "nsString.h" +#include "prmem.h" +#include "plstr.h" +#include "prprf.h" +#include "nsMemory.h" +#include "nsCOMPtr.h" +#include "nsImapUtils.h" +#include "nsIImapMockChannel.h" +#include "nsIImapMailFolderSink.h" +#include "nsIImapMessageSink.h" +#include "nsIImapServerSink.h" +#include "nsImapNamespace.h" +#include "nsICacheEntry.h" +#include "nsIMsgFolder.h" +#include "nsMsgUtils.h" +#include "nsIMsgHdr.h" +#include "nsServiceManagerUtils.h" +#include "mozilla/Logging.h" + +using namespace mozilla; +extern LazyLogModule IMAPCache; // defined in nsImapProtocol.cpp + +#define NS_IIMAPHOSTSESSIONLIST_CID \ + { \ + 0x479ce8fc, 0xe725, 0x11d2, { \ + 0xa5, 0x05, 0x00, 0x60, 0xb0, 0xfc, 0x04, 0xb7 \ + } \ + } +static NS_DEFINE_CID(kCImapHostSessionListCID, NS_IIMAPHOSTSESSIONLIST_CID); + +nsImapUrl::nsImapUrl() : mLock("nsImapUrl.mLock") { + m_listOfMessageIds = nullptr; + m_sourceCanonicalFolderPathSubString = nullptr; + m_destinationCanonicalFolderPathSubString = nullptr; + m_listOfMessageIds = nullptr; + m_tokenPlaceHolder = nullptr; + m_urlidSubString = nullptr; + m_searchCriteriaString = nullptr; + m_idsAreUids = false; + m_mimePartSelectorDetected = false; + m_msgLoadingFromCache = false; + m_storeResultsOffline = false; + m_storeOfflineOnFallback = false; + m_localFetchOnly = false; + m_rerunningUrl = false; + m_moreHeadersToDownload = false; + m_externalLinkUrl = true; // we'll start this at true, and set it false in + // nsImapService::CreateStartOfImapUrl + m_numBytesToFetch = 0; + m_validUrl = true; // assume the best. + m_runningUrl = false; + m_flags = 0; + m_extraStatus = ImapStatusNone; + m_onlineSubDirSeparator = '/'; + m_imapAction = 0; + mAutodetectCharset = false; + + // ** jt - the following are not ref counted + m_copyState = nullptr; + m_file = nullptr; + m_imapMailFolderSink = nullptr; + m_imapMessageSink = nullptr; + m_addDummyEnvelope = false; + m_canonicalLineEnding = false; +} + +nsImapUrl::~nsImapUrl() { + PR_FREEIF(m_listOfMessageIds); + PR_FREEIF(m_destinationCanonicalFolderPathSubString); + PR_FREEIF(m_sourceCanonicalFolderPathSubString); + PR_FREEIF(m_searchCriteriaString); +} + +NS_IMPL_ADDREF_INHERITED(nsImapUrl, nsMsgMailNewsUrl) + +NS_IMPL_RELEASE_INHERITED(nsImapUrl, nsMsgMailNewsUrl) + +NS_INTERFACE_MAP_BEGIN(nsImapUrl) + NS_INTERFACE_MAP_ENTRY(nsIImapUrl) + NS_INTERFACE_MAP_ENTRY(nsIMsgMessageUrl) + NS_INTERFACE_MAP_ENTRY(nsIMsgI18NUrl) +NS_INTERFACE_MAP_END_INHERITING(nsMsgMailNewsUrl) + +//////////////////////////////////////////////////////////////////////////////////// +// Begin nsIImapUrl specific support +//////////////////////////////////////////////////////////////////////////////////// + +NS_IMETHODIMP nsImapUrl::GetRequiredImapState(nsImapState* aImapUrlState) { + if (aImapUrlState) { + // the imap action determines the state we must be in...check the + // the imap action. + + if (m_imapAction & 0x10000000) + *aImapUrlState = nsImapSelectedState; + else + *aImapUrlState = nsImapAuthenticatedState; + } + + return NS_OK; +} + +NS_IMETHODIMP nsImapUrl::GetImapAction(nsImapAction* aImapAction) { + *aImapAction = m_imapAction; + return NS_OK; +} + +NS_IMETHODIMP nsImapUrl::SetImapAction(nsImapAction aImapAction) { + m_imapAction = aImapAction; + return NS_OK; +} + +NS_IMETHODIMP nsImapUrl::GetFolder(nsIMsgFolder** aMsgFolder) { + NS_ENSURE_ARG_POINTER(aMsgFolder); + NS_ENSURE_ARG_POINTER(m_imapFolder); + + nsCOMPtr<nsIMsgFolder> folder = do_QueryReferent(m_imapFolder); + folder.forget(aMsgFolder); + return NS_OK; +} + +NS_IMETHODIMP nsImapUrl::SetFolder(nsIMsgFolder* aMsgFolder) { + nsresult rv; + m_imapFolder = do_GetWeakReference(aMsgFolder, &rv); + if (aMsgFolder) { + nsCOMPtr<nsIMsgIncomingServer> incomingServer; + aMsgFolder->GetServer(getter_AddRefs(incomingServer)); + if (incomingServer) incomingServer->GetKey(m_serverKey); + } + return rv; +} + +NS_IMETHODIMP nsImapUrl::GetImapMailFolderSink( + nsIImapMailFolderSink** aImapMailFolderSink) { + NS_ENSURE_ARG_POINTER(aImapMailFolderSink); + if (!m_imapMailFolderSink) + return NS_ERROR_NULL_POINTER; // no assert, so don't use NS_ENSURE_POINTER. + + nsCOMPtr<nsIImapMailFolderSink> folderSink = + do_QueryReferent(m_imapMailFolderSink); + folderSink.forget(aImapMailFolderSink); + return NS_OK; +} + +NS_IMETHODIMP nsImapUrl::SetImapMailFolderSink( + nsIImapMailFolderSink* aImapMailFolderSink) { + nsresult rv; + m_imapMailFolderSink = do_GetWeakReference(aImapMailFolderSink, &rv); + return rv; +} + +NS_IMETHODIMP nsImapUrl::GetImapMessageSink( + nsIImapMessageSink** aImapMessageSink) { + NS_ENSURE_ARG_POINTER(aImapMessageSink); + NS_ENSURE_ARG_POINTER(m_imapMessageSink); + + nsCOMPtr<nsIImapMessageSink> messageSink = + do_QueryReferent(m_imapMessageSink); + messageSink.forget(aImapMessageSink); + return NS_OK; +} + +NS_IMETHODIMP nsImapUrl::SetImapMessageSink( + nsIImapMessageSink* aImapMessageSink) { + nsresult rv; + m_imapMessageSink = do_GetWeakReference(aImapMessageSink, &rv); + return rv; +} + +NS_IMETHODIMP nsImapUrl::GetImapServerSink( + nsIImapServerSink** aImapServerSink) { + NS_ENSURE_ARG_POINTER(aImapServerSink); + NS_ENSURE_ARG_POINTER(m_imapServerSink); + + nsCOMPtr<nsIImapServerSink> serverSink = do_QueryReferent(m_imapServerSink); + serverSink.forget(aImapServerSink); + return NS_OK; +} + +NS_IMETHODIMP nsImapUrl::SetImapServerSink(nsIImapServerSink* aImapServerSink) { + nsresult rv; + m_imapServerSink = do_GetWeakReference(aImapServerSink, &rv); + return rv; +} + +//////////////////////////////////////////////////////////////////////////////////// +// End nsIImapUrl specific support +//////////////////////////////////////////////////////////////////////////////////// + +nsresult nsImapUrl::SetSpecInternal(const nsACString& aSpec) { + nsresult rv = nsMsgMailNewsUrl::SetSpecInternal(aSpec); + if (NS_SUCCEEDED(rv)) { + m_validUrl = true; // assume the best. + rv = ParseUrl(); + } + return rv; +} + +nsresult nsImapUrl::SetQuery(const nsACString& aQuery) { + nsresult rv = nsMsgMailNewsUrl::SetQuery(aQuery); + if (NS_SUCCEEDED(rv)) rv = ParseUrl(); + return rv; +} + +nsresult nsImapUrl::ParseUrl() { + nsresult rv = NS_OK; + // extract the user name + GetUserPass(m_userName); + + nsAutoCString imapPartOfUrl; + rv = GetPathQueryRef(imapPartOfUrl); + nsAutoCString unescapedImapPartOfUrl; + MsgUnescapeString(imapPartOfUrl, 0, unescapedImapPartOfUrl); + if (NS_SUCCEEDED(rv) && !unescapedImapPartOfUrl.IsEmpty()) { + ParseImapPart(unescapedImapPartOfUrl.BeginWriting() + + 1); // GetPath leaves leading '/' in the path!!! + } + + return NS_OK; +} + +NS_IMETHODIMP nsImapUrl::CreateSearchCriteriaString(char** aResult) { + // this method should only be called from the imap thread... + // o.t. add lock protection.. + if (nullptr == aResult || !m_searchCriteriaString) + return NS_ERROR_NULL_POINTER; + *aResult = strdup(m_searchCriteriaString); + return NS_OK; +} + +// this method gets called from the UI thread and the imap thread +NS_IMETHODIMP nsImapUrl::GetListOfMessageIds(nsACString& aResult) { + MutexAutoLock mon(mLock); + if (!m_listOfMessageIds) return NS_ERROR_NULL_POINTER; + + int32_t bytesToCopy = strlen(m_listOfMessageIds); + + // mime may have glommed a "&part=" for a part download + // we return the entire message and let mime extract + // the part. Pop and news work this way also. + // this algorithm truncates the "&part" string. + char* currentChar = m_listOfMessageIds; + while (*currentChar && (*currentChar != '?')) currentChar++; + if (*currentChar == '?') bytesToCopy = currentChar - m_listOfMessageIds; + + // we should also strip off anything after "/;section=" + // since that can specify an IMAP MIME part + char* wherePart = PL_strstr(m_listOfMessageIds, "/;section="); + if (wherePart) + bytesToCopy = + std::min(bytesToCopy, int32_t(wherePart - m_listOfMessageIds)); + + aResult.Assign(m_listOfMessageIds, bytesToCopy); + return NS_OK; +} + +NS_IMETHODIMP nsImapUrl::GetCommand(nsACString& result) { + result = m_command; + return NS_OK; +} + +NS_IMETHODIMP nsImapUrl::GetCustomAttributeToFetch(nsACString& result) { + result = m_msgFetchAttribute; + return NS_OK; +} + +NS_IMETHODIMP nsImapUrl::GetCustomAttributeResult(nsACString& result) { + result = m_customAttributeResult; + return NS_OK; +} + +NS_IMETHODIMP nsImapUrl::SetCustomAttributeResult(const nsACString& result) { + m_customAttributeResult = result; + return NS_OK; +} + +NS_IMETHODIMP nsImapUrl::GetCustomCommandResult(nsACString& result) { + result = m_customCommandResult; + return NS_OK; +} + +NS_IMETHODIMP nsImapUrl::SetCustomCommandResult(const nsACString& result) { + m_customCommandResult = result; + return NS_OK; +} + +NS_IMETHODIMP nsImapUrl::GetCustomAddFlags(nsACString& aResult) { + aResult = m_customAddFlags; + return NS_OK; +} + +NS_IMETHODIMP nsImapUrl::GetCustomSubtractFlags(nsACString& aResult) { + aResult = m_customSubtractFlags; + return NS_OK; +} + +NS_IMETHODIMP nsImapUrl::GetImapPartToFetch(char** result) { + // here's the old code.... + + // unfortunately an imap part can have the form: /;section= OR + // it can have the form ?section=. We need to look for both. + if (m_listOfMessageIds) { + char* wherepart = PL_strstr(m_listOfMessageIds, ";section="); + if (!wherepart) // look for ?section too.... + wherepart = PL_strstr(m_listOfMessageIds, "?section="); + if (wherepart) { + wherepart += 9; // strlen("/;section=") + char* wherelibmimepart = PL_strstr(wherepart, "&part="); + if (!wherelibmimepart) wherelibmimepart = PL_strstr(wherepart, "?part="); + int numCharsToCopy = (wherelibmimepart) + ? wherelibmimepart - wherepart + : PL_strlen(m_listOfMessageIds) - + (wherepart - m_listOfMessageIds); + if (numCharsToCopy) { + *result = (char*)PR_Malloc(sizeof(char) * (numCharsToCopy + 1)); + if (*result) { + PL_strncpy(*result, wherepart, numCharsToCopy + 1); // appends a \0 + (*result)[numCharsToCopy] = '\0'; + } + } + } // if we got a wherepart + } // if we got a m_listOfMessageIds + return NS_OK; +} + +NS_IMETHODIMP nsImapUrl::GetOnlineSubDirSeparator(char* separator) { + if (separator) { + *separator = m_onlineSubDirSeparator; + return NS_OK; + } + return NS_ERROR_NULL_POINTER; +} + +NS_IMETHODIMP nsImapUrl::GetNumBytesToFetch(int32_t* aNumBytesToFetch) { + NS_ENSURE_ARG_POINTER(aNumBytesToFetch); + *aNumBytesToFetch = m_numBytesToFetch; + return NS_OK; +} + +NS_IMETHODIMP +nsImapUrl::SetOnlineSubDirSeparator(char onlineDirSeparator) { + m_onlineSubDirSeparator = onlineDirSeparator; + return NS_OK; +} + +// this method is only called from the imap thread +NS_IMETHODIMP nsImapUrl::MessageIdsAreUids(bool* result) { + *result = m_idsAreUids; + return NS_OK; +} + +NS_IMETHODIMP +nsImapUrl::SetExtraStatus(int32_t aExtraStatus) { + m_extraStatus = aExtraStatus; + return NS_OK; +} + +NS_IMETHODIMP nsImapUrl::GetExtraStatus(int32_t* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + *aResult = m_extraStatus; + return NS_OK; +} + +// this method is only called from the imap thread +NS_IMETHODIMP nsImapUrl::GetMsgFlags( + imapMessageFlagsType* result) // kAddMsgFlags or kSubtractMsgFlags only +{ + *result = m_flags; + return NS_OK; +} + +void nsImapUrl::ParseImapPart(char* imapPartOfUrl) { + m_tokenPlaceHolder = imapPartOfUrl; + m_urlidSubString = m_tokenPlaceHolder ? NS_strtok(IMAP_URL_TOKEN_SEPARATOR, + &m_tokenPlaceHolder) + : (char*)NULL; + + if (!m_urlidSubString) { + m_validUrl = false; + return; + } + + if (!PL_strcasecmp(m_urlidSubString, "fetch")) { + m_imapAction = nsImapMsgFetch; + ParseUidChoice(); + PR_FREEIF(m_sourceCanonicalFolderPathSubString); + ParseFolderPath(&m_sourceCanonicalFolderPathSubString); + ParseListOfMessageIds(); + // if fetched by spam filter, the action will be changed to + // nsImapMsgFetchPeek + } else { + if (!PL_strcasecmp(m_urlidSubString, "header")) { + m_imapAction = nsImapMsgHeader; + ParseUidChoice(); + ParseFolderPath(&m_sourceCanonicalFolderPathSubString); + ParseListOfMessageIds(); + } else if (!PL_strcasecmp(m_urlidSubString, "customFetch")) { + ParseUidChoice(); + ParseFolderPath(&m_sourceCanonicalFolderPathSubString); + ParseListOfMessageIds(); + ParseCustomMsgFetchAttribute(); + } else if (!PL_strcasecmp(m_urlidSubString, "previewBody")) { + ParseUidChoice(); + ParseFolderPath(&m_sourceCanonicalFolderPathSubString); + ParseListOfMessageIds(); + ParseNumBytes(); + } else if (!PL_strcasecmp(m_urlidSubString, "deletemsg")) { + m_imapAction = nsImapDeleteMsg; + ParseUidChoice(); + ParseFolderPath(&m_sourceCanonicalFolderPathSubString); + ParseListOfMessageIds(); + } else if (!PL_strcasecmp(m_urlidSubString, "uidexpunge")) { + m_imapAction = nsImapUidExpunge; + ParseFolderPath(&m_sourceCanonicalFolderPathSubString); + ParseListOfMessageIds(); + } else if (!PL_strcasecmp(m_urlidSubString, "deleteallmsgs")) { + m_imapAction = nsImapDeleteAllMsgs; + ParseFolderPath(&m_sourceCanonicalFolderPathSubString); + } else if (!PL_strcasecmp(m_urlidSubString, "addmsgflags")) { + m_imapAction = nsImapAddMsgFlags; + ParseUidChoice(); + ParseFolderPath(&m_sourceCanonicalFolderPathSubString); + ParseListOfMessageIds(); + ParseMsgFlags(); + } else if (!PL_strcasecmp(m_urlidSubString, "subtractmsgflags")) { + m_imapAction = nsImapSubtractMsgFlags; + ParseUidChoice(); + ParseFolderPath(&m_sourceCanonicalFolderPathSubString); + ParseListOfMessageIds(); + ParseMsgFlags(); + } else if (!PL_strcasecmp(m_urlidSubString, "setmsgflags")) { + m_imapAction = nsImapSetMsgFlags; + ParseUidChoice(); + ParseFolderPath(&m_sourceCanonicalFolderPathSubString); + ParseListOfMessageIds(); + ParseMsgFlags(); + } else if (!PL_strcasecmp(m_urlidSubString, "onlinecopy")) { + m_imapAction = nsImapOnlineCopy; + ParseUidChoice(); + ParseFolderPath(&m_sourceCanonicalFolderPathSubString); + ParseListOfMessageIds(); + ParseFolderPath(&m_destinationCanonicalFolderPathSubString); + } else if (!PL_strcasecmp(m_urlidSubString, "onlinemove")) { + m_imapAction = nsImapOnlineMove; + ParseUidChoice(); + ParseFolderPath(&m_sourceCanonicalFolderPathSubString); + ParseListOfMessageIds(); + ParseFolderPath(&m_destinationCanonicalFolderPathSubString); + } else if (!PL_strcasecmp(m_urlidSubString, "onlinetoofflinecopy")) { + m_imapAction = nsImapOnlineToOfflineCopy; + ParseUidChoice(); + ParseFolderPath(&m_sourceCanonicalFolderPathSubString); + ParseListOfMessageIds(); + ParseFolderPath(&m_destinationCanonicalFolderPathSubString); + } else if (!PL_strcasecmp(m_urlidSubString, "onlinetoofflinemove")) { + m_imapAction = nsImapOnlineToOfflineMove; + ParseUidChoice(); + ParseFolderPath(&m_sourceCanonicalFolderPathSubString); + ParseListOfMessageIds(); + ParseFolderPath(&m_destinationCanonicalFolderPathSubString); + } else if (!PL_strcasecmp(m_urlidSubString, "offlinetoonlinecopy")) { + m_imapAction = nsImapOfflineToOnlineMove; + ParseFolderPath(&m_destinationCanonicalFolderPathSubString); + } else if (!PL_strcasecmp(m_urlidSubString, "search")) { + m_imapAction = nsImapSearch; + ParseUidChoice(); + ParseFolderPath(&m_sourceCanonicalFolderPathSubString); + ParseSearchCriteriaString(); + } else if (!PL_strcasecmp(m_urlidSubString, "test")) { + m_imapAction = nsImapTest; + } else if (!PL_strcasecmp(m_urlidSubString, "select")) { + m_imapAction = nsImapSelectFolder; + ParseFolderPath(&m_sourceCanonicalFolderPathSubString); + if (m_tokenPlaceHolder && *m_tokenPlaceHolder) + ParseListOfMessageIds(); + else + m_listOfMessageIds = PL_strdup(""); + } else if (!PL_strcasecmp(m_urlidSubString, "liteselect")) { + m_imapAction = nsImapLiteSelectFolder; + ParseFolderPath(&m_sourceCanonicalFolderPathSubString); + } else if (!PL_strcasecmp(m_urlidSubString, "selectnoop")) { + m_imapAction = nsImapSelectNoopFolder; + m_listOfMessageIds = PL_strdup(""); + ParseFolderPath(&m_sourceCanonicalFolderPathSubString); + } else if (!PL_strcasecmp(m_urlidSubString, "expunge")) { + m_imapAction = nsImapExpungeFolder; + ParseFolderPath(&m_sourceCanonicalFolderPathSubString); + m_listOfMessageIds = PL_strdup(""); // no ids to UNDO + } else if (!PL_strcasecmp(m_urlidSubString, "create")) { + m_imapAction = nsImapCreateFolder; + ParseFolderPath(&m_sourceCanonicalFolderPathSubString); + } else if (!PL_strcasecmp(m_urlidSubString, "ensureExists")) { + m_imapAction = nsImapEnsureExistsFolder; + ParseFolderPath(&m_sourceCanonicalFolderPathSubString); + } else if (!PL_strcasecmp(m_urlidSubString, "discoverchildren")) { + m_imapAction = nsImapDiscoverChildrenUrl; + ParseFolderPath(&m_sourceCanonicalFolderPathSubString); + } else if (!PL_strcasecmp(m_urlidSubString, "discoverallboxes")) { + m_imapAction = nsImapDiscoverAllBoxesUrl; + } else if (!PL_strcasecmp(m_urlidSubString, + "discoverallandsubscribedboxes")) { + m_imapAction = nsImapDiscoverAllAndSubscribedBoxesUrl; + } else if (!PL_strcasecmp(m_urlidSubString, "delete")) { + m_imapAction = nsImapDeleteFolder; + ParseFolderPath(&m_sourceCanonicalFolderPathSubString); + } else if (!PL_strcasecmp(m_urlidSubString, "deletefolder")) { + m_imapAction = nsImapDeleteFolderAndMsgs; + ParseFolderPath(&m_sourceCanonicalFolderPathSubString); + } else if (!PL_strcasecmp(m_urlidSubString, "rename")) { + m_imapAction = nsImapRenameFolder; + ParseFolderPath(&m_sourceCanonicalFolderPathSubString); + ParseFolderPath(&m_destinationCanonicalFolderPathSubString); + } else if (!PL_strcasecmp(m_urlidSubString, "movefolderhierarchy")) { + m_imapAction = nsImapMoveFolderHierarchy; + ParseFolderPath(&m_sourceCanonicalFolderPathSubString); + if (m_tokenPlaceHolder && *m_tokenPlaceHolder) // handle promote to root + ParseFolderPath(&m_destinationCanonicalFolderPathSubString); + } else if (!PL_strcasecmp(m_urlidSubString, "list")) { + m_imapAction = nsImapLsubFolders; + ParseFolderPath(&m_destinationCanonicalFolderPathSubString); + } else if (!PL_strcasecmp(m_urlidSubString, "biff")) { + m_imapAction = nsImapBiff; + ParseFolderPath(&m_sourceCanonicalFolderPathSubString); + ParseListOfMessageIds(); + } else if (!PL_strcasecmp(m_urlidSubString, "netscape")) { + m_imapAction = nsImapGetMailAccountUrl; + } else if (!PL_strcasecmp(m_urlidSubString, "appendmsgfromfile")) { + m_imapAction = nsImapAppendMsgFromFile; + ParseFolderPath(&m_sourceCanonicalFolderPathSubString); + } else if (!PL_strcasecmp(m_urlidSubString, "appenddraftfromfile")) { + m_imapAction = nsImapAppendDraftFromFile; + ParseFolderPath(&m_sourceCanonicalFolderPathSubString); + ParseUidChoice(); + if (m_tokenPlaceHolder && *m_tokenPlaceHolder) + ParseListOfMessageIds(); + else + m_listOfMessageIds = strdup(""); + } else if (!PL_strcasecmp(m_urlidSubString, "subscribe")) { + m_imapAction = nsImapSubscribe; + ParseFolderPath(&m_sourceCanonicalFolderPathSubString); + } else if (!PL_strcasecmp(m_urlidSubString, "unsubscribe")) { + m_imapAction = nsImapUnsubscribe; + ParseFolderPath(&m_sourceCanonicalFolderPathSubString); + } else if (!PL_strcasecmp(m_urlidSubString, "refreshacl")) { + m_imapAction = nsImapRefreshACL; + ParseFolderPath(&m_sourceCanonicalFolderPathSubString); + } else if (!PL_strcasecmp(m_urlidSubString, "refreshfolderurls")) { + m_imapAction = nsImapRefreshFolderUrls; + ParseFolderPath(&m_sourceCanonicalFolderPathSubString); + } else if (!PL_strcasecmp(m_urlidSubString, "refreshallacls")) { + m_imapAction = nsImapRefreshAllACLs; + } else if (!PL_strcasecmp(m_urlidSubString, "listfolder")) { + m_imapAction = nsImapListFolder; + ParseFolderPath(&m_sourceCanonicalFolderPathSubString); + } else if (!PL_strcasecmp(m_urlidSubString, "upgradetosubscription")) { + m_imapAction = nsImapUpgradeToSubscription; + ParseFolderPath(&m_sourceCanonicalFolderPathSubString); + } else if (!PL_strcasecmp(m_urlidSubString, "folderstatus")) { + m_imapAction = nsImapFolderStatus; + ParseFolderPath(&m_sourceCanonicalFolderPathSubString); + } else if (!PL_strcasecmp(m_urlidSubString, "verifyLogon")) { + m_imapAction = nsImapVerifylogon; + } else if (m_imapAction == nsIImapUrl::nsImapUserDefinedMsgCommand) { + m_command = m_urlidSubString; // save this + ParseUidChoice(); + ParseFolderPath(&m_sourceCanonicalFolderPathSubString); + ParseListOfMessageIds(); + } else if (m_imapAction == nsIImapUrl::nsImapMsgStoreCustomKeywords) { + ParseUidChoice(); + ParseFolderPath(&m_sourceCanonicalFolderPathSubString); + ParseListOfMessageIds(); + bool addKeyword = (m_tokenPlaceHolder && *m_tokenPlaceHolder != '>'); + // if we're not adding a keyword, m_tokenPlaceHolder will now look like + // >keywordToSubtract> and strtok will leave flagsPtr pointing to + // keywordToSubtract. So detect this case and only set the + // customSubtractFlags. + char* flagsPtr = m_tokenPlaceHolder ? NS_strtok(IMAP_URL_TOKEN_SEPARATOR, + &m_tokenPlaceHolder) + : (char*)nullptr; + if (addKeyword) { + m_customAddFlags.Assign(flagsPtr); + flagsPtr = m_tokenPlaceHolder ? NS_strtok(IMAP_URL_TOKEN_SEPARATOR, + &m_tokenPlaceHolder) + : (char*)nullptr; + } + m_customSubtractFlags.Assign(flagsPtr); + } else { + m_validUrl = false; + } + } +} + +// Returns NULL if nothing was done. +// Otherwise, returns a newly allocated name. +NS_IMETHODIMP nsImapUrl::AddOnlineDirectoryIfNecessary( + const char* onlineMailboxName, char** directory) { + nsresult rv; + nsString onlineDirString; + char* newOnlineName = nullptr; + + nsCOMPtr<nsIImapHostSessionList> hostSessionList = + do_GetService(kCImapHostSessionListCID, &rv); + if (NS_FAILED(rv)) return rv; + rv = hostSessionList->GetOnlineDirForHost(m_serverKey.get(), onlineDirString); + nsAutoCString onlineDir; + LossyCopyUTF16toASCII(onlineDirString, onlineDir); + + nsImapNamespace* ns = nullptr; + rv = hostSessionList->GetNamespaceForMailboxForHost(m_serverKey.get(), + onlineMailboxName, ns); + if (!ns) + hostSessionList->GetDefaultNamespaceOfTypeForHost(m_serverKey.get(), + kPersonalNamespace, ns); + + if (onlineDir.IsEmpty() && ns) onlineDir = ns->GetPrefix(); + + // If this host has an online server directory configured + if (onlineMailboxName && !onlineDir.IsEmpty()) { + if (PL_strcasecmp(onlineMailboxName, "INBOX")) { + NS_ASSERTION(ns, "couldn't find namespace for host"); + nsAutoCString onlineDirWithDelimiter(onlineDir); + // make sure the onlineDir ends with the hierarchy delimiter + if (ns) { + char delimiter = ns->GetDelimiter(); + if (delimiter && delimiter != kOnlineHierarchySeparatorUnknown) { + // try to change the canonical online dir name to real dir name first + onlineDirWithDelimiter.ReplaceChar('/', delimiter); + // make sure the last character is the delimiter + if (onlineDirWithDelimiter.Last() != delimiter) + onlineDirWithDelimiter += delimiter; + if (!*onlineMailboxName) + onlineDirWithDelimiter.SetLength(onlineDirWithDelimiter.Length() - + 1); + } + } + if (ns && (PL_strlen(ns->GetPrefix()) != 0) && + !onlineDirWithDelimiter.Equals(ns->GetPrefix())) { + // check that onlineMailboxName doesn't start with the namespace. If + // that's the case, we don't want to prepend the online dir. + if (PL_strncmp(onlineMailboxName, ns->GetPrefix(), + PL_strlen(ns->GetPrefix()))) { + // The namespace for this mailbox is the root (""). + // Prepend the online server directory + int finalLen = + onlineDirWithDelimiter.Length() + strlen(onlineMailboxName) + 1; + newOnlineName = (char*)PR_Malloc(finalLen); + if (newOnlineName) { + PL_strcpy(newOnlineName, onlineDirWithDelimiter.get()); + PL_strcat(newOnlineName, onlineMailboxName); + } + } + } + // just prepend the online server directory if it doesn't start with it + // already + else if (strncmp(onlineMailboxName, onlineDirWithDelimiter.get(), + onlineDirWithDelimiter.Length())) { + newOnlineName = (char*)PR_Malloc(strlen(onlineMailboxName) + + onlineDirWithDelimiter.Length() + 1); + if (newOnlineName) { + PL_strcpy(newOnlineName, onlineDirWithDelimiter.get()); + PL_strcat(newOnlineName, onlineMailboxName); + } + } + } + } + if (directory) + *directory = newOnlineName; + else if (newOnlineName) + free(newOnlineName); + return rv; +} + +// Converts from canonical format (hierarchy is indicated by '/' and all real +// slashes ('/') are escaped) to the real online name on the server. +NS_IMETHODIMP nsImapUrl::AllocateServerPath(const char* canonicalPath, + char onlineDelimiter, + char** aAllocatedPath) { + nsresult retVal = NS_OK; + char* rv = NULL; + char delimiterToUse = onlineDelimiter; + if (onlineDelimiter == kOnlineHierarchySeparatorUnknown) + GetOnlineSubDirSeparator(&delimiterToUse); + NS_ASSERTION(delimiterToUse != kOnlineHierarchySeparatorUnknown, + "hierarchy separator unknown"); + if (canonicalPath) + rv = ReplaceCharsInCopiedString(canonicalPath, '/', delimiterToUse); + else + rv = strdup(""); + + if (delimiterToUse != '/') UnescapeSlashes(rv); + char* onlineNameAdded = nullptr; + AddOnlineDirectoryIfNecessary(rv, &onlineNameAdded); + if (onlineNameAdded) { + free(rv); + rv = onlineNameAdded; + } + + if (aAllocatedPath) + *aAllocatedPath = rv; + else + free(rv); + + return retVal; +} + +// escape '/' as ^, ^ -> ^^ - use UnescapeSlashes to revert +/* static */ nsresult nsImapUrl::EscapeSlashes(const char* sourcePath, + char** resultPath) { + NS_ENSURE_ARG(sourcePath); + NS_ENSURE_ARG(resultPath); + int32_t extra = 0; + int32_t len = strlen(sourcePath); + const char* src = sourcePath; + int32_t i; + for (i = 0; i < len; i++) { + if (*src == '^') extra += 1; /* ^ -> ^^ */ + src++; + } + char* result = (char*)moz_xmalloc(len + extra + 1); + if (!result) return NS_ERROR_OUT_OF_MEMORY; + + unsigned char* dst = (unsigned char*)result; + src = sourcePath; + for (i = 0; i < len; i++) { + unsigned char c = *src++; + if (c == '/') + *dst++ = '^'; + else if (c == '^') { + *dst++ = '^'; + *dst++ = '^'; + } else + *dst++ = c; + } + *dst = '\0'; /* tack on eos */ + *resultPath = result; + return NS_OK; +} + +static void unescapeSlashes(char* path, size_t* newLength) { + char* src = path; + char* start = src; + char* dst = path; + + while (*src) { + if (*src == '^') { + if (*(src + 1) == '^') { + *dst++ = '^'; + src++; // skip over second '^' + } else + *dst++ = '/'; + src++; + } else + *dst++ = *src++; + } + + *newLength = dst - start; +} + +/* static */ nsresult nsImapUrl::UnescapeSlashes(char* path) { + size_t newLength; + unescapeSlashes(path, &newLength); + path[newLength] = 0; + return NS_OK; +} + +/* static */ nsresult nsImapUrl::UnescapeSlashes(nsACString& path) { + size_t newLength; + unescapeSlashes(path.BeginWriting(), &newLength); + path.SetLength(newLength); + return NS_OK; +} + +/* static */ nsresult nsImapUrl::ConvertToCanonicalFormat( + const char* folderName, char onlineDelimiter, + char** resultingCanonicalPath) { + // Now, start the conversion to canonical form. + + char* canonicalPath; + if (onlineDelimiter != '/') { + nsCString escapedPath; + + EscapeSlashes(folderName, getter_Copies(escapedPath)); + canonicalPath = + ReplaceCharsInCopiedString(escapedPath.get(), onlineDelimiter, '/'); + } else { + canonicalPath = strdup(folderName); + } + if (canonicalPath) *resultingCanonicalPath = canonicalPath; + + return (canonicalPath) ? NS_OK : NS_ERROR_OUT_OF_MEMORY; +} + +// Converts the real online name on the server to canonical format: +// result is hierarchy is indicated by '/' and all real slashes ('/') are +// escaped. This method is only called from the IMAP thread. +NS_IMETHODIMP nsImapUrl::AllocateCanonicalPath(const char* serverPath, + char onlineDelimiter, + char** allocatedPath) { + NS_ENSURE_ARG_POINTER(serverPath); + + char delimiterToUse = onlineDelimiter; + *allocatedPath = nullptr; + + char* currentPath = (char*)serverPath; + + nsresult rv; + nsCOMPtr<nsIImapHostSessionList> hostSessionList = + do_GetService(kCImapHostSessionListCID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + if (onlineDelimiter == kOnlineHierarchySeparatorUnknown || + onlineDelimiter == 0) + GetOnlineSubDirSeparator(&delimiterToUse); + + nsString dir; + hostSessionList->GetOnlineDirForHost(m_serverKey.get(), dir); + // First we have to check to see if we should strip off an online server + // subdirectory + // If this host has an online server directory configured + nsAutoCString onlineDir; + LossyCopyUTF16toASCII(dir, onlineDir); + + if (!onlineDir.IsEmpty()) { + // By definition, the online dir must be at the root. + if (delimiterToUse && delimiterToUse != kOnlineHierarchySeparatorUnknown) { + // try to change the canonical online dir name to real dir name first + onlineDir.ReplaceChar('/', delimiterToUse); + // Add the delimiter + if (onlineDir.Last() != delimiterToUse) onlineDir += delimiterToUse; + } + int len = onlineDir.Length(); + if (!PL_strncmp(onlineDir.get(), currentPath, len)) { + // This online path begins with the server sub directory + currentPath += len; + + // This might occur, but it's most likely something not good. + // Basically, it means we're doing something on the online sub directory + // itself. + NS_ASSERTION(*currentPath, "Oops ... null currentPath"); + // Also make sure that the first character in the mailbox name is not '/'. + NS_ASSERTION(*currentPath != '/', + "Oops ... currentPath starts with a slash"); + } + } + + return ConvertToCanonicalFormat(currentPath, delimiterToUse, allocatedPath); +} + +// this method is only called from the imap thread +NS_IMETHODIMP nsImapUrl::CreateServerSourceFolderPathString(char** result) { + NS_ENSURE_ARG_POINTER(result); + AllocateServerPath(m_sourceCanonicalFolderPathSubString, + kOnlineHierarchySeparatorUnknown, result); + return NS_OK; +} + +// this method is called from the imap thread AND the UI thread... +NS_IMETHODIMP nsImapUrl::CreateCanonicalSourceFolderPathString(char** result) { + NS_ENSURE_ARG_POINTER(result); + MutexAutoLock mon(mLock); + *result = strdup(m_sourceCanonicalFolderPathSubString + ? m_sourceCanonicalFolderPathSubString + : ""); + return (*result) ? NS_OK : NS_ERROR_OUT_OF_MEMORY; +} + +// this method is called from the imap thread AND the UI thread... +NS_IMETHODIMP nsImapUrl::CreateServerDestinationFolderPathString( + char** result) { + NS_ENSURE_ARG_POINTER(result); + MutexAutoLock mon(mLock); + nsresult rv = AllocateServerPath(m_destinationCanonicalFolderPathSubString, + kOnlineHierarchySeparatorUnknown, result); + return (*result) ? rv : NS_ERROR_OUT_OF_MEMORY; +} + +NS_IMETHODIMP nsImapUrl::SetMimePartSelectorDetected( + bool mimePartSelectorDetected) { + m_mimePartSelectorDetected = mimePartSelectorDetected; + return NS_OK; +} + +NS_IMETHODIMP nsImapUrl::GetMimePartSelectorDetected( + bool* mimePartSelectorDetected) { + if (!mimePartSelectorDetected) return NS_ERROR_NULL_POINTER; + + *mimePartSelectorDetected = m_mimePartSelectorDetected; + return NS_OK; +} + +// this method is only called from the UI thread. +NS_IMETHODIMP nsImapUrl::SetCopyState(nsISupports* copyState) { + MutexAutoLock mon(mLock); + m_copyState = copyState; + return NS_OK; +} + +// this method is only called from the imap thread..but we still +// need a monitor 'cause the setter is called from the UI thread. +NS_IMETHODIMP nsImapUrl::GetCopyState(nsISupports** copyState) { + NS_ENSURE_ARG_POINTER(copyState); + MutexAutoLock mon(mLock); + NS_IF_ADDREF(*copyState = m_copyState); + + return NS_OK; +} + +NS_IMETHODIMP +nsImapUrl::SetMsgFile(nsIFile* aFile) { + nsresult rv = NS_OK; + MutexAutoLock mon(mLock); + m_file = aFile; + return rv; +} + +NS_IMETHODIMP +nsImapUrl::GetMsgFile(nsIFile** aFile) { + NS_ENSURE_ARG_POINTER(aFile); + + MutexAutoLock mon(mLock); + NS_IF_ADDREF(*aFile = m_file); + return NS_OK; +} + +// this method is called from the UI thread.. +NS_IMETHODIMP nsImapUrl::GetMockChannel(nsIImapMockChannel** aChannel) { + NS_ENSURE_ARG_POINTER(aChannel); + MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread(), + "should only access mock channel on ui thread"); + *aChannel = nullptr; + nsCOMPtr<nsIImapMockChannel> channel(do_QueryReferent(m_channelWeakPtr)); + channel.forget(aChannel); + return *aChannel ? NS_OK : NS_ERROR_FAILURE; +} + +NS_IMETHODIMP nsImapUrl::SetMockChannel(nsIImapMockChannel* aChannel) { + MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread(), + "should only access mock channel on ui thread"); + m_channelWeakPtr = do_GetWeakReference(aChannel); + return NS_OK; +} + +nsresult nsImapUrl::Clone(nsIURI** _retval) { + nsresult rv = nsMsgMailNewsUrl::Clone(_retval); + NS_ENSURE_SUCCESS(rv, rv); + // also clone the mURI member, because GetUri below won't work if + // mURI isn't set due to escaping issues. + nsCOMPtr<nsIMsgMessageUrl> clonedUrl = do_QueryInterface(*_retval); + if (clonedUrl) clonedUrl->SetUri(mURI); + return rv; +} + +NS_IMETHODIMP nsImapUrl::GetNormalizedSpec(nsACString& aPrincipalSpec) { + // URLs look like this: + // imap://user@domain@server:port/fetch>UID>folder>nn + // We simply strip any query part beginning with ? & or /; + // Normalized spec: imap://user@domain@server:port/fetch>UID>folder>nn + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsURL; + QueryInterface(NS_GET_IID(nsIMsgMailNewsUrl), getter_AddRefs(mailnewsURL)); + + nsAutoCString spec; + mailnewsURL->GetSpecIgnoringRef(spec); + + // Strip any query part beginning with ? or /; + MsgRemoveQueryPart(spec); + + aPrincipalSpec.Assign(spec); + return NS_OK; +} + +NS_IMETHODIMP nsImapUrl::SetUri(const nsACString& aURI) { + mURI = aURI; + return NS_OK; +} + +NS_IMETHODIMP nsImapUrl::GetUri(nsACString& aURI) { + nsresult rv = NS_OK; + if (!mURI.IsEmpty()) + aURI = mURI; + else { + uint32_t key = + m_listOfMessageIds ? strtoul(m_listOfMessageIds, nullptr, 10) : 0; + nsCString canonicalPath; + AllocateCanonicalPath(m_sourceCanonicalFolderPathSubString, + m_onlineSubDirSeparator, + (getter_Copies(canonicalPath))); + nsCString fullFolderPath("/"); + fullFolderPath.Append(m_userName); + nsAutoCString hostName; + rv = GetHost(hostName); + fullFolderPath.Append('@'); + fullFolderPath.Append(hostName); + fullFolderPath.Append('/'); + fullFolderPath.Append(canonicalPath); + + nsCString baseMessageURI; + nsCreateImapBaseMessageURI(fullFolderPath, baseMessageURI); + rv = nsBuildImapMessageURI(baseMessageURI.get(), key, aURI); + } + return rv; +} + +NS_IMPL_GETSET(nsImapUrl, AddDummyEnvelope, bool, m_addDummyEnvelope) +NS_IMPL_GETSET(nsImapUrl, CanonicalLineEnding, bool, m_canonicalLineEnding) +NS_IMETHODIMP nsImapUrl::GetMsgLoadingFromCache(bool* result) { + NS_ENSURE_ARG_POINTER(result); + *result = m_msgLoadingFromCache; + return NS_OK; +} +NS_IMPL_GETSET(nsImapUrl, LocalFetchOnly, bool, m_localFetchOnly) +NS_IMPL_GETSET(nsImapUrl, ExternalLinkUrl, bool, m_externalLinkUrl) +NS_IMPL_GETSET(nsImapUrl, RerunningUrl, bool, m_rerunningUrl) +NS_IMPL_GETSET(nsImapUrl, ValidUrl, bool, m_validUrl) +NS_IMPL_GETSET(nsImapUrl, MoreHeadersToDownload, bool, m_moreHeadersToDownload) + +NS_IMETHODIMP nsImapUrl::SetMsgLoadingFromCache(bool loadingFromCache) { + nsresult rv = NS_OK; + m_msgLoadingFromCache = loadingFromCache; + return rv; +} + +NS_IMETHODIMP nsImapUrl::SetMessageFile(nsIFile* aFile) { + m_messageFile = aFile; + return NS_OK; +} + +NS_IMETHODIMP nsImapUrl::GetMessageFile(nsIFile** aFile) { + if (aFile) NS_IF_ADDREF(*aFile = m_messageFile); + return NS_OK; +} + +NS_IMETHODIMP nsImapUrl::IsUrlType(uint32_t type, bool* isType) { + NS_ENSURE_ARG(isType); + + switch (type) { + case nsIMsgMailNewsUrl::eCopy: + *isType = ((m_imapAction == nsIImapUrl::nsImapOnlineCopy) || + (m_imapAction == nsIImapUrl::nsImapOnlineToOfflineCopy) || + (m_imapAction == nsIImapUrl::nsImapOfflineToOnlineCopy)); + break; + case nsIMsgMailNewsUrl::eMove: + *isType = ((m_imapAction == nsIImapUrl::nsImapOnlineMove) || + (m_imapAction == nsIImapUrl::nsImapOnlineToOfflineMove) || + (m_imapAction == nsIImapUrl::nsImapOfflineToOnlineMove)); + break; + case nsIMsgMailNewsUrl::eDisplay: + *isType = (m_imapAction == nsIImapUrl::nsImapMsgFetch || + m_imapAction == nsIImapUrl::nsImapMsgFetchPeek); + break; + default: + *isType = false; + }; + + return NS_OK; +} + +NS_IMETHODIMP +nsImapUrl::GetOriginalSpec(nsACString& aSpec) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsImapUrl::SetOriginalSpec(const nsACString& aSpec) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +char* nsImapUrl::ReplaceCharsInCopiedString(const char* stringToCopy, + char oldChar, char newChar) { + char oldCharString[2]; + *oldCharString = oldChar; + *(oldCharString + 1) = 0; + + char* translatedString = PL_strdup(stringToCopy); + char* currentSeparator = PL_strstr(translatedString, oldCharString); + + while (currentSeparator) { + *currentSeparator = newChar; + currentSeparator = PL_strstr(currentSeparator + 1, oldCharString); + } + + return translatedString; +} + +//////////////////////////////////////////////////////////////////////////////////// +// End of functions which should be made obsolete after modifying nsIURI +//////////////////////////////////////////////////////////////////////////////////// + +void nsImapUrl::ParseFolderPath(char** resultingCanonicalPath) { + char* resultPath = m_tokenPlaceHolder ? NS_strtok(IMAP_URL_TOKEN_SEPARATOR, + &m_tokenPlaceHolder) + : (char*)NULL; + + if (!resultPath) { + m_validUrl = false; + return; + } + NS_ASSERTION(*resultingCanonicalPath == nullptr, "whoops, mem leak"); + + char dirSeparator = *resultPath; + + nsCString unescapedResultingCanonicalPath; + MsgUnescapeString(nsDependentCString(resultPath + 1), 0, + unescapedResultingCanonicalPath); + *resultingCanonicalPath = ToNewCString(unescapedResultingCanonicalPath); + // The delimiter will be set for a given URL, but will not be statically + // available from an arbitrary URL. It is the creator's responsibility to + // fill in the correct delimiter from the folder's namespace when creating the + // URL. + if (dirSeparator != kOnlineHierarchySeparatorUnknown) + SetOnlineSubDirSeparator(dirSeparator); + + // if dirSeparator == kOnlineHierarchySeparatorUnknown, then this must be a + // create of a top level imap box. If there is an online subdir, we will + // automatically use its separator. If there is not an online subdir, we + // don't need a separator. +} + +void nsImapUrl::ParseSearchCriteriaString() { + if (m_tokenPlaceHolder) { + int quotedFlag = false; + + // skip initial separator + while (*m_tokenPlaceHolder == *IMAP_URL_TOKEN_SEPARATOR) + m_tokenPlaceHolder++; + + char* saveTokenPlaceHolder = m_tokenPlaceHolder; + + // m_searchCriteriaString = m_tokenPlaceHolder; + + // looking for another separator outside quoted string + while (*m_tokenPlaceHolder) { + if (*m_tokenPlaceHolder == '\\' && *(m_tokenPlaceHolder + 1) == '"') + m_tokenPlaceHolder++; + else if (*m_tokenPlaceHolder == '"') + quotedFlag = !quotedFlag; + else if (!quotedFlag && + *m_tokenPlaceHolder == *IMAP_URL_TOKEN_SEPARATOR) { + *m_tokenPlaceHolder = '\0'; + m_tokenPlaceHolder++; + break; + } + m_tokenPlaceHolder++; + } + m_searchCriteriaString = PL_strdup(saveTokenPlaceHolder); + if (*m_tokenPlaceHolder == '\0') m_tokenPlaceHolder = NULL; + + if (*m_searchCriteriaString == '\0') m_searchCriteriaString = (char*)NULL; + } else + m_searchCriteriaString = (char*)NULL; + if (!m_searchCriteriaString) m_validUrl = false; +} + +void nsImapUrl::ParseUidChoice() { + char* uidChoiceString = + m_tokenPlaceHolder + ? NS_strtok(IMAP_URL_TOKEN_SEPARATOR, &m_tokenPlaceHolder) + : (char*)NULL; + if (!uidChoiceString) + m_validUrl = false; + else + m_idsAreUids = strcmp(uidChoiceString, "UID") == 0; +} + +void nsImapUrl::ParseMsgFlags() { + char* flagsPtr = m_tokenPlaceHolder ? NS_strtok(IMAP_URL_TOKEN_SEPARATOR, + &m_tokenPlaceHolder) + : (char*)NULL; + if (flagsPtr) { + // the url is encodes the flags byte as ascii + int intFlags = atoi(flagsPtr); + m_flags = (imapMessageFlagsType)intFlags; // cast here + } else + m_flags = 0; +} + +void nsImapUrl::ParseListOfMessageIds() { + m_listOfMessageIds = m_tokenPlaceHolder ? NS_strtok(IMAP_URL_TOKEN_SEPARATOR, + &m_tokenPlaceHolder) + : (char*)NULL; + if (!m_listOfMessageIds) + m_validUrl = false; + else { + m_listOfMessageIds = strdup(m_listOfMessageIds); + m_mimePartSelectorDetected = PL_strstr(m_listOfMessageIds, "&part=") != 0 || + PL_strstr(m_listOfMessageIds, "?part=") != 0; + + // if it's a spam filter trying to fetch the msg, don't let it get marked + // read. + if (PL_strstr(m_listOfMessageIds, "?header=filter") != 0) + m_imapAction = nsImapMsgFetchPeek; + } +} + +void nsImapUrl::ParseCustomMsgFetchAttribute() { + m_msgFetchAttribute = m_tokenPlaceHolder ? NS_strtok(IMAP_URL_TOKEN_SEPARATOR, + &m_tokenPlaceHolder) + : (char*)nullptr; +} + +void nsImapUrl::ParseNumBytes() { + const char* numBytes = + (m_tokenPlaceHolder) + ? NS_strtok(IMAP_URL_TOKEN_SEPARATOR, &m_tokenPlaceHolder) + : 0; + m_numBytesToFetch = numBytes ? atoi(numBytes) : 0; +} + +// nsIMsgI18NUrl support + +nsresult nsImapUrl::GetMsgFolder(nsIMsgFolder** msgFolder) { + // if we have a RDF URI, then try to get the folder for that URI and then ask + // the folder for it's charset.... + + nsCString uri; + GetUri(uri); + NS_ENSURE_TRUE(!uri.IsEmpty(), NS_ERROR_FAILURE); + + nsCOMPtr<nsIMsgDBHdr> msg; + GetMsgDBHdrFromURI(uri, getter_AddRefs(msg)); + NS_ENSURE_TRUE(msg, NS_ERROR_FAILURE); + nsresult rv = msg->GetFolder(msgFolder); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(msgFolder, NS_ERROR_FAILURE); + + return NS_OK; +} + +NS_IMETHODIMP nsImapUrl::GetAutodetectCharset(bool* aAutodetectCharset) { + *aAutodetectCharset = mAutodetectCharset; + return NS_OK; +} + +NS_IMETHODIMP nsImapUrl::SetAutodetectCharset(bool aAutodetectCharset) { + mAutodetectCharset = aAutodetectCharset; + return NS_OK; +} + +NS_IMETHODIMP nsImapUrl::GetStoreResultsOffline(bool* aStoreResultsOffline) { + NS_ENSURE_ARG_POINTER(aStoreResultsOffline); + *aStoreResultsOffline = m_storeResultsOffline; + return NS_OK; +} + +NS_IMETHODIMP nsImapUrl::SetStoreResultsOffline(bool aStoreResultsOffline) { + m_storeResultsOffline = aStoreResultsOffline; + return NS_OK; +} + +NS_IMETHODIMP nsImapUrl::GetStoreOfflineOnFallback( + bool* aStoreOfflineOnFallback) { + NS_ENSURE_ARG_POINTER(aStoreOfflineOnFallback); + *aStoreOfflineOnFallback = m_storeOfflineOnFallback; + return NS_OK; +} + +NS_IMETHODIMP nsImapUrl::SetStoreOfflineOnFallback( + bool aStoreOfflineOnFallback) { + m_storeOfflineOnFallback = aStoreOfflineOnFallback; + return NS_OK; +} + +NS_IMETHODIMP nsImapUrl::GetMessageHeader(nsIMsgDBHdr** aMsgHdr) { + nsCString uri; + nsresult rv = GetUri(uri); + NS_ENSURE_SUCCESS(rv, rv); + return GetMsgDBHdrFromURI(uri, aMsgHdr); +} diff --git a/comm/mailnews/imap/src/nsImapUrl.h b/comm/mailnews/imap/src/nsImapUrl.h new file mode 100644 index 0000000000..c5cbdb15cf --- /dev/null +++ b/comm/mailnews/imap/src/nsImapUrl.h @@ -0,0 +1,132 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsImapUrl_h___ +#define nsImapUrl_h___ + +#include "mozilla/Attributes.h" +#include "nsIImapUrl.h" +#include "nsCOMPtr.h" +#include "nsMsgMailNewsUrl.h" +#include "nsIWeakReferenceUtils.h" +#include "nsIFile.h" +#include "mozilla/Mutex.h" + +class nsImapUrl : public nsIImapUrl, + public nsMsgMailNewsUrl, + public nsIMsgMessageUrl, + public nsIMsgI18NUrl { + public: + NS_DECL_ISUPPORTS_INHERITED + + // nsMsgMailNewsUrl overrides + nsresult SetSpecInternal(const nsACString& aSpec) override; + nsresult SetQuery(const nsACString& aQuery) override; + nsresult Clone(nsIURI** _retval) override; + + ////////////////////////////////////////////////////////////////////////////// + // we support the nsIImapUrl interface + ////////////////////////////////////////////////////////////////////////////// + NS_DECL_NSIIMAPURL + + // nsIMsgMailNewsUrl overrides + NS_IMETHOD IsUrlType(uint32_t type, bool* isType) override; + NS_IMETHOD GetFolder(nsIMsgFolder** aFolder) override; + NS_IMETHOD SetFolder(nsIMsgFolder* aFolder) override; + // nsIMsgMessageUrl + NS_DECL_NSIMSGMESSAGEURL + NS_DECL_NSIMSGI18NURL + + // nsImapUrl + nsImapUrl(); + + static nsresult ConvertToCanonicalFormat(const char* folderName, + char onlineDelimiter, + char** resultingCanonicalPath); + static nsresult EscapeSlashes(const char* sourcePath, char** resultPath); + static nsresult UnescapeSlashes(nsACString& path); + static nsresult UnescapeSlashes(char* path); + static char* ReplaceCharsInCopiedString(const char* stringToCopy, + char oldChar, char newChar); + + protected: + virtual ~nsImapUrl(); + virtual nsresult ParseUrl(); + + char* m_listOfMessageIds; + + // handle the imap specific parsing + void ParseImapPart(char* imapPartOfUrl); + + void ParseFolderPath(char** resultingCanonicalPath); + void ParseSearchCriteriaString(); + void ParseUidChoice(); + void ParseMsgFlags(); + void ParseListOfMessageIds(); + void ParseCustomMsgFetchAttribute(); + void ParseNumBytes(); + + nsresult GetMsgFolder(nsIMsgFolder** msgFolder); + + char* m_sourceCanonicalFolderPathSubString; + char* m_destinationCanonicalFolderPathSubString; + char* m_tokenPlaceHolder; + char* m_urlidSubString; + char m_onlineSubDirSeparator; + char* m_searchCriteriaString; // should we use m_search, or is this special? + nsCString m_command; // for custom commands + nsCString m_msgFetchAttribute; // for fetching custom msg attributes + nsCString m_customAttributeResult; // for fetching custom msg attributes + nsCString m_customCommandResult; // custom command response + nsCString + m_customAddFlags; // these two are for setting and clearing custom flags + nsCString m_customSubtractFlags; + int32_t m_numBytesToFetch; // when doing a msg body preview, how many bytes + // to read + bool m_validUrl; + bool m_runningUrl; + bool m_idsAreUids; + bool m_mimePartSelectorDetected; + bool m_msgLoadingFromCache; // if true, we might need to mark read on server + bool m_externalLinkUrl; // if true, we're running this url because the user + // True if the fetch results should be put in the offline store. + bool m_storeResultsOffline; + bool m_storeOfflineOnFallback; + bool m_localFetchOnly; + bool m_rerunningUrl; // first attempt running this failed with connection + // error; retrying + bool m_moreHeadersToDownload; + nsImapContentModifiedType m_contentModified; + + int32_t m_extraStatus; + + nsCString m_userName; + nsCString m_serverKey; + // event sinks + imapMessageFlagsType m_flags; + nsImapAction m_imapAction; + + nsWeakPtr m_imapFolder; + nsWeakPtr m_imapMailFolderSink; + nsWeakPtr m_imapMessageSink; + + nsWeakPtr m_imapServerSink; + + // online message copy support; i don't have a better solution yet + nsCOMPtr<nsISupports> m_copyState; // now, refcounted. + nsCOMPtr<nsIFile> m_file; + nsWeakPtr m_channelWeakPtr; + + // used by save message to disk + nsCOMPtr<nsIFile> m_messageFile; + bool m_addDummyEnvelope; + bool m_canonicalLineEnding; // CRLF + + nsCString mURI; // the RDF URI associated with this url. + bool mAutodetectCharset; // used by nsIMsgI18NUrl... + mozilla::Mutex mLock; +}; + +#endif /* nsImapUrl_h___ */ diff --git a/comm/mailnews/imap/src/nsImapUtils.cpp b/comm/mailnews/imap/src/nsImapUtils.cpp new file mode 100644 index 0000000000..9a5811511c --- /dev/null +++ b/comm/mailnews/imap/src/nsImapUtils.cpp @@ -0,0 +1,336 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" +#include "nsImapUtils.h" +#include "nsCOMPtr.h" +#include "prsystem.h" +#include "prprf.h" +#include "nsNetCID.h" + +#include "nsMsgUtils.h" +#include "nsImapFlagAndUidState.h" +#include "nsImapNamespace.h" +#include "nsIImapFlagAndUidState.h" + +nsresult nsImapURI2FullName(const char* rootURI, const char* hostName, + const char* uriStr, char** name) { + nsAutoCString uri(uriStr); + nsAutoCString fullName; + if (uri.Find(rootURI) != 0) return NS_ERROR_FAILURE; + fullName = Substring(uri, strlen(rootURI)); + uri = fullName; + int32_t hostStart = uri.Find(hostName); + if (hostStart <= 0) return NS_ERROR_FAILURE; + fullName = Substring(uri, hostStart); + uri = fullName; + int32_t hostEnd = uri.FindChar('/'); + if (hostEnd <= 0) return NS_ERROR_FAILURE; + fullName = Substring(uri, hostEnd + 1); + if (fullName.IsEmpty()) return NS_ERROR_FAILURE; + *name = ToNewCString(fullName); + return NS_OK; +} + +/* parses ImapMessageURI */ +nsresult nsParseImapMessageURI(const nsACString& uri, nsCString& folderURI, + uint32_t* key, char** part) { + if (!key) return NS_ERROR_NULL_POINTER; + + const nsPromiseFlatCString& uriStr = PromiseFlatCString(uri); + int32_t folderEnd = -1; + // imap-message uri's can have imap:// url strings tacked on the end, + // e.g., when opening/saving attachments. We don't want to look for '#' + // in that part of the uri, if the attachment name contains '#', + // so check for that here. + if (StringBeginsWith(uriStr, "imap-message"_ns)) + folderEnd = uriStr.Find("imap://"); + + int32_t keySeparator = uriStr.RFindChar('#', folderEnd); + if (keySeparator != -1) { + int32_t keyEndSeparator = MsgFindCharInSet(uriStr, "/?&", keySeparator); + nsAutoString folderPath; + folderURI = StringHead(uriStr, keySeparator); + folderURI.Cut(4, 8); // cut out the _message part of imap-message: + // folder uri's don't have fully escaped usernames. + int32_t atPos = folderURI.FindChar('@'); + if (atPos != -1) { + nsCString unescapedName, escapedName; + int32_t userNamePos = folderURI.Find("//") + 2; + uint32_t origUserNameLen = atPos - userNamePos; + if (NS_SUCCEEDED(MsgUnescapeString( + Substring(folderURI, userNamePos, origUserNameLen), 0, + unescapedName))) { + // Re-escape the username, matching the way we do it in uris, not the + // way necko escapes urls. See nsMsgIncomingServer::GetServerURI. + MsgEscapeString(unescapedName, nsINetUtil::ESCAPE_XALPHAS, escapedName); + folderURI.Replace(userNamePos, origUserNameLen, escapedName); + } + } + nsAutoCString keyStr; + if (keyEndSeparator != -1) + keyStr = Substring(uriStr, keySeparator + 1, + keyEndSeparator - (keySeparator + 1)); + else + keyStr = Substring(uriStr, keySeparator + 1); + + *key = strtoul(keyStr.get(), nullptr, 10); + + if (part && keyEndSeparator != -1) { + int32_t partPos = uriStr.Find("part=", keyEndSeparator); + if (partPos != -1) { + *part = ToNewCString(Substring(uriStr, keyEndSeparator)); + } + } + } + return NS_OK; +} + +nsresult nsBuildImapMessageURI(const char* baseURI, uint32_t key, + nsACString& uri) { + uri.Append(baseURI); + uri.Append('#'); + uri.AppendInt(key); + return NS_OK; +} + +nsresult nsCreateImapBaseMessageURI(const nsACString& baseURI, + nsCString& baseMessageURI) { + nsAutoCString tailURI(baseURI); + // chop off imap:/ + if (tailURI.Find(kImapRootURI) == 0) tailURI.Cut(0, PL_strlen(kImapRootURI)); + baseMessageURI = kImapMessageRootURI; + baseMessageURI += tailURI; + return NS_OK; +} + +// nsImapMailboxSpec definition +NS_IMPL_ISUPPORTS(nsImapMailboxSpec, nsIMailboxSpec) + +nsImapMailboxSpec::nsImapMailboxSpec() { + mFolder_UIDVALIDITY = 0; + mHighestModSeq = 0; + mNumOfMessages = 0; + mNumOfUnseenMessages = 0; + mNumOfRecentMessages = 0; + mNextUID = 0; + + mBoxFlags = 0; + mSupportedUserFlags = 0; + + mHierarchySeparator = '\0'; + + mFolderSelected = false; + mDiscoveredFromLsub = false; + + mOnlineVerified = false; + mNamespaceForFolder = nullptr; +} + +nsImapMailboxSpec::~nsImapMailboxSpec() {} + +NS_IMPL_GETSET(nsImapMailboxSpec, Folder_UIDVALIDITY, int32_t, + mFolder_UIDVALIDITY) +NS_IMPL_GETSET(nsImapMailboxSpec, HighestModSeq, uint64_t, mHighestModSeq) +NS_IMPL_GETSET(nsImapMailboxSpec, NumMessages, int32_t, mNumOfMessages) +NS_IMPL_GETSET(nsImapMailboxSpec, NumUnseenMessages, int32_t, + mNumOfUnseenMessages) +NS_IMPL_GETSET(nsImapMailboxSpec, NumRecentMessages, int32_t, + mNumOfRecentMessages) +NS_IMPL_GETSET(nsImapMailboxSpec, NextUID, int32_t, mNextUID) +NS_IMPL_GETSET(nsImapMailboxSpec, HierarchyDelimiter, char, mHierarchySeparator) +NS_IMPL_GETSET(nsImapMailboxSpec, FolderSelected, bool, mFolderSelected) +NS_IMPL_GETSET(nsImapMailboxSpec, DiscoveredFromLsub, bool, mDiscoveredFromLsub) +NS_IMPL_GETSET(nsImapMailboxSpec, OnlineVerified, bool, mOnlineVerified) +NS_IMPL_GETSET(nsImapMailboxSpec, SupportedUserFlags, uint32_t, + mSupportedUserFlags) +NS_IMPL_GETSET(nsImapMailboxSpec, Box_flags, uint32_t, mBoxFlags) +NS_IMPL_GETSET(nsImapMailboxSpec, NamespaceForFolder, nsImapNamespace*, + mNamespaceForFolder) + +NS_IMETHODIMP nsImapMailboxSpec::GetAllocatedPathName( + nsACString& aAllocatedPathName) { + aAllocatedPathName = mAllocatedPathName; + return NS_OK; +} + +NS_IMETHODIMP nsImapMailboxSpec::SetAllocatedPathName( + const nsACString& aAllocatedPathName) { + mAllocatedPathName = aAllocatedPathName; + return NS_OK; +} + +NS_IMETHODIMP nsImapMailboxSpec::GetUnicharPathName( + nsAString& aUnicharPathName) { + aUnicharPathName = mUnicharPathName; + return NS_OK; +} + +NS_IMETHODIMP nsImapMailboxSpec::SetUnicharPathName( + const nsAString& aUnicharPathName) { + mUnicharPathName = aUnicharPathName; + return NS_OK; +} + +NS_IMETHODIMP nsImapMailboxSpec::GetHostName(nsACString& aHostName) { + aHostName = mHostName; + return NS_OK; +} + +NS_IMETHODIMP nsImapMailboxSpec::SetHostName(const nsACString& aHostName) { + mHostName = aHostName; + return NS_OK; +} + +NS_IMETHODIMP nsImapMailboxSpec::GetFlagState( + nsIImapFlagAndUidState** aFlagState) { + NS_ENSURE_ARG_POINTER(aFlagState); + NS_IF_ADDREF(*aFlagState = mFlagState); + return NS_OK; +} + +NS_IMETHODIMP nsImapMailboxSpec::SetFlagState( + nsIImapFlagAndUidState* aFlagState) { + NS_ENSURE_ARG_POINTER(aFlagState); + mFlagState = aFlagState; + return NS_OK; +} + +nsImapMailboxSpec& nsImapMailboxSpec::operator=( + const nsImapMailboxSpec& aCopy) { + mFolder_UIDVALIDITY = aCopy.mFolder_UIDVALIDITY; + mHighestModSeq = aCopy.mHighestModSeq; + mNumOfMessages = aCopy.mNumOfMessages; + mNumOfUnseenMessages = aCopy.mNumOfUnseenMessages; + mNumOfRecentMessages = aCopy.mNumOfRecentMessages; + + mBoxFlags = aCopy.mBoxFlags; + mSupportedUserFlags = aCopy.mSupportedUserFlags; + + mAllocatedPathName.Assign(aCopy.mAllocatedPathName); + mUnicharPathName.Assign(aCopy.mUnicharPathName); + mHostName.Assign(aCopy.mHostName); + + mFlagState = aCopy.mFlagState; + mNamespaceForFolder = aCopy.mNamespaceForFolder; + + mFolderSelected = aCopy.mFolderSelected; + mDiscoveredFromLsub = aCopy.mDiscoveredFromLsub; + + mOnlineVerified = aCopy.mOnlineVerified; + + return *this; +} + +// use the flagState to determine if the gaps in the msgUids correspond to gaps +// in the mailbox, in which case we can still use ranges. If flagState is null, +// we won't do this. +void AllocateImapUidString(const uint32_t* msgUids, uint32_t& msgCount, + nsImapFlagAndUidState* flagState, + nsCString& returnString) { + uint32_t startSequence = (msgCount > 0) ? msgUids[0] : 0xFFFFFFFF; + uint32_t curSequenceEnd = startSequence; + uint32_t total = msgCount; + int32_t curFlagStateIndex = -1; + + // a partial fetch flag state doesn't help us, so don't use it. + if (flagState && flagState->GetPartialUIDFetch()) flagState = nullptr; + + for (uint32_t keyIndex = 0; keyIndex < total; keyIndex++) { + uint32_t curKey = msgUids[keyIndex]; + uint32_t nextKey = + (keyIndex + 1 < total) ? msgUids[keyIndex + 1] : 0xFFFFFFFF; + bool lastKey = (nextKey == 0xFFFFFFFF); + + if (lastKey) curSequenceEnd = curKey; + + if (!lastKey) { + if (nextKey == curSequenceEnd + 1) { + curSequenceEnd = nextKey; + curFlagStateIndex++; + continue; + } + if (flagState) { + if (curFlagStateIndex == -1) { + bool foundIt; + flagState->GetMessageFlagsFromUID(curSequenceEnd, &foundIt, + &curFlagStateIndex); + if (!foundIt) { + NS_WARNING("flag state missing key"); + // The start of this sequence is missing from flag state, so move + // on to the next key. + curFlagStateIndex = -1; + curSequenceEnd = startSequence = nextKey; + continue; + } + } + curFlagStateIndex++; + uint32_t nextUidInFlagState; + nsresult rv = + flagState->GetUidOfMessage(curFlagStateIndex, &nextUidInFlagState); + if (NS_SUCCEEDED(rv) && nextUidInFlagState == nextKey) { + curSequenceEnd = nextKey; + continue; + } + } + } + if (curSequenceEnd > startSequence) { + returnString.AppendInt((int64_t)startSequence); + returnString += ':'; + returnString.AppendInt((int64_t)curSequenceEnd); + startSequence = nextKey; + curSequenceEnd = startSequence; + curFlagStateIndex = -1; + } else { + startSequence = nextKey; + curSequenceEnd = startSequence; + returnString.AppendInt((int64_t)msgUids[keyIndex]); + curFlagStateIndex = -1; + } + // check if we've generated too long a string - if there's no flag state, + // it means we just need to go ahead and generate a too long string + // because the calling code won't handle breaking up the strings. + if (flagState && returnString.Length() > 950) { + msgCount = keyIndex; + break; + } + // If we are not the last item then we need to add the comma + // but it's important we do it here, after the length check + if (!lastKey) returnString += ','; + } +} + +void ParseUidString(const char* uidString, nsTArray<nsMsgKey>& keys) { + // This is in the form <id>,<id>, or <id1>:<id2> + if (!uidString) return; + + char curChar = *uidString; + bool isRange = false; + uint32_t curToken; + uint32_t saveStartToken = 0; + + for (const char* curCharPtr = uidString; curChar && *curCharPtr;) { + const char* currentKeyToken = curCharPtr; + curChar = *curCharPtr; + while (curChar != ':' && curChar != ',' && curChar != '\0') + curChar = *curCharPtr++; + + // we don't need to null terminate currentKeyToken because strtoul + // stops at non-numeric chars. + curToken = strtoul(currentKeyToken, nullptr, 10); + if (isRange) { + while (saveStartToken < curToken) keys.AppendElement(saveStartToken++); + } + keys.AppendElement(curToken); + isRange = (curChar == ':'); + if (isRange) saveStartToken = curToken + 1; + } +} + +void AppendUid(nsCString& msgIds, uint32_t uid) { + char buf[20]; + PR_snprintf(buf, sizeof(buf), "%u", uid); + msgIds.Append(buf); +} diff --git a/comm/mailnews/imap/src/nsImapUtils.h b/comm/mailnews/imap/src/nsImapUtils.h new file mode 100644 index 0000000000..8dc2c55ef3 --- /dev/null +++ b/comm/mailnews/imap/src/nsImapUtils.h @@ -0,0 +1,77 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef NS_IMAPUTILS_H +#define NS_IMAPUTILS_H + +#include "nsString.h" +#include "MailNewsTypes.h" +#include "nsTArray.h" +#include "nsIMailboxSpec.h" +#include "nsCOMPtr.h" + +class nsImapFlagAndUidState; +class nsImapProtocol; + +static const char kImapRootURI[] = "imap:/"; +static const char kImapMessageRootURI[] = "imap-message:/"; +static const char kModSeqPropertyName[] = "highestModSeq"; +static const char kHighestRecordedUIDPropertyName[] = "highestRecordedUID"; +static const char kDeletedHdrCountPropertyName[] = "numDeletedHeaders"; + +extern nsresult nsImapURI2FullName(const char* rootURI, const char* hostname, + const char* uriStr, char** name); + +extern nsresult nsParseImapMessageURI(const nsACString& uri, + nsCString& folderURI, uint32_t* key, + char** part); + +extern nsresult nsBuildImapMessageURI(const char* baseURI, uint32_t key, + nsACString& uri); + +extern nsresult nsCreateImapBaseMessageURI(const nsACString& baseURI, + nsCString& baseMessageURI); + +void AllocateImapUidString(const uint32_t* msgUids, uint32_t& msgCount, + nsImapFlagAndUidState* flagState, + nsCString& returnString); +void ParseUidString(const char* uidString, nsTArray<nsMsgKey>& keys); +void AppendUid(nsCString& msgIds, uint32_t uid); + +class nsImapMailboxSpec : public nsIMailboxSpec { + public: + nsImapMailboxSpec(); + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIMAILBOXSPEC + + nsImapMailboxSpec& operator=(const nsImapMailboxSpec& aCopy); + + nsCOMPtr<nsIImapFlagAndUidState> mFlagState; + nsImapNamespace* mNamespaceForFolder; + + uint32_t mBoxFlags; + uint32_t mSupportedUserFlags; + int32_t mFolder_UIDVALIDITY; + uint64_t mHighestModSeq; + int32_t mNumOfMessages; + int32_t mNumOfUnseenMessages; + int32_t mNumOfRecentMessages; + int32_t mNextUID; + nsCString mAllocatedPathName; + nsCString mHostName; + nsString mUnicharPathName; + char mHierarchySeparator; + bool mFolderSelected; + bool mDiscoveredFromLsub; + bool mOnlineVerified; + + nsImapProtocol* mConnection; // do we need this? It seems evil + + private: + virtual ~nsImapMailboxSpec(); +}; + +#endif // NS_IMAPUTILS_H diff --git a/comm/mailnews/imap/src/nsSyncRunnableHelpers.cpp b/comm/mailnews/imap/src/nsSyncRunnableHelpers.cpp new file mode 100644 index 0000000000..14028fef03 --- /dev/null +++ b/comm/mailnews/imap/src/nsSyncRunnableHelpers.cpp @@ -0,0 +1,596 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsSyncRunnableHelpers.h" +#include "nsImapCore.h" +#include "nsIMsgMailNewsUrl.h" +#include "nsIMsgIncomingServer.h" +#include "nsIMsgWindow.h" +#include "nsIImapMailFolderSink.h" + +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/Monitor.h" + +NS_IMPL_ISUPPORTS(StreamListenerProxy, nsIStreamListener) +NS_IMPL_ISUPPORTS(ImapMailFolderSinkProxy, nsIImapMailFolderSink) +NS_IMPL_ISUPPORTS(ImapServerSinkProxy, nsIImapServerSink) +NS_IMPL_ISUPPORTS(ImapMessageSinkProxy, nsIImapMessageSink) +NS_IMPL_ISUPPORTS(ImapProtocolSinkProxy, nsIImapProtocolSink) +namespace { + +// Traits class for a reference type, specialized for parameters which are +// already references. +template <typename T> +struct RefType { + typedef T& type; +}; + +template <> +struct RefType<nsAString&> { + typedef nsAString& type; +}; + +template <> +struct RefType<const nsAString&> { + typedef const nsAString& type; +}; + +template <> +struct RefType<nsACString&> { + typedef nsACString& type; +}; + +template <> +struct RefType<const nsACString&> { + typedef const nsACString& type; +}; + +template <> +struct RefType<const nsIID&> { + typedef const nsIID& type; +}; + +class SyncRunnableBase : public mozilla::Runnable { + public: + nsresult Result() { return mResult; } + + mozilla::Monitor& Monitor() { return mMonitor; } + + protected: + SyncRunnableBase() + : mozilla::Runnable("SyncRunnableBase"), + mResult(NS_ERROR_UNEXPECTED), + mMonitor("SyncRunnableBase") {} + + nsresult mResult; + mozilla::Monitor mMonitor; +}; + +template <typename Receiver> +class SyncRunnable0 : public SyncRunnableBase { + public: + typedef nsresult (NS_STDCALL Receiver::*ReceiverMethod)(); + + SyncRunnable0(Receiver* receiver, ReceiverMethod method) + : mReceiver(receiver), mMethod(method) {} + + NS_IMETHOD Run() { + mResult = (mReceiver->*mMethod)(); + mozilla::MonitorAutoLock(mMonitor).Notify(); + return NS_OK; + } + + private: + Receiver* mReceiver; + ReceiverMethod mMethod; +}; + +template <typename Receiver, typename Arg1> +class SyncRunnable1 : public SyncRunnableBase { + public: + typedef nsresult (NS_STDCALL Receiver::*ReceiverMethod)(Arg1); + typedef typename RefType<Arg1>::type Arg1Ref; + + SyncRunnable1(Receiver* receiver, ReceiverMethod method, Arg1Ref arg1) + : mReceiver(receiver), mMethod(method), mArg1(arg1) {} + + NS_IMETHOD Run() { + mResult = (mReceiver->*mMethod)(mArg1); + mozilla::MonitorAutoLock(mMonitor).Notify(); + return NS_OK; + } + + private: + Receiver* mReceiver; + ReceiverMethod mMethod; + Arg1Ref mArg1; +}; + +template <typename Receiver, typename Arg1, typename Arg2> +class SyncRunnable2 : public SyncRunnableBase { + public: + typedef nsresult (NS_STDCALL Receiver::*ReceiverMethod)(Arg1, Arg2); + typedef typename RefType<Arg1>::type Arg1Ref; + typedef typename RefType<Arg2>::type Arg2Ref; + + SyncRunnable2(Receiver* receiver, ReceiverMethod method, Arg1Ref arg1, + Arg2Ref arg2) + : mReceiver(receiver), mMethod(method), mArg1(arg1), mArg2(arg2) {} + + NS_IMETHOD Run() { + mResult = (mReceiver->*mMethod)(mArg1, mArg2); + mozilla::MonitorAutoLock(mMonitor).Notify(); + return NS_OK; + } + + private: + Receiver* mReceiver; + ReceiverMethod mMethod; + Arg1Ref mArg1; + Arg2Ref mArg2; +}; + +template <typename Receiver, typename Arg1, typename Arg2, typename Arg3> +class SyncRunnable3 : public SyncRunnableBase { + public: + typedef nsresult (NS_STDCALL Receiver::*ReceiverMethod)(Arg1, Arg2, Arg3); + typedef typename RefType<Arg1>::type Arg1Ref; + typedef typename RefType<Arg2>::type Arg2Ref; + typedef typename RefType<Arg3>::type Arg3Ref; + + SyncRunnable3(Receiver* receiver, ReceiverMethod method, Arg1Ref arg1, + Arg2Ref arg2, Arg3Ref arg3) + : mReceiver(receiver), + mMethod(method), + mArg1(arg1), + mArg2(arg2), + mArg3(arg3) {} + + NS_IMETHOD Run() { + mResult = (mReceiver->*mMethod)(mArg1, mArg2, mArg3); + mozilla::MonitorAutoLock(mMonitor).Notify(); + return NS_OK; + } + + private: + Receiver* mReceiver; + ReceiverMethod mMethod; + Arg1Ref mArg1; + Arg2Ref mArg2; + Arg3Ref mArg3; +}; + +template <typename Receiver, typename Arg1, typename Arg2, typename Arg3, + typename Arg4> +class SyncRunnable4 : public SyncRunnableBase { + public: + typedef nsresult (NS_STDCALL Receiver::*ReceiverMethod)(Arg1, Arg2, Arg3, + Arg4); + typedef typename RefType<Arg1>::type Arg1Ref; + typedef typename RefType<Arg2>::type Arg2Ref; + typedef typename RefType<Arg3>::type Arg3Ref; + typedef typename RefType<Arg4>::type Arg4Ref; + + SyncRunnable4(Receiver* receiver, ReceiverMethod method, Arg1Ref arg1, + Arg2Ref arg2, Arg3Ref arg3, Arg4Ref arg4) + : mReceiver(receiver), + mMethod(method), + mArg1(arg1), + mArg2(arg2), + mArg3(arg3), + mArg4(arg4) {} + + NS_IMETHOD Run() { + mResult = (mReceiver->*mMethod)(mArg1, mArg2, mArg3, mArg4); + mozilla::MonitorAutoLock(mMonitor).Notify(); + return NS_OK; + } + + private: + Receiver* mReceiver; + ReceiverMethod mMethod; + Arg1Ref mArg1; + Arg2Ref mArg2; + Arg3Ref mArg3; + Arg4Ref mArg4; +}; + +template <typename Receiver, typename Arg1, typename Arg2, typename Arg3, + typename Arg4, typename Arg5> +class SyncRunnable5 : public SyncRunnableBase { + public: + typedef nsresult (NS_STDCALL Receiver::*ReceiverMethod)(Arg1, Arg2, Arg3, + Arg4, Arg5); + typedef typename RefType<Arg1>::type Arg1Ref; + typedef typename RefType<Arg2>::type Arg2Ref; + typedef typename RefType<Arg3>::type Arg3Ref; + typedef typename RefType<Arg4>::type Arg4Ref; + typedef typename RefType<Arg5>::type Arg5Ref; + + SyncRunnable5(Receiver* receiver, ReceiverMethod method, Arg1Ref arg1, + Arg2Ref arg2, Arg3Ref arg3, Arg4Ref arg4, Arg5Ref arg5) + : mReceiver(receiver), + mMethod(method), + mArg1(arg1), + mArg2(arg2), + mArg3(arg3), + mArg4(arg4), + mArg5(arg5) {} + + NS_IMETHOD Run() { + mResult = (mReceiver->*mMethod)(mArg1, mArg2, mArg3, mArg4, mArg5); + mozilla::MonitorAutoLock(mMonitor).Notify(); + return NS_OK; + } + + private: + Receiver* mReceiver; + ReceiverMethod mMethod; + Arg1Ref mArg1; + Arg2Ref mArg2; + Arg3Ref mArg3; + Arg4Ref mArg4; + Arg5Ref mArg5; +}; + +nsresult DispatchSyncRunnable(SyncRunnableBase* r) { + if (NS_IsMainThread()) { + r->Run(); + } else { + mozilla::MonitorAutoLock lock(r->Monitor()); + nsresult rv = NS_DispatchToMainThread(r); + if (NS_FAILED(rv)) return rv; + lock.Wait(); + } + return r->Result(); +} + +} // anonymous namespace + +#define NS_SYNCRUNNABLEMETHOD0(iface, method) \ + NS_IMETHODIMP iface##Proxy::method() { \ + RefPtr<SyncRunnableBase> r = \ + new SyncRunnable0<nsI##iface>(mReceiver, &nsI##iface::method); \ + return DispatchSyncRunnable(r); \ + } + +#define NS_SYNCRUNNABLEMETHOD1(iface, method, arg1) \ + NS_IMETHODIMP iface##Proxy::method(arg1 a1) { \ + RefPtr<SyncRunnableBase> r = new SyncRunnable1<nsI##iface, arg1>( \ + mReceiver, &nsI##iface::method, a1); \ + return DispatchSyncRunnable(r); \ + } + +#define NS_SYNCRUNNABLEMETHOD2(iface, method, arg1, arg2) \ + NS_IMETHODIMP iface##Proxy::method(arg1 a1, arg2 a2) { \ + RefPtr<SyncRunnableBase> r = new SyncRunnable2<nsI##iface, arg1, arg2>( \ + mReceiver, &nsI##iface::method, a1, a2); \ + return DispatchSyncRunnable(r); \ + } + +#define NS_SYNCRUNNABLEMETHOD3(iface, method, arg1, arg2, arg3) \ + NS_IMETHODIMP iface##Proxy::method(arg1 a1, arg2 a2, arg3 a3) { \ + RefPtr<SyncRunnableBase> r = \ + new SyncRunnable3<nsI##iface, arg1, arg2, arg3>( \ + mReceiver, &nsI##iface::method, a1, a2, a3); \ + return DispatchSyncRunnable(r); \ + } + +#define NS_SYNCRUNNABLEMETHOD4(iface, method, arg1, arg2, arg3, arg4) \ + NS_IMETHODIMP iface##Proxy::method(arg1 a1, arg2 a2, arg3 a3, arg4 a4) { \ + RefPtr<SyncRunnableBase> r = \ + new SyncRunnable4<nsI##iface, arg1, arg2, arg3, arg4>( \ + mReceiver, &nsI##iface::method, a1, a2, a3, a4); \ + return DispatchSyncRunnable(r); \ + } + +#define NS_SYNCRUNNABLEMETHOD5(iface, method, arg1, arg2, arg3, arg4, arg5) \ + NS_IMETHODIMP iface##Proxy::method(arg1 a1, arg2 a2, arg3 a3, arg4 a4, \ + arg5 a5) { \ + RefPtr<SyncRunnableBase> r = \ + new SyncRunnable5<nsI##iface, arg1, arg2, arg3, arg4, arg5>( \ + mReceiver, &nsI##iface::method, a1, a2, a3, a4, a5); \ + return DispatchSyncRunnable(r); \ + } + +#define NS_SYNCRUNNABLEATTRIBUTE(iface, attribute, type) \ + NS_IMETHODIMP iface##Proxy::Get##attribute(type* a1) { \ + RefPtr<SyncRunnableBase> r = new SyncRunnable1<nsI##iface, type*>( \ + mReceiver, &nsI##iface::Get##attribute, a1); \ + return DispatchSyncRunnable(r); \ + } \ + NS_IMETHODIMP iface##Proxy::Set##attribute(type a1) { \ + RefPtr<SyncRunnableBase> r = new SyncRunnable1<nsI##iface, type>( \ + mReceiver, &nsI##iface::Set##attribute, a1); \ + return DispatchSyncRunnable(r); \ + } + +#define NS_NOTIMPLEMENTED \ + { \ + NS_RUNTIMEABORT("Not implemented"); \ + return NS_ERROR_UNEXPECTED; \ + } + +NS_SYNCRUNNABLEMETHOD4(StreamListener, OnDataAvailable, nsIRequest*, + nsIInputStream*, uint64_t, uint32_t) + +NS_SYNCRUNNABLEMETHOD1(StreamListener, OnStartRequest, nsIRequest*) + +NS_SYNCRUNNABLEMETHOD2(StreamListener, OnStopRequest, nsIRequest*, nsresult) + +NS_SYNCRUNNABLEMETHOD2(ImapProtocolSink, GetUrlWindow, nsIMsgMailNewsUrl*, + nsIMsgWindow**) + +NS_SYNCRUNNABLEMETHOD0(ImapProtocolSink, CloseStreams) +NS_SYNCRUNNABLEMETHOD0(ImapProtocolSink, SetupMainThreadProxies) + +NS_SYNCRUNNABLEATTRIBUTE(ImapMailFolderSink, FolderNeedsACLListed, bool) +NS_SYNCRUNNABLEATTRIBUTE(ImapMailFolderSink, FolderNeedsSubscribing, bool) +NS_SYNCRUNNABLEATTRIBUTE(ImapMailFolderSink, FolderNeedsAdded, bool) +NS_SYNCRUNNABLEATTRIBUTE(ImapMailFolderSink, AclFlags, uint32_t) +NS_SYNCRUNNABLEATTRIBUTE(ImapMailFolderSink, UidValidity, int32_t) +NS_SYNCRUNNABLEATTRIBUTE(ImapMailFolderSink, FolderQuotaCommandIssued, bool) +NS_SYNCRUNNABLEMETHOD4(ImapMailFolderSink, SetFolderQuotaData, uint32_t, + const nsACString&, uint64_t, uint64_t) +NS_SYNCRUNNABLEMETHOD1(ImapMailFolderSink, GetShouldDownloadAllHeaders, bool*) +NS_SYNCRUNNABLEMETHOD1(ImapMailFolderSink, GetOnlineDelimiter, char*) +NS_SYNCRUNNABLEMETHOD0(ImapMailFolderSink, OnNewIdleMessages) +NS_SYNCRUNNABLEMETHOD2(ImapMailFolderSink, UpdateImapMailboxStatus, + nsIImapProtocol*, nsIMailboxSpec*) +NS_SYNCRUNNABLEMETHOD2(ImapMailFolderSink, UpdateImapMailboxInfo, + nsIImapProtocol*, nsIMailboxSpec*) +NS_SYNCRUNNABLEMETHOD3(ImapMailFolderSink, GetMsgHdrsToDownload, bool*, + int32_t*, nsTArray<nsMsgKey>&) +NS_SYNCRUNNABLEMETHOD2(ImapMailFolderSink, ParseMsgHdrs, nsIImapProtocol*, + nsIImapHeaderXferInfo*) +NS_SYNCRUNNABLEMETHOD1(ImapMailFolderSink, AbortHeaderParseStream, + nsIImapProtocol*) +NS_SYNCRUNNABLEMETHOD2(ImapMailFolderSink, OnlineCopyCompleted, + nsIImapProtocol*, ImapOnlineCopyState) +NS_SYNCRUNNABLEMETHOD1(ImapMailFolderSink, StartMessage, nsIMsgMailNewsUrl*) +NS_SYNCRUNNABLEMETHOD2(ImapMailFolderSink, EndMessage, nsIMsgMailNewsUrl*, + nsMsgKey) +NS_SYNCRUNNABLEMETHOD2(ImapMailFolderSink, NotifySearchHit, nsIMsgMailNewsUrl*, + const char*) +NS_SYNCRUNNABLEMETHOD2(ImapMailFolderSink, CopyNextStreamMessage, bool, + nsISupports*) +NS_SYNCRUNNABLEMETHOD1(ImapMailFolderSink, CloseMockChannel, + nsIImapMockChannel*) +NS_SYNCRUNNABLEMETHOD5(ImapMailFolderSink, SetUrlState, nsIImapProtocol*, + nsIMsgMailNewsUrl*, bool, bool, nsresult) +NS_SYNCRUNNABLEMETHOD1(ImapMailFolderSink, ReleaseUrlCacheEntry, + nsIMsgMailNewsUrl*) +NS_SYNCRUNNABLEMETHOD1(ImapMailFolderSink, HeaderFetchCompleted, + nsIImapProtocol*) +NS_SYNCRUNNABLEMETHOD1(ImapMailFolderSink, SetBiffStateAndUpdate, int32_t) +NS_SYNCRUNNABLEMETHOD3(ImapMailFolderSink, ProgressStatusString, + nsIImapProtocol*, const char*, const char16_t*) +NS_SYNCRUNNABLEMETHOD5(ImapMailFolderSink, PercentProgress, nsIImapProtocol*, + nsACString const&, nsAString const&, int64_t, int64_t) +NS_SYNCRUNNABLEMETHOD0(ImapMailFolderSink, ClearFolderRights) +NS_SYNCRUNNABLEMETHOD2(ImapMailFolderSink, SetCopyResponseUid, const char*, + nsIImapUrl*) +NS_SYNCRUNNABLEMETHOD2(ImapMailFolderSink, SetAppendMsgUid, nsMsgKey, + nsIImapUrl*) +NS_SYNCRUNNABLEMETHOD2(ImapMailFolderSink, GetMessageId, nsIImapUrl*, + nsACString&) + +NS_SYNCRUNNABLEMETHOD2(ImapMessageSink, SetupMsgWriteStream, nsIFile*, bool) +NS_SYNCRUNNABLEMETHOD3(ImapMessageSink, ParseAdoptedMsgLine, const char*, + nsMsgKey, nsIImapUrl*) +NS_SYNCRUNNABLEMETHOD4(ImapMessageSink, NormalEndMsgWriteStream, nsMsgKey, bool, + nsIImapUrl*, int32_t) +NS_SYNCRUNNABLEMETHOD0(ImapMessageSink, AbortMsgWriteStream) +NS_SYNCRUNNABLEMETHOD0(ImapMessageSink, BeginMessageUpload) +NS_SYNCRUNNABLEMETHOD4(ImapMessageSink, NotifyMessageFlags, uint32_t, + const nsACString&, nsMsgKey, uint64_t) +NS_SYNCRUNNABLEMETHOD3(ImapMessageSink, NotifyMessageDeleted, const char*, bool, + const char*) +NS_SYNCRUNNABLEMETHOD2(ImapMessageSink, GetMessageSizeFromDB, const char*, + uint32_t*) +NS_SYNCRUNNABLEMETHOD4(ImapMessageSink, GetCurMoveCopyMessageInfo, nsIImapUrl*, + PRTime*, nsACString&, uint32_t*) + +NS_SYNCRUNNABLEMETHOD4(ImapServerSink, PossibleImapMailbox, const nsACString&, + char, int32_t, bool*) +NS_SYNCRUNNABLEMETHOD2(ImapServerSink, FolderNeedsACLInitialized, + const nsACString&, bool*) +NS_SYNCRUNNABLEMETHOD3(ImapServerSink, AddFolderRights, const nsACString&, + const nsACString&, const nsACString&) +NS_SYNCRUNNABLEMETHOD1(ImapServerSink, RefreshFolderRights, const nsACString&) +NS_SYNCRUNNABLEMETHOD0(ImapServerSink, DiscoveryDone) +NS_SYNCRUNNABLEMETHOD1(ImapServerSink, OnlineFolderDelete, const nsACString&) +NS_SYNCRUNNABLEMETHOD1(ImapServerSink, OnlineFolderCreateFailed, + const nsACString&) +NS_SYNCRUNNABLEMETHOD3(ImapServerSink, OnlineFolderRename, nsIMsgWindow*, + const nsACString&, const nsACString&) +NS_SYNCRUNNABLEMETHOD2(ImapServerSink, FolderIsNoSelect, const nsACString&, + bool*) +NS_SYNCRUNNABLEMETHOD2(ImapServerSink, SetFolderAdminURL, const nsACString&, + const nsACString&) +NS_SYNCRUNNABLEMETHOD2(ImapServerSink, FolderVerifiedOnline, const nsACString&, + bool*) +NS_SYNCRUNNABLEMETHOD1(ImapServerSink, SetCapability, eIMAPCapabilityFlags) +NS_SYNCRUNNABLEMETHOD1(ImapServerSink, SetServerID, const nsACString&) +NS_SYNCRUNNABLEMETHOD2(ImapServerSink, LoadNextQueuedUrl, nsIImapProtocol*, + bool*) +NS_SYNCRUNNABLEMETHOD2(ImapServerSink, PrepareToRetryUrl, nsIImapUrl*, + nsIImapMockChannel**) +NS_SYNCRUNNABLEMETHOD1(ImapServerSink, SuspendUrl, nsIImapUrl*) +NS_SYNCRUNNABLEMETHOD2(ImapServerSink, RetryUrl, nsIImapUrl*, + nsIImapMockChannel*) +NS_SYNCRUNNABLEMETHOD0(ImapServerSink, AbortQueuedUrls) +NS_SYNCRUNNABLEMETHOD2(ImapServerSink, GetImapStringByName, const char*, + nsAString&) +NS_SYNCRUNNABLEMETHOD2(ImapServerSink, PromptLoginFailed, nsIMsgWindow*, + int32_t*) +NS_SYNCRUNNABLEMETHOD2(ImapServerSink, FEAlert, const nsAString&, + nsIMsgMailNewsUrl*) +NS_SYNCRUNNABLEMETHOD2(ImapServerSink, FEAlertWithName, const char*, + nsIMsgMailNewsUrl*) +NS_SYNCRUNNABLEMETHOD2(ImapServerSink, FEAlertFromServer, const nsACString&, + nsIMsgMailNewsUrl*) +NS_SYNCRUNNABLEMETHOD0(ImapServerSink, CommitNamespaces) +NS_SYNCRUNNABLEMETHOD3(ImapServerSink, AsyncGetPassword, nsIImapProtocol*, bool, + nsAString&) +NS_SYNCRUNNABLEMETHOD1(ImapServerSink, SyncGetPassword, nsAString&) +NS_SYNCRUNNABLEATTRIBUTE(ImapServerSink, UserAuthenticated, bool) +NS_SYNCRUNNABLEMETHOD3(ImapServerSink, SetMailServerUrls, const nsACString&, + const nsACString&, const nsACString&) +NS_SYNCRUNNABLEMETHOD1(ImapServerSink, GetArbitraryHeaders, nsACString&) +NS_SYNCRUNNABLEMETHOD0(ImapServerSink, ForgetPassword) +NS_SYNCRUNNABLEMETHOD1(ImapServerSink, GetShowAttachmentsInline, bool*) +NS_SYNCRUNNABLEMETHOD3(ImapServerSink, CramMD5Hash, const char*, const char*, + char**) +NS_SYNCRUNNABLEMETHOD1(ImapServerSink, GetLoginUsername, nsACString&) +NS_SYNCRUNNABLEMETHOD1(ImapServerSink, UpdateTrySTARTTLSPref, bool) +NS_SYNCRUNNABLEMETHOD1(ImapServerSink, GetOriginalUsername, nsACString&) +NS_SYNCRUNNABLEMETHOD1(ImapServerSink, GetServerKey, nsACString&) +NS_SYNCRUNNABLEMETHOD1(ImapServerSink, GetServerPassword, nsAString&) +NS_SYNCRUNNABLEMETHOD1(ImapServerSink, RemoveServerConnection, nsIImapProtocol*) +NS_SYNCRUNNABLEMETHOD1(ImapServerSink, GetServerShuttingDown, bool*) +NS_SYNCRUNNABLEMETHOD1(ImapServerSink, ResetServerConnection, const nsACString&) +NS_SYNCRUNNABLEMETHOD1(ImapServerSink, SetServerDoingLsub, bool) +NS_SYNCRUNNABLEMETHOD1(ImapServerSink, SetServerUtf8AcceptEnabled, bool) + +namespace mozilla { +namespace mailnews { + +NS_IMPL_ISUPPORTS(OAuth2ThreadHelper, msgIOAuth2ModuleListener) + +OAuth2ThreadHelper::OAuth2ThreadHelper(nsIMsgIncomingServer* aServer) + : mMonitor("OAuth thread lock"), mServer(aServer) {} + +OAuth2ThreadHelper::~OAuth2ThreadHelper() { + if (mOAuth2Support) { + NS_ReleaseOnMainThread("OAuth2ThreadHelper::mOAuth2Support", + mOAuth2Support.forget()); + } +} + +bool OAuth2ThreadHelper::SupportsOAuth2() { + // Acquire a lock early, before reading anything. Guarantees memory visibility + // issues. + MonitorAutoLock lockGuard(mMonitor); + + // If we don't have a server, we can't init, and therefore, we don't support + // OAuth2. + if (!mServer) return false; + + // If we have this, then we support OAuth2. + if (mOAuth2Support) return true; + + // Initialize. This needs to be done on-main-thread: if we're off that thread, + // synchronously dispatch to the main thread. + if (NS_IsMainThread()) { + MonitorAutoUnlock lockGuard(mMonitor); + Init(); + } else { + nsCOMPtr<nsIRunnable> runInit = NewRunnableMethod( + "OAuth2ThreadHelper::SupportsOAuth2", this, &OAuth2ThreadHelper::Init); + nsresult rv = NS_DispatchToMainThread(runInit); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + mMonitor.Wait(); + } + + // After synchronously initializing, if we didn't get an object, then we don't + // support XOAuth2. + return mOAuth2Support != nullptr; +} + +void OAuth2ThreadHelper::GetXOAuth2String(nsACString& base64Str) { + MOZ_ASSERT(!NS_IsMainThread(), "This method cannot run on the main thread"); + + // Acquire a lock early, before reading anything. Guarantees memory visibility + // issues. + MonitorAutoLock lockGuard(mMonitor); + + // Umm... what are you trying to do? + if (!mOAuth2Support) return; + + nsCOMPtr<nsIRunnable> runInit = + NewRunnableMethod("OAuth2ThreadHelper::GetXOAuth2String", this, + &OAuth2ThreadHelper::Connect); + nsresult rv = NS_DispatchToMainThread(runInit); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + mMonitor.Wait(); + + nsCOMPtr<nsIRunnable> shutdownNotify = NS_NewRunnableFunction( + "GetXOAuth2StringShutdownNotifier", [self = RefPtr(this)]() { + mozilla::RunOnShutdown([self]() { + // Notify anyone waiting that we're done. + self->mMonitor.Notify(); + }); + }); + rv = NS_DispatchToMainThread(shutdownNotify.forget()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + // Now we either have the string, or we failed (in which case the string is + // empty). + base64Str = mOAuth2String; +} + +void OAuth2ThreadHelper::Init() { + MOZ_ASSERT(NS_IsMainThread(), "Can't touch JS off-main-thread"); + MonitorAutoLock lockGuard(mMonitor); + + // Create the OAuth2 helper module and initialize it. If the preferences are + // not set up on this server, we don't support OAuth2, and we nullify our + // members to indicate this. + mOAuth2Support = do_CreateInstance(MSGIOAUTH2MODULE_CONTRACTID); + if (mOAuth2Support) { + bool supportsOAuth = false; + mOAuth2Support->InitFromMail(mServer, &supportsOAuth); + if (!supportsOAuth) mOAuth2Support = nullptr; + } + + // There is now no longer any need for the server. Kill it now--this helps + // prevent us from maintaining a refcount cycle. + mServer = nullptr; + + // Notify anyone waiting that we're done. + mMonitor.Notify(); +} + +void OAuth2ThreadHelper::Connect() { + MOZ_ASSERT(NS_IsMainThread(), "Can't touch JS off-main-thread"); + MOZ_ASSERT(mOAuth2Support, "Should not be here if no OAuth2 support"); + + // OK to delay lock since mOAuth2Support is only written on main thread. + nsresult rv = mOAuth2Support->Connect(true, this); + // If the method failed, we'll never get a callback, so notify the monitor + // immediately so that IMAP can react. + if (NS_FAILED(rv)) { + MonitorAutoLock lockGuard(mMonitor); + mMonitor.Notify(); + } +} + +nsresult OAuth2ThreadHelper::OnSuccess(const nsACString& aOAuth2String) { + MOZ_ASSERT(NS_IsMainThread(), "Can't touch JS off-main-thread"); + MonitorAutoLock lockGuard(mMonitor); + + MOZ_ASSERT(mOAuth2Support, "Should not be here if no OAuth2 support"); + mOAuth2String = aOAuth2String; + mMonitor.Notify(); + return NS_OK; +} + +nsresult OAuth2ThreadHelper::OnFailure(nsresult aError) { + MOZ_ASSERT(NS_IsMainThread(), "Can't touch JS off-main-thread"); + MonitorAutoLock lockGuard(mMonitor); + + mOAuth2String.Truncate(); + mMonitor.Notify(); + return NS_OK; +} + +} // namespace mailnews +} // namespace mozilla diff --git a/comm/mailnews/imap/src/nsSyncRunnableHelpers.h b/comm/mailnews/imap/src/nsSyncRunnableHelpers.h new file mode 100644 index 0000000000..9be1aafef6 --- /dev/null +++ b/comm/mailnews/imap/src/nsSyncRunnableHelpers.h @@ -0,0 +1,149 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsSyncRunnableHelpers_h +#define nsSyncRunnableHelpers_h + +#include "nsThreadUtils.h" +#include "nsProxyRelease.h" + +#include "mozilla/Monitor.h" +#include "msgIOAuth2Module.h" +#include "nsIStreamListener.h" +#include "nsIImapMailFolderSink.h" +#include "nsIImapServerSink.h" +#include "nsIImapProtocolSink.h" +#include "nsIImapMessageSink.h" + +// The classes in this file proxy method calls to the main thread +// synchronously. The main thread must not block on this thread, or a +// deadlock condition can occur. + +class StreamListenerProxy final : public nsIStreamListener { + public: + explicit StreamListenerProxy(nsIStreamListener* receiver) + : mReceiver(receiver) { + MOZ_DIAGNOSTIC_ASSERT( + receiver, "Null receiver, crash now to get feedback instead of later"); + } + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + + private: + ~StreamListenerProxy() { + NS_ReleaseOnMainThread("StreamListenerProxy::mReceiver", + mReceiver.forget()); + } + nsCOMPtr<nsIStreamListener> mReceiver; +}; + +class ImapMailFolderSinkProxy final : public nsIImapMailFolderSink { + public: + explicit ImapMailFolderSinkProxy(nsIImapMailFolderSink* receiver) + : mReceiver(receiver) { + MOZ_DIAGNOSTIC_ASSERT( + receiver, "Null receiver, crash now to get feedback instead of later"); + } + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIIMAPMAILFOLDERSINK + + private: + ~ImapMailFolderSinkProxy() { + NS_ReleaseOnMainThread("ImapMailFolderSinkProxy::mReceiver", + mReceiver.forget()); + } + nsCOMPtr<nsIImapMailFolderSink> mReceiver; +}; + +class ImapServerSinkProxy final : public nsIImapServerSink { + public: + explicit ImapServerSinkProxy(nsIImapServerSink* receiver) + : mReceiver(receiver) { + MOZ_DIAGNOSTIC_ASSERT( + receiver, "Null receiver, crash now to get feedback instead of later"); + } + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIIMAPSERVERSINK + + private: + ~ImapServerSinkProxy() { + NS_ReleaseOnMainThread("ImapServerSinkProxy::mReceiver", + mReceiver.forget()); + } + nsCOMPtr<nsIImapServerSink> mReceiver; +}; + +class ImapMessageSinkProxy final : public nsIImapMessageSink { + public: + explicit ImapMessageSinkProxy(nsIImapMessageSink* receiver) + : mReceiver(receiver) { + MOZ_DIAGNOSTIC_ASSERT( + receiver, "Null receiver, crash now to get feedback instead of later"); + } + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIIMAPMESSAGESINK + + private: + ~ImapMessageSinkProxy() { + NS_ReleaseOnMainThread("ImapMessageSinkProxy::mReceiver", + mReceiver.forget()); + } + nsCOMPtr<nsIImapMessageSink> mReceiver; +}; + +class ImapProtocolSinkProxy final : public nsIImapProtocolSink { + public: + explicit ImapProtocolSinkProxy(nsIImapProtocolSink* receiver) + : mReceiver(receiver) { + MOZ_DIAGNOSTIC_ASSERT( + receiver, "Null receiver, crash now to get feedback instead of later"); + } + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIIMAPPROTOCOLSINK + + private: + ~ImapProtocolSinkProxy() { + NS_ReleaseOnMainThread("ImapProtocolSinkProxy::mReceiver", + mReceiver.forget()); + } + nsCOMPtr<nsIImapProtocolSink> mReceiver; +}; + +class msgIOAuth2Module; +class nsIMsgIncomingServer; + +namespace mozilla { +namespace mailnews { + +class OAuth2ThreadHelper final : public msgIOAuth2ModuleListener { + public: + explicit OAuth2ThreadHelper(nsIMsgIncomingServer* aServer); + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_MSGIOAUTH2MODULELISTENER + + bool SupportsOAuth2(); + void GetXOAuth2String(nsACString& base64Str); + + private: + ~OAuth2ThreadHelper(); + void Init(); + void Connect(); + + Monitor mMonitor; + nsCOMPtr<msgIOAuth2Module> mOAuth2Support; + nsCOMPtr<nsIMsgIncomingServer> mServer; + nsCString mOAuth2String; +}; + +} // namespace mailnews +} // namespace mozilla + +#endif // nsSyncRunnableHelpers_h diff --git a/comm/mailnews/imap/test/TestImapFlagAndUidState.cpp b/comm/mailnews/imap/test/TestImapFlagAndUidState.cpp new file mode 100644 index 0000000000..a7658322b8 --- /dev/null +++ b/comm/mailnews/imap/test/TestImapFlagAndUidState.cpp @@ -0,0 +1,166 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#include <stdio.h> +#include "TestHarness.h" +#include "nsCOMPtr.h" +#include "msgCore.h" +#include "nsImapProtocol.h" + +struct msgState { + uint32_t uid; + uint16_t flag; + uint32_t index; +}; + +char errorMsg[200]; + +const char* MainChecks(nsImapFlagAndUidState* flagState, + struct msgState* expectedState, uint32_t numMessages, + uint32_t expectedNumUnread) { + // Verify that flag state matches the expected state. + for (uint32_t i = 0; i < numMessages; i++) { + uint32_t uid; + uint16_t flag; + flagState->GetUidOfMessage(expectedState[i].index, &uid); + flagState->GetMessageFlags(expectedState[i].index, &flag); + if (uid != expectedState[i].uid) { + PR_snprintf(errorMsg, sizeof(errorMsg), + "expected uid %d, got %d at index %d\n", expectedState[i].uid, + uid, i); + return errorMsg; + } + if (flag != expectedState[i].flag) { + PR_snprintf(errorMsg, sizeof(errorMsg), + "expected flag %d, got %d at index %d\n", + expectedState[i].flag, flag, i); + return errorMsg; + } + } + int32_t numMsgsInFlagState; + int32_t numUnread = 0; + int32_t expectedMsgIndex = 0; + + flagState->GetNumberOfMessages(&numMsgsInFlagState); + for (int32_t msgIndex = 0; msgIndex < numMsgsInFlagState; msgIndex++) { + uint32_t uidOfMessage; + flagState->GetUidOfMessage(msgIndex, &uidOfMessage); + if (!uidOfMessage || uidOfMessage == nsMsgKey_None) continue; + if (uidOfMessage != expectedState[expectedMsgIndex++].uid) { + PR_snprintf( + errorMsg, sizeof(errorMsg), + "got a uid w/o a match in expected state, uid %d at index %d\n", + uidOfMessage, msgIndex); + return errorMsg; + } + imapMessageFlagsType flags; + flagState->GetMessageFlags(msgIndex, &flags); + if (!(flags & kImapMsgSeenFlag)) numUnread++; + } + if (numUnread != expectedNumUnread) { + PR_snprintf(errorMsg, sizeof(errorMsg), + "expected %d unread message, got %d\n", expectedNumUnread, + numUnread); + return errorMsg; + } + return nullptr; +} + +// General note about return values: +// return 1 for a setup or xpcom type failure, return 2 for a real test failure +int main(int argc, char** argv) { + ScopedXPCOM xpcom("TestImapFlagAndUidState.cpp"); + if (xpcom.failed()) return 1; + + struct msgState msgState1[] = {{10, kImapMsgSeenFlag, 0}, + {15, kImapMsgSeenFlag, 1}, + {16, kImapMsgSeenFlag, 2}, + {17, kImapMsgSeenFlag, 3}, + {18, kImapMsgSeenFlag, 4}}; + + RefPtr<nsImapFlagAndUidState> flagState = new nsImapFlagAndUidState(10); + int32_t numMsgs = sizeof(msgState1) / sizeof(msgState1[0]); + for (int32_t i = 0; i < numMsgs; i++) + flagState->AddUidFlagPair(msgState1[i].uid, msgState1[i].flag, + msgState1[i].index); + + const char* error = MainChecks(flagState, msgState1, numMsgs, 0); + if (error) { + printf("TEST-UNEXPECTED-FAIL | %s | %s\n", __FILE__, error); + return 1; + } + + // Now reset all + flagState->Reset(); + + // This tests adding some messages to a partial uid flag state, + // i.e., CONDSTORE. + struct msgState msgState2[] = {{68, kImapMsgSeenFlag, 69}, + {71, kImapMsgSeenFlag, 70}, + {73, kImapMsgSeenFlag, 71}}; + numMsgs = sizeof(msgState2) / sizeof(msgState2[0]); + for (int32_t i = 0; i < numMsgs; i++) + flagState->AddUidFlagPair(msgState2[i].uid, msgState2[i].flag, + msgState2[i].index); + error = MainChecks(flagState, msgState2, numMsgs, 0); + if (error) { + printf("TEST-UNEXPECTED-FAIL | %s | %s\n", __FILE__, error); + return 1; + } + // Reset all + flagState->Reset(); + // This tests generating a uid string from a non-sequential set of + // messages where the first message is not in the flag state, but the + // missing message from the sequence is in the set. I.e., we're + // generating a uid string from 69,71, but only 70 and 71 are in + // the flag state. + struct msgState msgState3[] = {{10, kImapMsgSeenFlag, 0}, + {69, kImapMsgSeenFlag, 1}, + {70, kImapMsgSeenFlag, 2}, + {71, kImapMsgSeenFlag, 3}}; + + flagState->SetPartialUIDFetch(false); + numMsgs = sizeof(msgState3) / sizeof(msgState3[0]); + for (int32_t i = 0; i < numMsgs; i++) + flagState->AddUidFlagPair(msgState3[i].uid, msgState3[i].flag, + msgState3[i].index); + flagState->ExpungeByIndex(2); + nsCString uidString; + uint32_t msgUids[] = {69, 71}; + uint32_t msgCount = 2; + AllocateImapUidString(&msgUids[0], msgCount, flagState, uidString); + if (!uidString.EqualsLiteral("71")) { + printf("TEST-UNEXPECTED-FAIL | uid String is %s, not 71 | %s\n", + uidString.get(), __FILE__); + return -1; + } + // Reset all + flagState->Reset(); + // This tests the middle message missing from the flag state. + struct msgState msgState4[] = {{10, kImapMsgSeenFlag, 0}, + {69, kImapMsgSeenFlag, 1}, + {70, kImapMsgSeenFlag, 2}, + {71, kImapMsgSeenFlag, 3}, + {73, kImapMsgSeenFlag, 4}}; + + flagState->SetPartialUIDFetch(false); + numMsgs = sizeof(msgState4) / sizeof(msgState4[0]); + for (int32_t i = 0; i < numMsgs; i++) + flagState->AddUidFlagPair(msgState4[i].uid, msgState4[i].flag, + msgState4[i].index); + flagState->ExpungeByIndex(4); + uint32_t msgUids2[] = {69, 71, 73}; + msgCount = 3; + nsCString uidString2; + + AllocateImapUidString(&msgUids2[0], msgCount, flagState, uidString2); + if (!uidString2.EqualsLiteral("69,73")) { + printf("TEST-UNEXPECTED-FAIL | uid String is %s, not 71 | %s\n", + uidString.get(), __FILE__); + return -1; + } + + printf("TEST-PASS | %s | all tests passed\n", __FILE__); + return 0; +} diff --git a/comm/mailnews/imap/test/TestImapHdrXferInfo.cpp b/comm/mailnews/imap/test/TestImapHdrXferInfo.cpp new file mode 100644 index 0000000000..442ca03567 --- /dev/null +++ b/comm/mailnews/imap/test/TestImapHdrXferInfo.cpp @@ -0,0 +1,101 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#include <stdio.h> +#include "TestHarness.h" +#include "nsCOMPtr.h" +#include "msgCore.h" +#include "nsImapProtocol.h" + +// What to check +enum EHdrArrayCheck { eAddNoCheck, eCheckSame }; + +int MainChecks(nsMsgImapHdrXferInfo* hdrInfo, nsIImapHeaderInfo** hdrArray, + EHdrArrayCheck hdrArrayCheck) { + nsCOMPtr<nsIImapHeaderInfo> hdr; + int32_t numHdrs = -1; + + // Check the number of headers initially is zero + if (NS_FAILED(hdrInfo->GetNumHeaders(&numHdrs))) return 1; + + if (numHdrs != 0) return 2; + + // Get a header that doesn't exist + if (hdrInfo->GetHeader(1, getter_AddRefs(hdr)) != NS_ERROR_NULL_POINTER) + return 3; + + int32_t i; + for (i = 0; i < kNumHdrsToXfer; ++i) { + // Now kick off a new one. + hdr = hdrInfo->StartNewHdr(); + if (!hdr) return 4; + + // Check pointers are different or not depending on which cycle we are in + switch (hdrArrayCheck) { + case eAddNoCheck: + hdrArray[i] = hdr; + break; + case eCheckSame: + if (hdrArray[i] != hdr) return 5; + break; + default: + return 1; + } + + if (NS_FAILED(hdrInfo->GetNumHeaders(&numHdrs))) return 1; + + if (numHdrs != i + 1) return 7; + } + + // Now try and get one more (this should return null) + if (hdrInfo->StartNewHdr()) return 8; + + // Now check the number of headers + if (NS_FAILED(hdrInfo->GetNumHeaders(&numHdrs))) return 1; + + if (numHdrs != kNumHdrsToXfer) return 9; + + // Now check our pointers align with those from GetHeader + if (hdrArrayCheck != 2) { + for (i = 0; i < kNumHdrsToXfer; ++i) { + if (NS_FAILED(hdrInfo->GetHeader(i, getter_AddRefs(hdr)))) return 1; + + if (hdr != hdrArray[i]) return 10; + } + } + return 0; +} + +// General note about return values: +// return 1 for a setup or xpcom type failure, return 2 for a real test failure +int main(int argc, char** argv) { + ScopedXPCOM xpcom("TestImapHdrXferInfo.cpp"); + if (xpcom.failed()) return 1; + + RefPtr<nsMsgImapHdrXferInfo> hdrInfo = new nsMsgImapHdrXferInfo(); + // Purposely not reference counted to ensure we get the same pointers the + // second time round MainChecks. + nsIImapHeaderInfo* hdrArray[kNumHdrsToXfer] = {nullptr}; + + int result = MainChecks(hdrInfo, hdrArray, eAddNoCheck); + if (result) { + printf("TEST-UNEXPECTED-FAIL | %s | %d\n", __FILE__, result); + return result; + } + + // Now reset all + hdrInfo->ResetAll(); + + // and repeat + result = MainChecks(hdrInfo, hdrArray, eCheckSame); + if (result) { + // add 100 to differentiate results + result += 100; + printf("TEST-UNEXPECTED-FAIL | %s | %d\n", __FILE__, result); + return result; + } + + printf("TEST-PASS | %s | all tests passed\n", __FILE__); + return result; +} diff --git a/comm/mailnews/imap/test/moz.build b/comm/mailnews/imap/test/moz.build new file mode 100644 index 0000000000..8b6d9424ce --- /dev/null +++ b/comm/mailnews/imap/test/moz.build @@ -0,0 +1,27 @@ +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +XPCSHELL_TESTS_MANIFESTS += [ + "unit/xpcshell-cpp.ini", + "unit/xpcshell.ini", + "unit/xpcshell_maildir-cpp.ini", + "unit/xpcshell_maildir.ini", +] + +LOCAL_INCLUDES += [ + "../../base/src", + "../src", + "/xpcom/tests", +] + +USE_LIBS += [ + "msgbsutl_s", + "msgimap_s", + "nspr", + "xpcomglue_s", + "xul", +] + +OS_LIBS += CONFIG["MOZ_ZLIB_LIBS"] diff --git a/comm/mailnews/imap/test/unit/head_imap_maildir.js b/comm/mailnews/imap/test/unit/head_imap_maildir.js new file mode 100644 index 0000000000..676becd52a --- /dev/null +++ b/comm/mailnews/imap/test/unit/head_imap_maildir.js @@ -0,0 +1,9 @@ +/* import-globals-from head_server.js */ +load("head_server.js"); + +info("Running test with maildir"); + +Services.prefs.setCharPref( + "mail.serverDefaultStoreContractID", + "@mozilla.org/msgstore/maildirstore;1" +); diff --git a/comm/mailnews/imap/test/unit/head_server.js b/comm/mailnews/imap/test/unit/head_server.js new file mode 100644 index 0000000000..b092b9a21b --- /dev/null +++ b/comm/mailnews/imap/test/unit/head_server.js @@ -0,0 +1,201 @@ +// We can be executed from multiple depths +// Provide gDEPTH if not already defined +if (typeof gDEPTH == "undefined") { + var gDEPTH = "../../../../"; +} + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +var { mailTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/MailTestUtils.jsm" +); +var { localAccountUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/LocalAccountUtils.jsm" +); +var { IMAPPump, setupIMAPPump, teardownIMAPPump } = ChromeUtils.import( + "resource://testing-common/mailnews/IMAPpump.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +var CC = Components.Constructor; + +// WebApps.jsm called by ProxyAutoConfig (PAC) requires a valid nsIXULAppInfo. +var { getAppInfo, newAppInfo, updateAppInfo } = ChromeUtils.importESModule( + "resource://testing-common/AppInfo.sys.mjs" +); +updateAppInfo(); + +// Ensure the profile directory is set up +do_get_profile(); + +// Import fakeserver +var { + nsMailServer, + gThreadManager, + fsDebugNone, + fsDebugAll, + fsDebugRecv, + fsDebugRecvSend, +} = ChromeUtils.import("resource://testing-common/mailnews/Maild.jsm"); + +var { + ImapDaemon, + ImapMessage, + configurations, + IMAP_RFC3501_handler, + mixinExtension, + IMAP_RFC2197_extension, + IMAP_RFC2342_extension, + IMAP_RFC3348_extension, + IMAP_RFC4315_extension, + IMAP_RFC5258_extension, + IMAP_RFC2195_extension, +} = ChromeUtils.import("resource://testing-common/mailnews/Imapd.jsm"); +var { AuthPLAIN, AuthLOGIN, AuthCRAM } = ChromeUtils.import( + "resource://testing-common/mailnews/Auth.jsm" +); +var { SmtpDaemon, SMTP_RFC2821_handler } = ChromeUtils.import( + "resource://testing-common/mailnews/Smtpd.jsm" +); + +function makeServer(daemon, infoString, otherProps) { + if (infoString in configurations) { + return makeServer(daemon, configurations[infoString].join(","), otherProps); + } + + function createHandler(d) { + var handler = new IMAP_RFC3501_handler(d); + if (!infoString) { + infoString = "RFC2195"; + } + + var parts = infoString.split(/ *, */); + for (var part of parts) { + if (part.startsWith("RFC")) { + let ext; + switch (part) { + case "RFC2197": + ext = IMAP_RFC2197_extension; + break; + case "RFC2342": + ext = IMAP_RFC2342_extension; + break; + case "RFC3348": + ext = IMAP_RFC3348_extension; + break; + case "RFC4315": + ext = IMAP_RFC4315_extension; + break; + case "RFC5258": + ext = IMAP_RFC5258_extension; + break; + case "RFC2195": + ext = IMAP_RFC2195_extension; + break; + default: + throw new Error("Unknown extension: " + part); + } + mixinExtension(handler, ext); + } + } + if (otherProps) { + for (var prop in otherProps) { + handler[prop] = otherProps[prop]; + } + } + return handler; + } + var server = new nsMailServer(createHandler, daemon); + server.start(); + return server; +} + +function createLocalIMAPServer(port, hostname = "localhost") { + let server = localAccountUtils.create_incoming_server( + "imap", + port, + "user", + "password", + hostname + ); + server.QueryInterface(Ci.nsIImapIncomingServer); + return server; +} + +// <copied from="head_maillocal.js"> +/** + * @param fromServer server.playTransaction + * @param expected ["command", "command", ...] + * @param withParams if false, + * everything apart from the IMAP command will the stripped. + * E.g. 'lsub "" "*"' will be compared as 'lsub'. + * Exception is "authenticate", which also get its first parameter in upper case, + * e.g. "authenticate CRAM-MD5". + */ +function do_check_transaction(fromServer, expected, withParams) { + // If we don't spin the event loop before starting the next test, the readers + // aren't expired. In this case, the "real" real transaction is the last one. + if (fromServer instanceof Array) { + fromServer = fromServer[fromServer.length - 1]; + } + + let realTransaction = []; + for (let i = 0; i < fromServer.them.length; i++) { + var line = fromServer.them[i]; // e.g. '1 login "user" "password"' + var components = line.split(" "); + if (components.length < 2) { + throw new Error("IMAP command in transaction log missing: " + line); + } + if (withParams) { + realTransaction.push(line.substr(components[0].length + 1)); + } else if (components[1].toUpperCase() == "AUTHENTICATE") { + realTransaction.push(components[1] + " " + components[2].toUpperCase()); + } else { + realTransaction.push(components[1]); + } + } + + Assert.equal( + realTransaction.join(", ").toUpperCase(), + expected.join(", ").toUpperCase() + ); +} + +/** + * add a simple message to the IMAP pump mailbox + */ +function addImapMessage() { + let messages = []; + let messageGenerator = new MessageGenerator(); // eslint-disable-line no-undef + messages = messages.concat(messageGenerator.makeMessage()); + let dataUri = Services.io.newURI( + "data:text/plain;base64," + btoa(messages[0].toMessageString()) + ); + let imapMsg = new ImapMessage(dataUri.spec, IMAPPump.mailbox.uidnext++, []); + IMAPPump.mailbox.addMessage(imapMsg); +} + +registerCleanupFunction(function () { + load(gDEPTH + "mailnews/resources/mailShutdown.js"); +}); + +// Setup the SMTP daemon and server +function setupSmtpServerDaemon(handler) { + if (!handler) { + handler = function (d) { + return new SMTP_RFC2821_handler(d); + }; + } + var server = new nsMailServer(handler, new SmtpDaemon()); + return server; +} + +// profile-after-change is not triggered in xpcshell tests, manually run the +// getService to load the correct imap modules. +Cc["@mozilla.org/messenger/imap-module-loader;1"].getService(); diff --git a/comm/mailnews/imap/test/unit/test_ImapResponse.js b/comm/mailnews/imap/test/unit/test_ImapResponse.js new file mode 100644 index 0000000000..4dd60e6d32 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_ImapResponse.js @@ -0,0 +1,288 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var { ImapResponse } = ChromeUtils.import( + "resource:///modules/ImapResponse.jsm" +); +var { ImapUtils } = ChromeUtils.import("resource:///modules/ImapUtils.jsm"); + +/** + * Test CAPABILITY response can be correctly parsed. + */ +add_task(function test_CapabilityResponse() { + let response = new ImapResponse(); + response.parse( + "32 OK [CAPABILITY IMAP4rev1 IDLE STARTTLS AUTH=LOGIN AUTH=PLAIN] server ready\r\n" + ); + + deepEqual(response.authMethods, ["LOGIN", "PLAIN"]); + deepEqual(response.capabilities, ["IMAP4REV1", "IDLE", "STARTTLS"]); + + response = new ImapResponse(); + response.parse("* CAPABILITY IMAP4rev1 ID IDLE STARTTLS AUTH=PLAIN\r\n"); + + deepEqual(response.authMethods, ["PLAIN"]); + deepEqual(response.capabilities, ["IMAP4REV1", "ID", "IDLE", "STARTTLS"]); +}); + +/** + * Test flags from a FETCH response can be correctly parsed. + */ +add_task(function test_FetchResponse_flags() { + let response = new ImapResponse(); + response.parse( + [ + "* 1 FETCH (UID 500 FLAGS (\\Answered \\Seen $Forwarded))", + "* 2 FETCH (UID 600 FLAGS (\\Seen))", + "", + ].join("\r\n") + ); + ok(!response.done); + + response.parse( + ["* 3 FETCH (UID 601 FLAGS ())", "40 OK Fetch completed", ""].join("\r\n") + ); + + ok(response.done); + deepEqual(response.messages[0], { + sequence: 1, + uid: 500, + flags: + ImapUtils.FLAG_ANSWERED | ImapUtils.FLAG_SEEN | ImapUtils.FLAG_FORWARDED, + keywords: "$Forwarded", + customAttributes: {}, + }); + deepEqual(response.messages[1], { + sequence: 2, + uid: 600, + flags: ImapUtils.FLAG_SEEN, + keywords: "", + customAttributes: {}, + }); + deepEqual(response.messages[2], { + sequence: 3, + uid: 601, + flags: 0, + keywords: "", + customAttributes: {}, + }); +}); + +/** + * Test body from a FETCH response can be correctly parsed. + */ +add_task(function test_messageBody() { + let response = new ImapResponse(); + response.parse( + [ + "* 1 FETCH (UID 500 FLAGS (\\Answered \\Seen $Forwarded) BODY[HEADER.FIELDS (FROM TO)] {12}", + "abcd", + "efgh", + ")", + "* 2 FETCH (UID 600 FLAGS (\\Seen) BODY[] {15}", + "Hello ", + "world", + ")", + "40 OK Fetch completed", + "", + ].join("\r\n") + ); + + equal(response.messages[0].body, "abcd\r\nefgh\r\n"); + equal(response.messages[1].body, "Hello \r\nworld\r\n"); +}); + +/** + * Test msg body spanning multiple chuncks can be correctly parsed. + */ +add_task(function test_messageBodyIncremental() { + let response = new ImapResponse(); + // Chunk 1. + response.parse( + [ + "* 1 FETCH (UID 500 FLAGS (\\Answered \\Seen $Forwarded) BODY[HEADER.FIELDS (FROM TO)] {12}", + "abcd", + "efgh", + ")", + "* 2 FETCH (UID 600 FLAGS (\\Seen) BODY[] {15}", + "Hel", + ].join("\r\n") + ); + equal(response.messages[0].body, "abcd\r\nefgh\r\n"); + ok(!response.done); + + // Chunk 2. + response.parse("lo \r\nworld\r\n"); + ok(!response.done); + + // Chunk 3. + response.parse(")\r\n40 OK Fetch completed\r\n"); + ok(response.done); + equal(response.messages[1].body, "Hello \r\nworld\r\n"); +}); + +/** + * Test FLAGS response can be correctly parsed. + */ +add_task(function test_FlagsResponse() { + let response = new ImapResponse(); + response.parse( + [ + "* FLAGS (\\Seen \\Draft $Forwarded)", + "* OK [PERMANENTFLAGS (\\Seen \\Draft $Forwarded \\*)] Flags permitted.", + "* 6 EXISTS", + "* OK [UNSEEN 2] First unseen.", + "* OK [UIDVALIDITY 1594877893] UIDs valid", + "* OK [UIDNEXT 625] Predicted next UID", + "* OK [HIGHESTMODSEQ 1148] Highest", + "42 OK [READ-WRITE] Select completed", + "", + ].join("\r\n") + ); + + equal( + response.flags, + ImapUtils.FLAG_SEEN | ImapUtils.FLAG_DRAFT | ImapUtils.FLAG_FORWARDED + ); + equal( + response.permanentflags, + ImapUtils.FLAG_SEEN | + ImapUtils.FLAG_DRAFT | + ImapUtils.FLAG_FORWARDED | + ImapUtils.FLAG_LABEL | + ImapUtils.FLAG_MDN_SENT | + ImapUtils.FLAG_FORWARDED | + ImapUtils.FLAG_SUPPORT_USER_FLAG + ); + equal(response.highestmodseq, 1148); + equal(response.exists, 6); +}); + +/** + * Test mailbox updates can be correctly parsed. + */ +add_task(function test_MailboxResponse() { + let response = new ImapResponse(); + response.parse("* 7 EXISTS\r\n"); + response.parse("* 1 EXPUNGE\r\n* 3 EXPUNGE\r\n"); + equal(response.exists, 7); + deepEqual(response.expunged, [1, 3]); +}); + +/** + * Test LIST response can be correctly parsed. + */ +add_task(function test_ListResponse() { + let response = new ImapResponse(); + response.parse( + [ + '* LIST (\\Subscribed \\NoInferiors \\Marked \\Trash) "/" "Trash"', + '* LIST () "/" "a \\"b\\" c"', + '* LIST (\\Subscribed) "/" INBOX', + "84 OK List completed (0.002 + 0.000 + 0.001 secs).", + "", + ].join("\r\n") + ); + equal(response.mailboxes.length, 3); + deepEqual(response.mailboxes[0], { + name: "Trash", + delimiter: "/", + flags: + ImapUtils.FLAG_SUBSCRIBED | + ImapUtils.FLAG_NO_INFERIORS | + ImapUtils.FLAG_HAS_NO_CHILDREN | + ImapUtils.FLAG_MARKED | + ImapUtils.FLAG_IMAP_TRASH | + ImapUtils.FLAG_IMAP_XLIST_TRASH, + }); + deepEqual(response.mailboxes[1], { + name: 'a "b" c', + delimiter: "/", + flags: 0, + }); + deepEqual(response.mailboxes[2], { + name: "INBOX", + delimiter: "/", + flags: ImapUtils.FLAG_SUBSCRIBED, + }); +}); + +/** + * Test folder names containg [] or () or "" can be correctly parsed. + */ +add_task(function test_parseFolderNames() { + let response = new ImapResponse(); + response.parse( + [ + '* LSUB () "/" "[Gmail]"', + '* LSUB () "/" "[Gmail]/All Mail"', + '* LSUB () "/" "[Gmail]/Sent"', + '* LSUB () "/" "[a(b)])"', + '* LSUB () "/" "a \\"b \\"c\\""', + "84 OK LSUB completed", + "", + ].join("\r\n") + ); + equal(response.mailboxes.length, 5); + deepEqual( + response.mailboxes.map(x => x.name), + ["[Gmail]", "[Gmail]/All Mail", "[Gmail]/Sent", "[a(b)])", 'a "b "c"'] + ); +}); + +/** + * Test STATUS response can be correctly parsed. + */ +add_task(function test_StatusResponse() { + let response = new ImapResponse(); + response.parse( + '* STATUS "sub folder 2" (UIDNEXT 2 MESSAGES 1 UNSEEN 1 RECENT 0)\r\n' + ); + deepEqual(response.attributes, { + mailbox: "sub folder 2", + uidnext: 2, + messages: 1, + unseen: 1, + recent: 0, + }); +}); + +/** + * Test GETQUOTAROOT response can be correctly parsed. + */ +add_task(function test_QuotaResponse() { + let response = new ImapResponse(); + response.parse( + ["* QUOTAROOT Sent INBOX", "* QUOTA INBOX (STORAGE 123 456)", ""].join( + "\r\n" + ) + ); + deepEqual(response.quotaRoots, ["INBOX"]); + deepEqual(response.quotas, [["INBOX", "STORAGE", 123, 456]]); +}); + +/** + * Test IDLE and DONE response can be correctly parsed. + */ +add_task(function test_IdleDoneResponse() { + let response = new ImapResponse(); + response.parse("+ idling\r\n"); + deepEqual( + [response.tag, response.status, response.done], + ["+", "idling", true] + ); + + response = new ImapResponse(); + response.parse(["+ idling", "75 OK Completed", ""].join("\r\n")); + deepEqual([response.tag, response.status, response.done], [75, "OK", true]); +}); + +/** + * Test SEARCH response can be correctly parsed. + */ +add_task(function test_SearchResponse() { + let response = new ImapResponse(); + response.parse("* SEARCH 1 4 9\r\n90 OK SEARCH COMPLETED\r\n"); + deepEqual(response.search, [1, 4, 9]); +}); diff --git a/comm/mailnews/imap/test/unit/test_autosync_date_constraints.js b/comm/mailnews/imap/test/unit/test_autosync_date_constraints.js new file mode 100644 index 0000000000..101413d491 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_autosync_date_constraints.js @@ -0,0 +1,88 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Test autosync date constraints + */ + +var { MessageGenerator } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageGenerator.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +var gMsgImapInboxFolder; + +// Adds some messages directly to a mailbox (eg new mail) +function addMessagesToServer(messages, mailbox) { + // Create the ImapMessages and store them on the mailbox + messages.forEach(function (message) { + let dataUri = "data:text/plain," + message.toMessageString(); + mailbox.addMessage(new ImapMessage(dataUri, mailbox.uidnext++, [])); + }); +} + +add_setup(function () { + Services.prefs.setIntPref("mail.server.server1.autosync_max_age_days", 4); + + setupIMAPPump(); + + gMsgImapInboxFolder = IMAPPump.inbox.QueryInterface(Ci.nsIMsgImapMailFolder); + // these hacks are required because we've created the inbox before + // running initial folder discovery, and adding the folder bails + // out before we set it as verified online, so we bail out, and + // then remove the INBOX folder since it's not verified. + gMsgImapInboxFolder.hierarchyDelimiter = "/"; + gMsgImapInboxFolder.verifiedAsOnlineFolder = true; + + // Add a couple of messages to the INBOX + // this is synchronous, afaik + let messageGenerator = new MessageGenerator(); + + // build up a diverse list of messages + let messages = []; + messages = messages.concat( + messageGenerator.makeMessage({ age: { days: 2, hours: 1 } }) + ); + messages = messages.concat( + messageGenerator.makeMessage({ age: { days: 8, hours: 1 } }) + ); + messages = messages.concat( + messageGenerator.makeMessage({ age: { days: 10, hours: 1 } }) + ); + + addMessagesToServer(messages, IMAPPump.daemon.getMailbox("INBOX")); +}); + +add_task(async function downloadForOffline() { + // ...and download for offline use. + // This downloads all messages, ignoring the autosync age constraints. + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.downloadAllForOffline(listener, null); + await listener.promise; +}); + +add_task(function test_applyRetentionSettings() { + IMAPPump.inbox.applyRetentionSettings(); + let enumerator = IMAPPump.inbox.msgDatabase.enumerateMessages(); + if (enumerator) { + let now = new Date(); + let dateInSeconds = now.getSeconds(); + let cutOffDateInSeconds = dateInSeconds - 5 * 60 * 24; + for (let header of enumerator) { + if (header instanceof Ci.nsIMsgDBHdr) { + if (header.dateInSeconds < cutOffDateInSeconds) { + Assert.equal(header.getStringProperty("pendingRemoval"), "1"); + } else { + Assert.equal(header.getStringProperty("pendingRemoval"), ""); + } + } + } + } +}); + +add_task(function endTest() { + teardownIMAPPump(); +}); diff --git a/comm/mailnews/imap/test/unit/test_bccProperty.js b/comm/mailnews/imap/test/unit/test_bccProperty.js new file mode 100644 index 0000000000..42795245c7 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_bccProperty.js @@ -0,0 +1,52 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Test to ensure that BCC gets added to message headers on IMAP download + * + * adapted from test_downloadOffline.js + * + * original author Kent James <kent@caspia.com> + */ + +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +var gFileName = "draft1"; +var gMsgFile = do_get_file("../../../data/" + gFileName); + +add_setup(async function () { + setupIMAPPump(); + + /* + * Ok, prelude done. Read the original message from disk + * (through a file URI), and add it to the Inbox. + */ + let msgfileuri = Services.io + .newFileURI(gMsgFile) + .QueryInterface(Ci.nsIFileURL); + + IMAPPump.mailbox.addMessage( + new ImapMessage(msgfileuri.spec, IMAPPump.mailbox.uidnext++, []) + ); + + // ...and download for offline use. + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.downloadAllForOffline(listener, null); + await listener.promise; +}); + +add_task(function checkBccs() { + // locate the new message by enumerating through the database + for (let hdr of IMAPPump.inbox.msgDatabase.enumerateMessages()) { + Assert.ok(hdr.bccList.includes("Another Person")); + Assert.ok(hdr.bccList.includes("<u1@example.com>")); + Assert.ok(!hdr.bccList.includes("IDoNotExist")); + } +}); + +add_task(function endTest() { + teardownIMAPPump(); +}); diff --git a/comm/mailnews/imap/test/unit/test_bug460636.js b/comm/mailnews/imap/test/unit/test_bug460636.js new file mode 100644 index 0000000000..6cb8b60056 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_bug460636.js @@ -0,0 +1,82 @@ +/* + * Test bug 460636 - nsMsgSaveAsListener sometimes inserts extra LF characters + */ + +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +var gSavedMsgFile; + +var gIMAPService = Cc[ + "@mozilla.org/messenger/messageservice;1?type=imap" +].getService(Ci.nsIMsgMessageService); + +var gFileName = "bug460636"; +var gMsgFile = do_get_file("../../../data/" + gFileName); + +add_task(async function run_the_test() { + await setup(); + await checkSavedMessage(); + teardown(); +}); + +async function setup() { + setupIMAPPump(); + + // Ok, prelude done. Read the original message from disk + // (through a file URI), and add it to the Inbox. + var msgfileuri = Services.io + .newFileURI(gMsgFile) + .QueryInterface(Ci.nsIFileURL); + + IMAPPump.mailbox.addMessage( + new ImapMessage(msgfileuri.spec, IMAPPump.mailbox.uidnext++, []) + ); + let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, promiseUrlListener); + await promiseUrlListener.promise; + + // Save the message to a local file. IMapMD corresponds to + // <profile_dir>/mailtest/ImapMail (where fakeserver puts the IMAP mailbox + // files). If we pass the test, we'll remove the file afterwards + // (cf. UrlListener), otherwise it's kept in IMapMD. + gSavedMsgFile = Services.dirsvc.get("IMapMD", Ci.nsIFile); + gSavedMsgFile.append(gFileName + ".eml"); + + // From nsIMsgMessageService.idl: + // void SaveMessageToDisk(in string aMessageURI, in nsIFile aFile, + // in boolean aGenerateDummyEnvelope, + // in nsIUrlListener aUrlListener, out nsIURI aURL, + // in boolean canonicalLineEnding, + // in nsIMsgWindow aMsgWindow); + // Enforcing canonicalLineEnding (i.e., CRLF) makes sure that the + let promiseUrlListener2 = new PromiseTestUtils.PromiseUrlListener(); + gIMAPService.SaveMessageToDisk( + "imap-message://user@localhost/INBOX#" + (IMAPPump.mailbox.uidnext - 1), + gSavedMsgFile, + false, + promiseUrlListener2, + {}, + true, + null + ); + await promiseUrlListener2.promise; +} + +async function checkSavedMessage() { + Assert.equal( + await IOUtils.readUTF8(gMsgFile.path), + await IOUtils.readUTF8(gSavedMsgFile.path) + ); +} + +function teardown() { + try { + gSavedMsgFile.remove(false); + } catch (ex) { + dump(ex); + do_throw(ex); + } + teardownIMAPPump(); +} diff --git a/comm/mailnews/imap/test/unit/test_chunkLastLF.js b/comm/mailnews/imap/test/unit/test_chunkLastLF.js new file mode 100644 index 0000000000..003320ad2d --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_chunkLastLF.js @@ -0,0 +1,108 @@ +/* -*- Mode: JavaScript; 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 ***** */ + +/* + * Test content length for the IMAP protocol. This focuses on necko URLs + * that are run externally. + */ + +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +var gFile = do_get_file("../../../data/bug92111b"); +var gIMAPDaemon, gIMAPServer, gIMAPIncomingServer; + +// Adds some messages directly to a mailbox (eg new mail) +function addMessageToServer(file, mailbox) { + let URI = Services.io.newFileURI(file).QueryInterface(Ci.nsIFileURL); + let msg = new ImapMessage(URI.spec, mailbox.uidnext++, []); + // underestimate the actual file size, like some IMAP servers do + msg.setSize(file.fileSize - 55); + mailbox.addMessage(msg); +} + +add_task(async function verifyContentLength() { + // Disable new mail notifications + Services.prefs.setBoolPref("mail.biff.play_sound", false); + Services.prefs.setBoolPref("mail.biff.show_alert", false); + Services.prefs.setBoolPref("mail.biff.show_tray_icon", false); + Services.prefs.setBoolPref("mail.biff.animate_dock_icon", false); + Services.prefs.setBoolPref( + "mail.server.server1.autosync_offline_stores", + false + ); + + // Crank down the message chunk size to make test cases easier + Services.prefs.setBoolPref("mail.server.default.fetch_by_chunks", true); + Services.prefs.setIntPref("mail.imap.chunk_size", 1000); + Services.prefs.setIntPref("mail.imap.min_chunk_size_threshold", 1500); + Services.prefs.setIntPref("mail.imap.chunk_add", 0); + + // set up IMAP fakeserver and incoming server + gIMAPDaemon = new ImapDaemon(); + gIMAPServer = makeServer(gIMAPDaemon, ""); + gIMAPIncomingServer = createLocalIMAPServer(gIMAPServer.port); + + // The server doesn't support more than one connection + Services.prefs.setIntPref("mail.server.server1.max_cached_connections", 1); + // We aren't interested in downloading messages automatically + Services.prefs.setBoolPref("mail.server.server1.download_on_biff", false); + + dump("adding message to server\n"); + // Add a message to the IMAP server + addMessageToServer(gFile, gIMAPDaemon.getMailbox("INBOX")); + + let imapS = Cc[ + "@mozilla.org/messenger/messageservice;1?type=imap" + ].getService(Ci.nsIMsgMessageService); + + let uri = imapS.getUrlForUri("imap-message://user@localhost/INBOX#1"); + + // Get a channel from this URI, and check its content length + let channel = Services.io.newChannelFromURI( + uri, + null, + Services.scriptSecurityManager.getSystemPrincipal(), + null, + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER + ); + + let promiseStreamListener = new PromiseTestUtils.PromiseStreamListener(); + + // Read all the contents + channel.asyncOpen(promiseStreamListener, null); + let streamData = (await promiseStreamListener.promise).replace(/\r\n/g, "\n"); + + // Now check whether our stream listener got the right bytes + // First, clean up line endings to avoid CRLF vs. LF differences + let origData = (await IOUtils.readUTF8(gFile.path)).replace(/\r\n/g, "\n"); + Assert.equal(origData.length, streamData.length); + Assert.equal(origData, streamData); + + // Now try an attachment. &part=1.2 + // let attachmentURL = Services.io.newURI(neckoURL.value.spec + "&part=1.2", + // null, null); + // let attachmentChannel = Services.io.newChannelFromURI(attachmentURL, + // null, + // Services.scriptSecurityManager.getSystemPrincipal(), + // null, + // Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + // Ci.nsIContentPolicy.TYPE_OTHER); + // Currently attachments have their content length set to the length of the + // entire message + // do_check_eq(attachmentChannel.contentLength, gFile.fileSize); + + gIMAPIncomingServer.closeCachedConnections(); + gIMAPServer.stop(); + let thread = gThreadManager.currentThread; + while (thread.hasPendingEvents()) { + thread.processNextEvent(true); + } +}); diff --git a/comm/mailnews/imap/test/unit/test_compactOfflineStore.js b/comm/mailnews/imap/test/unit/test_compactOfflineStore.js new file mode 100644 index 0000000000..9c3f0ef898 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_compactOfflineStore.js @@ -0,0 +1,194 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Test to ensure that compacting offline stores works correctly with imap folders + * and returns success. + */ + +var { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); +var { MessageGenerator } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageGenerator.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +Services.prefs.setCharPref( + "mail.serverDefaultStoreContractID", + "@mozilla.org/msgstore/berkeleystore;1" +); + +// Globals +var gRootFolder; +var gImapInboxOfflineStoreSize; + +var gMsgFile1 = do_get_file("../../../data/bugmail10"); +var gMsgFile2 = do_get_file("../../../data/bugmail11"); +// var gMsgFile3 = do_get_file("../../../data/draft1"); +var gMsgFile4 = do_get_file("../../../data/bugmail7"); +var gMsgFile5 = do_get_file("../../../data/bugmail6"); + +// Copied straight from the example files +var gMsgId1 = "200806061706.m56H6RWT004933@mrapp54.mozilla.org"; +var gMsgId2 = "200804111417.m3BEHTk4030129@mrapp51.mozilla.org"; +// var gMsgId3 = "4849BF7B.2030800@example.com"; +var gMsgId4 = "bugmail7.m47LtAEf007542@mrapp51.mozilla.org"; +var gMsgId5 = "bugmail6.m47LtAEf007542@mrapp51.mozilla.org"; + +// Adds some messages directly to a mailbox (e.g. new mail). +function addMessagesToServer(messages, mailbox) { + // For every message we have, we need to convert it to a file:/// URI + messages.forEach(function (message) { + let URI = Services.io + .newFileURI(message.file) + .QueryInterface(Ci.nsIFileURL); + // Create the ImapMessage and store it on the mailbox. + mailbox.addMessage(new ImapMessage(URI.spec, mailbox.uidnext++, [])); + }); +} + +function addGeneratedMessagesToServer(messages, mailbox) { + // Create the ImapMessages and store them on the mailbox + messages.forEach(function (message) { + let dataUri = Services.io.newURI( + "data:text/plain;base64," + btoa(message.toMessageString()) + ); + mailbox.addMessage(new ImapMessage(dataUri.spec, mailbox.uidnext++, [])); + }); +} + +function checkOfflineStore(prevOfflineStoreSize) { + let enumerator = IMAPPump.inbox.msgDatabase.enumerateMessages(); + if (enumerator) { + for (let header of enumerator) { + // this will verify that the message in the offline store + // starts with "From " - otherwise, it returns an error. + if ( + header instanceof Ci.nsIMsgDBHdr && + header.flags & Ci.nsMsgMessageFlags.Offline + ) { + IMAPPump.inbox.getLocalMsgStream(header).close(); + } + } + } + // check that the offline store shrunk by at least 100 bytes. + // (exact calculation might be fragile). + Assert.ok(prevOfflineStoreSize > IMAPPump.inbox.filePath.fileSize + 100); +} + +add_setup(function () { + setupIMAPPump(); + + gRootFolder = IMAPPump.incomingServer.rootFolder; + // 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. + IMAPPump.inbox.hierarchyDelimiter = "/"; + IMAPPump.inbox.verifiedAsOnlineFolder = true; + + let messageGenerator = new MessageGenerator(); + let messages = []; + for (let i = 0; i < 50; i++) { + messages = messages.concat(messageGenerator.makeMessage()); + } + + addGeneratedMessagesToServer(messages, IMAPPump.daemon.getMailbox("INBOX")); + + // Add a couple of messages to the INBOX + // this is synchronous, afaik + addMessagesToServer( + [ + { file: gMsgFile1, messageId: gMsgId1 }, + { file: gMsgFile4, messageId: gMsgId4 }, + { file: gMsgFile2, messageId: gMsgId2 }, + { file: gMsgFile5, messageId: gMsgId5 }, + ], + IMAPPump.daemon.getMailbox("INBOX") + ); +}); + +add_task(async function downloadForOffline() { + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.downloadAllForOffline(listener, null); + await listener.promise; +}); + +add_task(async function markOneMsgDeleted() { + // mark a message deleted, and then do a compact of just + // that folder. + let msgHdr = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID(gMsgId5); + // store the deleted flag + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.storeImapFlags(0x0008, true, [msgHdr.messageKey], listener); + await listener.promise; +}); + +add_task(async function compactOneFolder() { + IMAPPump.incomingServer.deleteModel = Ci.nsMsgImapDeleteModels.IMAPDelete; + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.compact(listener, null); + await listener.promise; +}); + +add_task(async function test_deleteOneMessage() { + // check that nstmp file has been cleaned up. + let tmpFile = gRootFolder.filePath; + tmpFile.append("nstmp"); + Assert.ok(!tmpFile.exists()); + // Deleting one message. + IMAPPump.incomingServer.deleteModel = Ci.nsMsgImapDeleteModels.MoveToTrash; + let msgHdr = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID(gMsgId1); + let copyListener = new PromiseTestUtils.PromiseCopyListener(); + IMAPPump.inbox.deleteMessages( + [msgHdr], + null, + false, + true, + copyListener, + false + ); + await copyListener.promise; + + let trashFolder = gRootFolder.getChildNamed("Trash"); + // hack to force uid validity to get initialized for trash. + trashFolder.updateFolder(null); +}); + +add_task(async function compactOfflineStore() { + gImapInboxOfflineStoreSize = IMAPPump.inbox.filePath.fileSize; + let listener = new PromiseTestUtils.PromiseUrlListener(); + gRootFolder.compactAll(listener, null); + await listener.promise; +}); + +add_task(function test_checkCompactionResult1() { + checkOfflineStore(gImapInboxOfflineStoreSize); +}); + +add_task(async function pendingRemoval() { + let msgHdr = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID(gMsgId2); + IMAPPump.inbox.markPendingRemoval(msgHdr, true); + gImapInboxOfflineStoreSize = IMAPPump.inbox.filePath.fileSize; + let listener = new PromiseTestUtils.PromiseUrlListener(); + gRootFolder.compactAll(listener, null); + await listener.promise; +}); + +add_task(function test_checkCompactionResult2() { + let tmpFile = gRootFolder.filePath; + tmpFile.append("nstmp"); + Assert.ok(!tmpFile.exists()); + checkOfflineStore(gImapInboxOfflineStoreSize); + let msgHdr = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID(gMsgId2); + Assert.equal(msgHdr.flags & Ci.nsMsgMessageFlags.Offline, 0); +}); + +add_task(function endTest() { + gRootFolder = null; + teardownIMAPPump(); +}); diff --git a/comm/mailnews/imap/test/unit/test_converterImap.js b/comm/mailnews/imap/test/unit/test_converterImap.js new file mode 100644 index 0000000000..a05e236ce6 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_converterImap.js @@ -0,0 +1,110 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var { convertMailStoreTo } = ChromeUtils.import( + "resource:///modules/mailstoreConverter.jsm" +); + +var { FileUtils } = ChromeUtils.importESModule( + "resource://gre/modules/FileUtils.sys.mjs" +); +var { MessageGenerator } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageGenerator.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +// Globals +let gMsgFile1 = do_get_file("../../../data/bugmail10"); +let gTestMsgs = [gMsgFile1, gMsgFile1, gMsgFile1, gMsgFile1]; + +function checkConversion(aSource, aTarget) { + for (let sourceContent of aSource.directoryEntries) { + let sourceContentName = sourceContent.leafName; + let ext = sourceContentName.slice(-4); + let targetFile = FileUtils.File( + PathUtils.join(aTarget.path, sourceContentName) + ); + + // Checking path. + if (ext == ".msf" || ext == ".dat") { + Assert.ok(targetFile.exists()); + } else if (sourceContent.isDirectory()) { + Assert.ok(targetFile.exists()); + checkConversion(sourceContent, targetFile); + } else { + Assert.ok(targetFile.exists()); + let cur = FileUtils.File(PathUtils.join(targetFile.path, "cur")); + Assert.ok(cur.exists()); + let tmp = FileUtils.File(PathUtils.join(targetFile.path, "tmp")); + Assert.ok(tmp.exists()); + if (targetFile.leafName == "INBOX") { + let curContentsCount = [...cur.directoryEntries].length; + Assert.equal(curContentsCount, 8); + } + } + } +} + +var EventTarget = function () { + this.dispatchEvent = function (aEvent) { + if (aEvent.type == "progress") { + dump("Progress: " + aEvent.detail + "\n"); + } + }; +}; + +add_setup(async function () { + // Force mbox. + Services.prefs.setCharPref( + "mail.serverDefaultStoreContractID", + "@mozilla.org/msgstore/berkeleystore;1" + ); + + setupIMAPPump(); + + // 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. + IMAPPump.inbox.hierarchyDelimiter = "/"; + IMAPPump.inbox.verifiedAsOnlineFolder = true; + + // Add our test messages to the INBOX. + let mailbox = IMAPPump.daemon.getMailbox("INBOX"); + for (let file of gTestMsgs) { + let URI = Services.io.newFileURI(file).QueryInterface(Ci.nsIFileURL); + mailbox.addMessage(new ImapMessage(URI.spec, mailbox.uidnext++, [])); + } +}); + +add_task(async function downloadForOffline() { + // Download for offline use. + let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.downloadAllForOffline(promiseUrlListener, null); + await promiseUrlListener.promise; +}); + +add_task(async function convert() { + let mailstoreContractId = Services.prefs.getCharPref( + "mail.server." + IMAPPump.incomingServer.key + ".storeContractID" + ); + let eventTarget = new EventTarget(); + let originalRootFolder = IMAPPump.incomingServer.rootFolder.filePath; + await convertMailStoreTo( + mailstoreContractId, + IMAPPump.incomingServer, + eventTarget + ); + // Conversion done. + let newRootFolder = IMAPPump.incomingServer.rootFolder.filePath; + checkConversion(originalRootFolder, newRootFolder); + let newRootFolderMsf = FileUtils.File(newRootFolder.path + ".msf"); + Assert.ok(newRootFolderMsf.exists()); +}); + +add_task(function endTest() { + teardownIMAPPump(); +}); diff --git a/comm/mailnews/imap/test/unit/test_copyThenMove.js b/comm/mailnews/imap/test/unit/test_copyThenMove.js new file mode 100644 index 0000000000..9abdb5e45d --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_copyThenMove.js @@ -0,0 +1,200 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This file extends test_imapFolderCopy.js to test message + * moves from a local folder to an IMAP folder. + * + * Original Author: Kent James <kent@caspia.com> + */ + +var { MessageGenerator } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageGenerator.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +var gEmptyLocal1, gEmptyLocal2; +var gLastKey; +var gMessages = []; + +add_setup(function () { + // Turn off autosync_offline_stores because + // fetching messages is invoked after copying the messages. + // (i.e. The fetching process will be invoked after OnStopCopy) + // It will cause crash with an assertion + // (ASSERTION: tried to add duplicate listener: 'index == -1') on teardown. + Services.prefs.setBoolPref( + "mail.server.default.autosync_offline_stores", + false + ); + + setupIMAPPump(); + + gEmptyLocal1 = localAccountUtils.rootFolder.createLocalSubfolder("empty 1"); + gEmptyLocal2 = localAccountUtils.rootFolder.createLocalSubfolder("empty 2"); + + // 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. + IMAPPump.inbox.hierarchyDelimiter = "/"; + IMAPPump.inbox.verifiedAsOnlineFolder = true; +}); + +add_task(async function copyFolder1() { + let copyListener = new PromiseTestUtils.PromiseCopyListener({ + SetMessageKey(aKey) { + gLastKey = aKey; + }, + }); + MailServices.copy.copyFolder( + gEmptyLocal1, + IMAPPump.inbox, + false, + copyListener, + null + ); + await copyListener.promise; +}); + +add_task(async function copyFolder2() { + let copyListener = new PromiseTestUtils.PromiseCopyListener({ + SetMessageKey(aKey) { + gLastKey = aKey; + }, + }); + MailServices.copy.copyFolder( + gEmptyLocal2, + IMAPPump.inbox, + false, + copyListener, + null + ); + await copyListener.promise; +}); + +add_task(async function getLocalMessage1() { + let copyListener = new PromiseTestUtils.PromiseCopyListener({ + SetMessageKey(aKey) { + gLastKey = aKey; + }, + }); + let file = do_get_file("../../../data/bugmail1"); + MailServices.copy.copyFileMessage( + file, + localAccountUtils.inboxFolder, + null, + false, + 0, + "", + copyListener, + null + ); + await copyListener.promise; +}); + +add_task(async function getLocalMessage2() { + gMessages.push(localAccountUtils.inboxFolder.GetMessageHeader(gLastKey)); + let file = do_get_file("../../../data/draft1"); + let copyListener = new PromiseTestUtils.PromiseCopyListener({ + SetMessageKey(aKey) { + gLastKey = aKey; + }, + }); + MailServices.copy.copyFileMessage( + file, + localAccountUtils.inboxFolder, + null, + false, + 0, + "", + copyListener, + null + ); + await copyListener.promise; +}); + +add_task(async function copyMessages() { + gMessages.push(localAccountUtils.inboxFolder.GetMessageHeader(gLastKey)); + let folder1 = IMAPPump.inbox.getChildNamed("empty 1"); + let copyListener = new PromiseTestUtils.PromiseCopyListener({ + SetMessageKey(aKey) { + gLastKey = aKey; + }, + }); + MailServices.copy.copyMessages( + localAccountUtils.inboxFolder, + gMessages, + folder1, + false, + copyListener, + null, + false + ); + await copyListener.promise; +}); + +add_task(async function moveMessages() { + let folder2 = IMAPPump.inbox.getChildNamed("empty 2"); + let copyListener = new PromiseTestUtils.PromiseCopyListener({ + SetMessageKey(aKey) { + gLastKey = aKey; + }, + }); + MailServices.copy.copyMessages( + localAccountUtils.inboxFolder, + gMessages, + folder2, + true, + copyListener, + null, + false + ); + await copyListener.promise; +}); + +add_task(async function update1() { + let folder1 = IMAPPump.inbox + .getChildNamed("empty 1") + .QueryInterface(Ci.nsIMsgImapMailFolder); + let listener = new PromiseTestUtils.PromiseUrlListener(); + folder1.updateFolderWithListener(null, listener); + await listener.promise; +}); + +add_task(async function update2() { + let folder2 = IMAPPump.inbox + .getChildNamed("empty 2") + .QueryInterface(Ci.nsIMsgImapMailFolder); + let listener = new PromiseTestUtils.PromiseUrlListener(); + folder2.updateFolderWithListener(null, listener); + await listener.promise; +}); + +add_task(function verifyFolders() { + let folder1 = IMAPPump.inbox.getChildNamed("empty 1"); + Assert.equal(folderCount(folder1), 2); + let folder2 = IMAPPump.inbox.getChildNamed("empty 2"); + Assert.ok(folder2 !== null); + // folder 1 and 2 should each now have two messages in them. + Assert.ok(folder1 !== null); + Assert.equal(folderCount(folder2), 2); + // The local inbox folder should now be empty, since the second + // operation was a move. + Assert.equal(folderCount(localAccountUtils.inboxFolder), 0); +}); +add_task(function endTest() { + gMessages = []; + teardownIMAPPump(); +}); + +function folderCount(folder) { + return [...folder.msgDatabase.enumerateMessages()].length; +} diff --git a/comm/mailnews/imap/test/unit/test_customCommandReturnsFetchResponse.js b/comm/mailnews/imap/test/unit/test_customCommandReturnsFetchResponse.js new file mode 100644 index 0000000000..abf45ab3e3 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_customCommandReturnsFetchResponse.js @@ -0,0 +1,134 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Test to ensure that imap customCommandResult function works properly + * Bug 778246 + */ + +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +// IMAP pump + +// Globals + +// Messages to load must have CRLF line endings, that is Windows style +var gMessageFileName = "bugmail10"; // message file used as the test message +var gMessage, gExpectedLength; + +var gCustomList = ["Custom1", "Custom2", "Custom3"]; + +var gMsgWindow = Cc["@mozilla.org/messenger/msgwindow;1"].createInstance( + Ci.nsIMsgWindow +); + +setupIMAPPump("CUSTOM1"); + +add_setup(async function () { + Services.prefs.setBoolPref( + "mail.server.server1.autosync_offline_stores", + false + ); + // Load and update a message in the imap fake server. + + gMessage = new ImapMessage( + specForFileName(gMessageFileName), + IMAPPump.mailbox.uidnext++, + [] + ); + gMessage.xCustomList = []; + IMAPPump.mailbox.addMessage(gMessage); + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, listener); + await listener.promise; +}); + +add_task(async function testStoreCustomList() { + let msgHdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox); + gExpectedLength = gCustomList.length; + let uri = IMAPPump.inbox.issueCommandOnMsgs( + "STORE", + msgHdr.messageKey + " X-CUSTOM-LIST (" + gCustomList.join(" ") + ")", + gMsgWindow + ); + uri.QueryInterface(Ci.nsIMsgMailNewsUrl); + // Listens for response from customCommandResult request for X-CUSTOM-LIST. + let storeCustomListSetListener = new PromiseTestUtils.PromiseUrlListener({ + OnStopRunningUrl(aUrl, aExitCode) { + aUrl.QueryInterface(Ci.nsIImapUrl); + Assert.equal( + aUrl.customCommandResult, + "(" + gMessage.xCustomList.join(" ") + ")" + ); + Assert.equal(gMessage.xCustomList.length, gExpectedLength); + }, + }); + uri.RegisterListener(storeCustomListSetListener); + await storeCustomListSetListener.promise; +}); + +add_task(async function testStoreMinusCustomList() { + let msgHdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox); + gExpectedLength--; + let uri = IMAPPump.inbox.issueCommandOnMsgs( + "STORE", + msgHdr.messageKey + " -X-CUSTOM-LIST (" + gCustomList[0] + ")", + gMsgWindow + ); + uri.QueryInterface(Ci.nsIMsgMailNewsUrl); + // Listens for response from customCommandResult request for X-CUSTOM-LIST. + let storeCustomListRemovedListener = new PromiseTestUtils.PromiseUrlListener({ + OnStopRunningUrl(aUrl, aExitCode) { + aUrl.QueryInterface(Ci.nsIImapUrl); + Assert.equal( + aUrl.customCommandResult, + "(" + gMessage.xCustomList.join(" ") + ")" + ); + Assert.equal(gMessage.xCustomList.length, gExpectedLength); + }, + }); + uri.RegisterListener(storeCustomListRemovedListener); + await storeCustomListRemovedListener.promise; +}); + +add_task(async function testStorePlusCustomList() { + let msgHdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox); + gExpectedLength++; + let uri = IMAPPump.inbox.issueCommandOnMsgs( + "STORE", + msgHdr.messageKey + ' +X-CUSTOM-LIST ("Custom4")', + gMsgWindow + ); + uri.QueryInterface(Ci.nsIMsgMailNewsUrl); + let storeCustomListAddedListener = new PromiseTestUtils.PromiseUrlListener({ + OnStopRunningUrl(aUrl, aExitCode) { + aUrl.QueryInterface(Ci.nsIImapUrl); + Assert.equal( + aUrl.customCommandResult, + "(" + gMessage.xCustomList.join(" ") + ")" + ); + Assert.equal(gMessage.xCustomList.length, gExpectedLength); + }, + }); + uri.RegisterListener(storeCustomListAddedListener); + await storeCustomListAddedListener.promise; +}); + +// Cleanup at end +add_task(function endTest() { + teardownIMAPPump(); +}); + +/* + * helper functions + */ + +// given a test file, return the file uri spec +function specForFileName(aFileName) { + let file = do_get_file("../../../data/" + aFileName); + let msgfileuri = Services.io.newFileURI(file).QueryInterface(Ci.nsIFileURL); + return msgfileuri.spec; +} diff --git a/comm/mailnews/imap/test/unit/test_dontStatNoSelect.js b/comm/mailnews/imap/test/unit/test_dontStatNoSelect.js new file mode 100644 index 0000000000..7c05cc0c1f --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_dontStatNoSelect.js @@ -0,0 +1,149 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests that checking folders for new mail with STATUS +// doesn't try to STAT noselect folders. + +var gServer, gImapServer; +var gIMAPInbox; +var gFolder2Mailbox; +var gFolder1, gFolder2; + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { MessageGenerator } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageGenerator.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +add_setup(function () { + var daemon = new ImapDaemon(); + daemon.createMailbox("folder 1", { subscribed: true }); + let folder1Mailbox = daemon.getMailbox("folder 1"); + folder1Mailbox.flags.push("\\Noselect"); + daemon.createMailbox("folder 2", { subscribed: true }); + gFolder2Mailbox = daemon.getMailbox("folder 2"); + addMessageToFolder(gFolder2Mailbox); + gServer = makeServer(daemon, ""); + + gImapServer = createLocalIMAPServer(gServer.port); + + // Bug 1050840: check a newly created server has the default number of connections + Assert.equal(gImapServer.maximumConnectionsNumber, 5); + gImapServer.maximumConnectionsNumber = 1; + + localAccountUtils.loadLocalMailAccount(); + + // We need an identity so that updateFolder doesn't fail + let localAccount = MailServices.accounts.createAccount(); + let identity = MailServices.accounts.createIdentity(); + localAccount.addIdentity(identity); + localAccount.defaultIdentity = identity; + localAccount.incomingServer = localAccountUtils.incomingServer; + + // Let's also have another account, using the same identity + let imapAccount = MailServices.accounts.createAccount(); + imapAccount.addIdentity(identity); + imapAccount.defaultIdentity = identity; + imapAccount.incomingServer = gImapServer; + MailServices.accounts.defaultAccount = imapAccount; + + // Get the folder list... + gImapServer.performExpand(null); + gServer.performTest("SUBSCRIBE"); + // pref tuning: one connection only, turn off notifications + // Make sure no biff notifications happen + Services.prefs.setBoolPref("mail.biff.play_sound", false); + Services.prefs.setBoolPref("mail.biff.show_alert", false); + Services.prefs.setBoolPref("mail.biff.show_tray_icon", false); + Services.prefs.setBoolPref("mail.biff.animate_dock_icon", false); + + let rootFolder = gImapServer.rootFolder; + gIMAPInbox = rootFolder.getFolderWithFlags(Ci.nsMsgFolderFlags.Inbox); + gFolder1 = rootFolder.getChildNamed("folder 1"); + gFolder2 = rootFolder.getChildNamed("folder 2"); + gFolder1.setFlag(Ci.nsMsgFolderFlags.CheckNew); + gFolder2.setFlag(Ci.nsMsgFolderFlags.CheckNew); +}); + +add_task(function checkStatSelect() { + // imap fake server's resetTest resets the authentication state - charming. + // So poke the _test member directly. + gServer._test = true; + gIMAPInbox.getNewMessages(null, null); + gServer.performTest("STATUS"); + // We want to wait for the STATUS to be really done before we issue + // more STATUS commands, so we do a NOOP on the + // INBOX, and since we only have one connection with the fake server, + // that will essentially serialize things. + gServer._test = true; + gIMAPInbox.updateFolder(null); + gServer.performTest("NOOP"); +}); + +add_task(async function checkStatNoSelect() { + // folder 2 should have been stat'd, but not folder 1. All we can really check + // is that folder 2 was stat'd and that its unread msg count is 1 + Assert.equal(gFolder2.getNumUnread(false), 1); + addMessageToFolder(gFolder2Mailbox); + gFolder1.clearFlag(Ci.nsMsgFolderFlags.ImapNoselect); + gServer._test = true; + + let folderListener = new FolderListener(); + + // we've cleared the ImapNoselect flag, so we will attempt to STAT folder 1, + // which will fail. So we verify that we go on and STAT folder 2, and that + // it picks up the message we added to it above. + MailServices.mailSession.AddFolderListener( + folderListener, + Ci.nsIFolderListener.boolPropertyChanged + ); + gIMAPInbox.getNewMessages(null, null); + // Wait for the folder listener to get told about new messages. + await folderListener.promise; +}); + +add_task(function endTest() { + Assert.equal(gFolder2.getNumUnread(false), 2); + + // Clean up the server in preparation + gServer.resetTest(); + gImapServer.closeCachedConnections(); + gServer.performTest(); + gServer.stop(); +}); + +function addMessageToFolder(mbox) { + // make a couple of messages + let messages = []; + let gMessageGenerator = new MessageGenerator(); + messages = messages.concat(gMessageGenerator.makeMessage()); + + let msgURI = Services.io.newURI( + "data:text/plain;base64," + btoa(messages[0].toMessageString()) + ); + let message = new ImapMessage(msgURI.spec, mbox.uidnext++); + mbox.addMessage(message); +} + +function FolderListener() { + this._promise = new Promise(resolve => { + this._resolve = resolve; + }); +} + +FolderListener.prototype = { + onFolderBoolPropertyChanged(aItem, aProperty, aOldValue, aNewValue) { + // This means that the STAT on "folder 2" has finished. + if (aProperty == "NewMessages" && aNewValue) { + this._resolve(); + } + }, + get promise() { + return this._promise; + }, +}; diff --git a/comm/mailnews/imap/test/unit/test_downloadOffline.js b/comm/mailnews/imap/test/unit/test_downloadOffline.js new file mode 100644 index 0000000000..931995ee01 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_downloadOffline.js @@ -0,0 +1,75 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Test to ensure that downloadAllForOffline works correctly with imap folders + * and returns success. + */ + +/* import-globals-from ../../../test/resources/logHelper.js */ +/* import-globals-from ../../../test/resources/MessageGenerator.jsm */ +load("../../../resources/logHelper.js"); +load("../../../resources/MessageGenerator.jsm"); + +var gFileName = "bug460636"; +var gMsgFile = do_get_file("../../../data/" + gFileName); + +var tests = [setup, downloadAllForOffline, verifyDownloaded, teardownIMAPPump]; + +async function setup() { + setupIMAPPump(); + + /* + * Ok, prelude done. Read the original message from disk + * (through a file URI), and add it to the Inbox. + */ + let msgfileuri = Services.io + .newFileURI(gMsgFile) + .QueryInterface(Ci.nsIFileURL); + + IMAPPump.mailbox.addMessage( + new ImapMessage(msgfileuri.spec, IMAPPump.mailbox.uidnext++, []) + ); + + let messages = []; + let gMessageGenerator = new MessageGenerator(); + messages = messages.concat(gMessageGenerator.makeMessage()); + let dataUri = Services.io.newURI( + "data:text/plain;base64," + btoa(messages[0].toMessageString()) + ); + let imapMsg = new ImapMessage(dataUri.spec, IMAPPump.mailbox.uidnext++, []); + imapMsg.setSize(5000); + IMAPPump.mailbox.addMessage(imapMsg); + + // ...and download for offline use. + let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.downloadAllForOffline(promiseUrlListener, null); + await promiseUrlListener.promise; +} + +async function downloadAllForOffline() { + let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.downloadAllForOffline(promiseUrlListener, null); + await promiseUrlListener.promise; +} + +function verifyDownloaded() { + // verify that the message headers have the offline flag set. + for (let header of IMAPPump.inbox.msgDatabase.enumerateMessages()) { + // Verify that each message has been downloaded and looks OK. + if ( + header instanceof Ci.nsIMsgDBHdr && + header.flags & Ci.nsMsgMessageFlags.Offline + ) { + IMAPPump.inbox.getLocalMsgStream(header).close(); + } else { + do_throw("Message not downloaded for offline use"); + } + } +} + +function run_test() { + tests.forEach(x => add_task(x)); + run_next_test(); +} diff --git a/comm/mailnews/imap/test/unit/test_fetchCustomAttribute.js b/comm/mailnews/imap/test/unit/test_fetchCustomAttribute.js new file mode 100644 index 0000000000..631e925cfa --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_fetchCustomAttribute.js @@ -0,0 +1,105 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Test to ensure that imap fetchCustomMsgAttribute function works properly + */ + +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +// IMAP pump + +// Globals + +// Messages to load must have CRLF line endings, that is Windows style +var gMessage = "bugmail10"; // message file used as the test message + +var gCustomValue = "Custom"; +var gCustomList = ["Custom1", "Custom2", "Custom3"]; + +var gMsgWindow = Cc["@mozilla.org/messenger/msgwindow;1"].createInstance( + Ci.nsIMsgWindow +); + +add_setup(async function () { + setupIMAPPump("CUSTOM1"); + Services.prefs.setBoolPref( + "mail.server.server1.autosync_offline_stores", + false + ); + // Load and update a message in the imap fake server. + let message = new ImapMessage( + specForFileName(gMessage), + IMAPPump.mailbox.uidnext++, + [] + ); + message.xCustomValue = gCustomValue; + message.xCustomList = gCustomList; + IMAPPump.mailbox.addMessage(message); + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, listener); + await listener.promise; +}); + +// Used to verify that nsIServerResponseParser.msg_fetch() can handle +// not in a parenthesis group - Bug 750012 +add_task(async function testFetchCustomValue() { + let msgHdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox); + let uri = IMAPPump.inbox.fetchCustomMsgAttribute( + "X-CUSTOM-VALUE", + msgHdr.messageKey, + gMsgWindow + ); + uri.QueryInterface(Ci.nsIMsgMailNewsUrl); + // Listens for response from fetchCustomMsgAttribute request for X-CUSTOM-VALUE. + let fetchCustomValueListener = new PromiseTestUtils.PromiseUrlListener({ + OnStopRunningUrl(aUrl, aExitCode) { + aUrl.QueryInterface(Ci.nsIImapUrl); + Assert.equal(aUrl.customAttributeResult, gCustomValue); + }, + }); + uri.RegisterListener(fetchCustomValueListener); + await fetchCustomValueListener.promise; +}); + +// Used to verify that nsIServerResponseParser.msg_fetch() can handle a parenthesis group - Bug 735542 +add_task(async function testFetchCustomList() { + let msgHdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox); + let uri = IMAPPump.inbox.fetchCustomMsgAttribute( + "X-CUSTOM-LIST", + msgHdr.messageKey, + gMsgWindow + ); + uri.QueryInterface(Ci.nsIMsgMailNewsUrl); + // Listens for response from fetchCustomMsgAttribute request for X-CUSTOM-VALUE. + let fetchCustomListListener = new PromiseTestUtils.PromiseUrlListener({ + OnStopRunningUrl(aUrl, aExitCode) { + aUrl.QueryInterface(Ci.nsIImapUrl); + Assert.equal( + aUrl.customAttributeResult, + "(" + gCustomList.join(" ") + ")" + ); + }, + }); + uri.RegisterListener(fetchCustomListListener); + await fetchCustomListListener.promise; +}); + +// Cleanup at end +add_task(function endTest() { + teardownIMAPPump(); +}); + +/* + * helper functions + */ + +// given a test file, return the file uri spec +function specForFileName(aFileName) { + let file = do_get_file("../../../data/" + aFileName); + let msgfileuri = Services.io.newFileURI(file).QueryInterface(Ci.nsIFileURL); + return msgfileuri.spec; +} diff --git a/comm/mailnews/imap/test/unit/test_filterCustomHeaders.js b/comm/mailnews/imap/test/unit/test_filterCustomHeaders.js new file mode 100644 index 0000000000..05e2a71b66 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_filterCustomHeaders.js @@ -0,0 +1,66 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * This file tests hdr parsing in the filter running context, specifically + * filters on custom headers. + * See https://bugzilla.mozilla.org/show_bug.cgi?id=655578 + * for more info. + * + * Original author: David Bienvenu <bienvenu@mozilla.com> + */ + +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +// IMAP pump + +var { IMAPPump, setupIMAPPump, teardownIMAPPump } = ChromeUtils.import( + "resource://testing-common/mailnews/IMAPpump.jsm" +); + +add_setup(async function () { + setupIMAPPump(); + // Create a test filter. + let filterList = IMAPPump.incomingServer.getFilterList(null); + let filter = filterList.createFilter("test list-id"); + let searchTerm = filter.createTerm(); + searchTerm.attrib = Ci.nsMsgSearchAttrib.OtherHeader + 1; + searchTerm.op = Ci.nsMsgSearchOp.Contains; + let value = searchTerm.value; + value.attrib = Ci.nsMsgSearchAttrib.OtherHeader; + value.str = "gnupg-users.gnupg.org"; + searchTerm.value = value; + searchTerm.booleanAnd = false; + searchTerm.arbitraryHeader = "List-Id"; + filter.appendTerm(searchTerm); + filter.enabled = true; + + // create a mark read action + let action = filter.createAction(); + action.type = Ci.nsMsgFilterAction.MarkRead; + filter.appendAction(action); + filterList.insertFilterAt(0, filter); + Services.prefs.setBoolPref( + "mail.server.default.autosync_offline_stores", + false + ); + let file = do_get_file("../../../data/bugmail19"); + let msgfileuri = Services.io.newFileURI(file).QueryInterface(Ci.nsIFileURL); + + IMAPPump.mailbox.addMessage( + new ImapMessage(msgfileuri.spec, IMAPPump.mailbox.uidnext++, []) + ); + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, listener); + await listener.promise; +}); + +add_task(function checkFilterResults() { + let msgHdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox); + Assert.ok(msgHdr.isRead); + IMAPPump.server.performTest("UID STORE"); + teardownIMAPPump(); +}); diff --git a/comm/mailnews/imap/test/unit/test_filterNeedsBody.js b/comm/mailnews/imap/test/unit/test_filterNeedsBody.js new file mode 100644 index 0000000000..e7720b3222 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_filterNeedsBody.js @@ -0,0 +1,113 @@ +/* + * This file tests the needsBody attribute added to a + * custom filter action in bug 555051. + * + * Original author: Kent James <kent@caspia.com> + * adapted from test_imapFilterActions.js + */ + +/* import-globals-from ../../../test/resources/logHelper.js */ +load("../../../resources/logHelper.js"); + +// Globals +var gFilter; // a message filter with a subject search +var gAction; // current message action (reused) +var gMessage = "draft1"; // message file used as the test message + +// Definition of tests +var tests = [ + setupIMAPPump, + setup, + function NeedsBodyTrue() { + gAction.type = Ci.nsMsgFilterAction.Custom; + gAction.customId = "mailnews@mozilla.org#testOffline"; + actionTestOffline.needsBody = true; + gAction.strValue = "true"; + }, + runFilterAction, + function NeedsBodyFalse() { + gAction.type = Ci.nsMsgFilterAction.Custom; + gAction.customId = "mailnews@mozilla.org#testOffline"; + actionTestOffline.needsBody = false; + gAction.strValue = "false"; + }, + runFilterAction, + teardownIMAPPump, +]; + +function setup() { + // Create a test filter. + let filterList = IMAPPump.incomingServer.getFilterList(null); + gFilter = filterList.createFilter("test offline"); + let searchTerm = gFilter.createTerm(); + searchTerm.matchAll = true; + + gFilter.appendTerm(searchTerm); + gFilter.enabled = true; + + // an action that can be modified by tests + gAction = gFilter.createAction(); + + // add the custom actions + MailServices.filters.addCustomAction(actionTestOffline); +} + +// basic preparation done for each test +async function runFilterAction() { + let filterList = IMAPPump.incomingServer.getFilterList(null); + while (filterList.filterCount) { + filterList.removeFilterAt(0); + } + if (gFilter) { + gFilter.clearActionList(); + if (gAction) { + gFilter.appendAction(gAction); + filterList.insertFilterAt(0, gFilter); + } + } + IMAPPump.mailbox.addMessage( + new ImapMessage(specForFileName(gMessage), IMAPPump.mailbox.uidnext++, []) + ); + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, listener); + await listener.promise; +} + +function run_test() { + tests.forEach(x => add_task(x)); + run_next_test(); +} + +// custom action to test offline status +var actionTestOffline = { + id: "mailnews@mozilla.org#testOffline", + name: "test if offline", + applyAction(aMsgHdrs, aActionValue, aListener, aType, aMsgWindow) { + for (let msgHdr of aMsgHdrs) { + let isOffline = msgHdr.flags & Ci.nsMsgMessageFlags.Offline; + Assert.equal(!!isOffline, aActionValue == "true"); + } + }, + isValidForType(type, scope) { + return true; + }, + + validateActionValue(value, folder, type) { + return null; + }, + + allowDuplicates: false, + + needsBody: false, // set during test setup +}; + +/* + * helper functions + */ + +// given a test file, return the file uri spec +function specForFileName(aFileName) { + let file = do_get_file(gDEPTH + "mailnews/data/" + aFileName); + let msgfileuri = Services.io.newFileURI(file).QueryInterface(Ci.nsIFileURL); + return msgfileuri.spec; +} diff --git a/comm/mailnews/imap/test/unit/test_folderOfflineFlags.js b/comm/mailnews/imap/test/unit/test_folderOfflineFlags.js new file mode 100644 index 0000000000..b1c26069c6 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_folderOfflineFlags.js @@ -0,0 +1,108 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test that the folders that should get flagged for offline use do, and that + * those that shouldn't don't. + */ + +// make SOLO_FILE="test_folderOfflineFlags.js" -C mailnews/imap/test check-one + +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +/** + * Setup the mailboxes that will be used for this test. + */ +add_setup(async function () { + setupIMAPPump("GMail"); + + IMAPPump.mailbox.subscribed = true; + IMAPPump.mailbox.specialUseFlag = "\\Inbox"; + IMAPPump.daemon.createMailbox("[Gmail]", { + flags: ["\\Noselect"], + subscribed: true, + }); + IMAPPump.daemon.createMailbox("[Gmail]/All Mail", { + specialUseFlag: "\\AllMail", + subscribed: true, + }); + IMAPPump.daemon.createMailbox("[Gmail]/Drafts", { + specialUseFlag: "\\Drafts", + subscribed: true, + }); + IMAPPump.daemon.createMailbox("[Gmail]/Sent", { + specialUseFlag: "\\Sent", + subscribed: true, + }); + IMAPPump.daemon.createMailbox("[Gmail]/Spam", { + specialUseFlag: "\\Spam", + subscribed: true, + }); + IMAPPump.daemon.createMailbox("[Gmail]/Starred", { + specialUseFlag: "\\Starred", + subscribed: true, + }); + IMAPPump.daemon.createMailbox("[Gmail]/Trash", { + specialUseFlag: "\\Trash", + subscribed: true, + }); + IMAPPump.daemon.createMailbox("folder1", { subscribed: true }); + IMAPPump.daemon.createMailbox("folder2", { subscribed: true }); + + // select the inbox to force folder discovery, etc. + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, listener); + await listener.promise; +}); + +/** + * Test that folders generally are marked for offline use by default. + */ +add_task(function testGeneralFoldersOffline() { + Assert.ok(IMAPPump.inbox.getFlag(Ci.nsMsgFolderFlags.Offline)); + + let gmail = IMAPPump.incomingServer.rootFolder.getChildNamed("[Gmail]"); + + let allmail = gmail.getFolderWithFlags(Ci.nsMsgFolderFlags.Archive); + Assert.ok(allmail.getFlag(Ci.nsMsgFolderFlags.Offline)); + + let drafts = gmail.getFolderWithFlags(Ci.nsMsgFolderFlags.Drafts); + Assert.ok(drafts.getFlag(Ci.nsMsgFolderFlags.Offline)); + + let sent = gmail.getFolderWithFlags(Ci.nsMsgFolderFlags.SentMail); + Assert.ok(sent.getFlag(Ci.nsMsgFolderFlags.Offline)); + + let rootFolder = IMAPPump.incomingServer.rootFolder; + + let folder1 = rootFolder.getChildNamed("folder1"); + Assert.ok(folder1.getFlag(Ci.nsMsgFolderFlags.Offline)); + + let folder2 = rootFolder.getChildNamed("folder2"); + Assert.ok(folder2.getFlag(Ci.nsMsgFolderFlags.Offline)); +}); + +/** + * Test that Trash isn't flagged for offline use by default. + */ +add_task(function testTrashNotOffline() { + let gmail = IMAPPump.incomingServer.rootFolder.getChildNamed("[Gmail]"); + let trash = gmail.getFolderWithFlags(Ci.nsMsgFolderFlags.Trash); + Assert.ok(!trash.getFlag(Ci.nsMsgFolderFlags.Offline)); +}); + +/** + * Test that Junk isn't flagged for offline use by default. + */ +add_task(function testJunkNotOffline() { + let gmail = IMAPPump.incomingServer.rootFolder.getChildNamed("[Gmail]"); + let spam = gmail.getFolderWithFlags(Ci.nsMsgFolderFlags.Junk); + Assert.ok(!spam.getFlag(Ci.nsMsgFolderFlags.Offline)); +}); + +/** Cleanup at the end. */ +add_task(function endTest() { + teardownIMAPPump(); +}); diff --git a/comm/mailnews/imap/test/unit/test_gmailAttributes.js b/comm/mailnews/imap/test/unit/test_gmailAttributes.js new file mode 100644 index 0000000000..5b424462c5 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_gmailAttributes.js @@ -0,0 +1,91 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Test to ensure that, in case of GMail server, fetching of custom GMail + * attributes works properly. + * + * Bug 721316 + * + * See https://bugzilla.mozilla.org/show_bug.cgi?id=721316 + * for more info. + * + * Original Author: Atul Jangra<atuljangra66@gmail.com> + */ + +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +// Messages to load must have CRLF line endings, that is Windows style +var gMessage = "bugmail10"; // message file used as the test message + +var gXGmMsgid = "1278455344230334865"; +var gXGmThrid = "1266894439832287888"; +var gXGmLabels = '(\\Inbox \\Sent Important "Muy Importante" foo)'; + +add_setup(async function () { + Services.prefs.setBoolPref( + "mail.server.server1.autosync_offline_stores", + false + ); + setupIMAPPump("GMail"); + IMAPPump.mailbox.specialUseFlag = "\\Inbox"; + IMAPPump.mailbox.subscribed = true; + + // need all mail folder to identify this as gmail server. + IMAPPump.daemon.createMailbox("[Gmail]", { flags: ["\\NoSelect"] }); + IMAPPump.daemon.createMailbox("[Gmail]/All Mail", { + subscribed: true, + specialUseFlag: "\\AllMail", + }); + // Load and update a message in the imap fake server. + let message = new ImapMessage( + specForFileName(gMessage), + IMAPPump.mailbox.uidnext++, + [] + ); + message.xGmMsgid = gXGmMsgid; + message.xGmThrid = gXGmThrid; + message.xGmLabels = gXGmLabels; + IMAPPump.mailbox.addMessage(message); + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, listener); + await listener.promise; +}); + +add_task(function testFetchXGmMsgid() { + let msgHdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox); + let val = msgHdr.getStringProperty("X-GM-MSGID"); + Assert.equal(val, gXGmMsgid); +}); + +add_task(function testFetchXGmThrid() { + let msgHdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox); + let val = msgHdr.getStringProperty("X-GM-THRID"); + Assert.equal(val, gXGmThrid); +}); + +add_task(function testFetchXGmLabels() { + let msgHdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox); + let val = msgHdr.getStringProperty("X-GM-LABELS"); + // We need to remove the starting "(" and ending ")" from gXGmLabels while comparing + Assert.equal(val, gXGmLabels.substring(1, gXGmLabels.length - 1)); +}); + +// Cleanup at end +add_task(function endTest() { + teardownIMAPPump(); +}); + +/* + * helper functions + */ + +// given a test file, return the file uri spec +function specForFileName(aFileName) { + let file = do_get_file("../../../data/" + aFileName); + let msgfileuri = Services.io.newFileURI(file).QueryInterface(Ci.nsIFileURL); + return msgfileuri.spec; +} diff --git a/comm/mailnews/imap/test/unit/test_gmailOfflineMsgStore.js b/comm/mailnews/imap/test/unit/test_gmailOfflineMsgStore.js new file mode 100644 index 0000000000..5ab93d1ad9 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_gmailOfflineMsgStore.js @@ -0,0 +1,229 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Test to ensure that, in case of GMail server, fetching of a message, which is + * already present in offline store of some folder, from a folder doesn't make + * us add it to the offline store twice(in this case, in general it can be any + * number of times). + * + * Bug 721316 + * + * See https://bugzilla.mozilla.org/show_bug.cgi?id=721316 + * for more info. + * + * Original Author: Atul Jangra<atuljangra66@gmail.com> + */ + +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +// Messages to load must have CRLF line endings, that is Windows style. + +var gMessage1 = "bugmail10"; // message file used as the test message for Inbox and fooFolder. +var gXGmMsgid1 = "1278455344230334865"; +var gXGmThrid1 = "1266894439832287888"; +// We need to have different X-GM-LABELS for different folders. I am doing it here manually, but this issue will be tackled in Bug 781443. +var gXGmLabels11 = '( "\\\\Sent" foo bar)'; // for message in Inbox. +var gXGmLabels12 = '("\\\\Inbox" "\\\\Sent" bar)'; // for message in fooFolder. +var gMsgId1 = "200806061706.m56H6RWT004933@mrapp54.mozilla.org"; + +var gMessage2 = "bugmail11"; // message file used as the test message for fooFolder. +var gMsgId2 = "200804111417.m3BEHTk4030129@mrapp51.mozilla.org"; +var gXGmMsgid2 = "1278455345230334555"; +var gXGmThrid2 = "1266894639832287111"; +var gXGmLabels2 = '("\\\\Sent")'; + +var fooBox; +var fooFolder; + +var gImapInboxOfflineStoreSizeInitial; +var gImapInboxOfflineStoreSizeFinal; + +var gFooOfflineStoreSizeInitial; +var gFooOfflineStoreSizeFinal; + +add_setup(async function () { + // We aren't interested in downloading messages automatically. + Services.prefs.setBoolPref( + "mail.server.server1.autosync_offline_stores", + false + ); + Services.prefs.setBoolPref("mail.server.server1.offline_download", true); + Services.prefs.setBoolPref("mail.biff.alert.show_preview", false); + + setupIMAPPump("GMail"); + + IMAPPump.mailbox.specialUseFlag = "\\Inbox"; + IMAPPump.mailbox.subscribed = true; + + // need all mail folder to identify this as gmail server. + IMAPPump.daemon.createMailbox("[Gmail]", { flags: ["\\NoSelect"] }); + IMAPPump.daemon.createMailbox("[Gmail]/All Mail", { + subscribed: true, + specialUseFlag: "\\AllMail", + }); + + // Creating the mailbox "foo" + IMAPPump.daemon.createMailbox("foo", { subscribed: true }); + fooBox = IMAPPump.daemon.getMailbox("foo"); + + // Add message1 to inbox. + let message = new ImapMessage( + specForFileName(gMessage1), + IMAPPump.mailbox.uidnext++, + [] + ); + message.messageId = gMsgId1; + message.xGmMsgid = gXGmMsgid1; + message.xGmThrid = gXGmThrid1; + message.xGmLabels = gXGmLabels11; // With labels excluding "//INBOX". + IMAPPump.mailbox.addMessage(message); + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, listener); + await listener.promise; +}); + +add_task(async function selectInboxMsg() { + // Select mesasage1 from inbox which makes message1 available in offline store. + let imapService = Cc[ + "@mozilla.org/messenger/messageservice;1?type=imap" + ].getService(Ci.nsIMsgMessageService); + let db = IMAPPump.inbox.msgDatabase; + let msg1 = db.getMsgHdrForMessageID(gMsgId1); + let streamListener = new PromiseTestUtils.PromiseStreamListener(); + let urlListener = new PromiseTestUtils.PromiseUrlListener(); + imapService.loadMessage( + IMAPPump.inbox.getUriForMsg(msg1), + streamListener, + null, + urlListener, + false + ); + await urlListener.promise; +}); + +add_task(async function StreamMessageInbox() { + // Stream message1 from inbox. + let newMsgHdr = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID(gMsgId1); + let msgURI = newMsgHdr.folder.getUriForMsg(newMsgHdr); + let msgServ = MailServices.messageServiceFromURI(msgURI); + let streamLister = new PromiseTestUtils.PromiseStreamListener(); + msgServ.streamMessage(msgURI, streamLister, null, null, false, "", false); + gImapInboxOfflineStoreSizeInitial = IMAPPump.inbox.filePath.fileSize; // Initial Size of Inbox. + await streamLister.promise; +}); + +add_task(async function createAndUpdate() { + let rootFolder = IMAPPump.incomingServer.rootFolder; + fooFolder = rootFolder + .getChildNamed("foo") + .QueryInterface(Ci.nsIMsgImapMailFolder); // We have created the mailbox earlier. + let listener = new PromiseTestUtils.PromiseUrlListener(); + fooFolder.updateFolderWithListener(null, listener); + await listener.promise; +}); + +add_task(function addToFoo() { + // Adding our test message. + let message = new ImapMessage( + specForFileName(gMessage1), + fooBox.uidnext++, + [] + ); + message.messageId = gMsgId1; + message.xGmMsgid = gXGmMsgid1; + message.xGmThrid = gXGmThrid1; + message.xGmLabels = gXGmLabels12; // With labels excluding "foo". + fooBox.addMessage(message); + // Adding another message so that fooFolder behaves as LocalFolder while calculating it's size. + let message1 = new ImapMessage( + specForFileName(gMessage2), + fooBox.uidnext++, + [] + ); + message1.messageId = gMsgId2; + message1.xGmMsgid = gXGmMsgid2; + message1.xGmThrid = gXGmThrid2; + message1.xGmLabels = gXGmLabels2; + fooBox.addMessage(message1); +}); + +add_task(async function updateFoo() { + let listener = new PromiseTestUtils.PromiseUrlListener(); + fooFolder.updateFolderWithListener(null, listener); + await listener.promise; +}); + +add_task(async function selectFooMsg() { + // Select message2 from fooFolder, which makes fooFolder a local folder. + let imapService = Cc[ + "@mozilla.org/messenger/messageservice;1?type=imap" + ].getService(Ci.nsIMsgMessageService); + let msg1 = fooFolder.msgDatabase.getMsgHdrForMessageID(gMsgId2); + let streamListener = new PromiseTestUtils.PromiseStreamListener(); + let urlListener = new PromiseTestUtils.PromiseUrlListener(); + imapService.loadMessage( + fooFolder.getUriForMsg(msg1), + streamListener, + null, + urlListener, + false + ); + await urlListener.promise; +}); + +add_task(async function StreamMessageFoo() { + // Stream message2 from fooFolder. + let newMsgHdr = fooFolder.msgDatabase.getMsgHdrForMessageID(gMsgId2); + let msgURI = newMsgHdr.folder.getUriForMsg(newMsgHdr); + let msgServ = MailServices.messageServiceFromURI(msgURI); + let streamListener = new PromiseTestUtils.PromiseStreamListener(); + msgServ.streamMessage(msgURI, streamListener, null, null, false, "", false); + gFooOfflineStoreSizeInitial = fooFolder.filePath.fileSize; + await streamListener.promise; +}); + +add_task(async function crossStreaming() { + /** + * Streaming message1 from fooFolder. message1 is present in + * offline store of inbox. We now test that streaming the message1 + * from fooFolder does not make us add message1 to offline store of + * fooFolder. We check this by comparing the sizes of inbox and fooFolder + * before and after streaming. + */ + let msg2 = fooFolder.msgDatabase.getMsgHdrForMessageID(gMsgId1); + Assert.ok(msg2 !== null); + let msgURI = fooFolder.getUriForMsg(msg2); + let msgServ = MailServices.messageServiceFromURI(msgURI); + // pass true for aLocalOnly since message should be in offline store of Inbox. + let streamListener = new PromiseTestUtils.PromiseStreamListener(); + msgServ.streamMessage(msgURI, streamListener, null, null, false, "", true); + await streamListener.promise; + gFooOfflineStoreSizeFinal = fooFolder.filePath.fileSize; + gImapInboxOfflineStoreSizeFinal = IMAPPump.inbox.filePath.fileSize; + Assert.equal(gFooOfflineStoreSizeFinal, gFooOfflineStoreSizeInitial); + Assert.equal( + gImapInboxOfflineStoreSizeFinal, + gImapInboxOfflineStoreSizeInitial + ); +}); + +add_task(function endTest() { + teardownIMAPPump(); +}); + +/* + * helper functions + */ + +/** + * Given a test file, return the file uri spec. + */ +function specForFileName(aFileName) { + let file = do_get_file("../../../data/" + aFileName); + let msgfileuri = Services.io.newFileURI(file).QueryInterface(Ci.nsIFileURL); + return msgfileuri.spec; +} diff --git a/comm/mailnews/imap/test/unit/test_imapAttachmentSaves.js b/comm/mailnews/imap/test/unit/test_imapAttachmentSaves.js new file mode 100644 index 0000000000..d3e3951492 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_imapAttachmentSaves.js @@ -0,0 +1,199 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Tests imap save and detach attachments. + */ + +var { MessageGenerator } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageGenerator.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +// javascript mime emitter functions +var { MsgHdrToMimeMessage } = ChromeUtils.import( + "resource:///modules/gloda/MimeMessage.jsm" +); + +var kAttachFileName = "bob.txt"; +function SaveAttachmentCallback() { + this.attachments = null; + this._promise = new Promise((resolve, reject) => { + this._resolve = resolve; + this._reject = reject; + }); +} + +SaveAttachmentCallback.prototype = { + callback: function saveAttachmentCallback_callback(aMsgHdr, aMimeMessage) { + this.attachments = aMimeMessage.allAttachments; + this._resolve(); + }, + get promise() { + return this._promise; + }, +}; +var gCallbackObject = new SaveAttachmentCallback(); + +// Dummy message window so we can say the inbox is open in a window. +var dummyMsgWindow = Cc["@mozilla.org/messenger/msgwindow;1"].createInstance( + Ci.nsIMsgWindow +); + +function MsgsDeletedListener() { + this._promise = new Promise(resolve => (this._resolve = resolve)); +} +MsgsDeletedListener.prototype = { + msgsDeleted(aMsgArray) { + this._resolve(); + }, + get promise() { + return this._promise; + }, +}; +var trackDeletionMessageListener = new MsgsDeletedListener(); + +add_setup(function () { + setupIMAPPump(); + + // Add folder listeners that will capture async events + MailServices.mfn.addListener( + trackDeletionMessageListener, + Ci.nsIMsgFolderNotificationService.msgsDeleted + ); + + // We need to register the dummyMsgWindow so that when we've finished running + // the append url, in nsImapMailFolder::OnStopRunningUrl, we'll think the + // Inbox is open in a folder and update it, which the detach code relies + // on to finish the detach. + + dummyMsgWindow.openFolder = IMAPPump.inbox; + MailServices.mailSession.AddMsgWindow(dummyMsgWindow); +}); + +// load and update a message in the imap fake server +add_task(async function loadImapMessage() { + let gMessageGenerator = new MessageGenerator(); + // create a synthetic message with attachment + let smsg = gMessageGenerator.makeMessage({ + attachments: [{ filename: kAttachFileName, body: "I like cheese!" }], + }); + + let msgURI = Services.io.newURI( + "data:text/plain;base64," + btoa(smsg.toMessageString()) + ); + let imapInbox = IMAPPump.daemon.getMailbox("INBOX"); + let message = new ImapMessage(msgURI.spec, imapInbox.uidnext++, []); + IMAPPump.mailbox.addMessage(message); + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, listener); + await listener.promise; + Assert.equal(1, IMAPPump.inbox.getTotalMessages(false)); + let msgHdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox); + Assert.ok(msgHdr instanceof Ci.nsIMsgDBHdr); +}); + +// process the message through mime +add_task(async function startMime() { + let msgHdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox); + + MsgHdrToMimeMessage( + msgHdr, + gCallbackObject, + gCallbackObject.callback, + true // allowDownload + ); + await gCallbackObject.promise; +}); + +// detach any found attachments +add_task(async function startDetach() { + let msgHdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox); + let msgURI = msgHdr.folder.generateMessageURI(msgHdr.messageKey); + + let messenger = Cc["@mozilla.org/messenger;1"].createInstance( + Ci.nsIMessenger + ); + let attachment = gCallbackObject.attachments[0]; + + messenger.detachAttachmentsWOPrompts( + do_get_profile(), + [attachment.contentType], + [attachment.url], + [attachment.name], + [msgURI], + null + ); + // deletion of original message should kick async_driver. + await trackDeletionMessageListener.promise; +}); + +// test that the detachment was successful +add_task(async function testDetach() { + // Check that the file attached to the message now exists in the profile + // directory. + let checkFile = do_get_profile().clone(); + checkFile.append(kAttachFileName); + Assert.ok(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 - detached copy has UID 2. + let msgHdr = IMAPPump.inbox.GetMessageHeader(2); + Assert.ok(msgHdr !== null); + let messageContent = await getContentFromMessage(msgHdr); + Assert.ok(messageContent.includes("AttachmentDetached")); +}); + +// Cleanup +add_task(function endTest() { + teardownIMAPPump(); +}); + +/** + * Get the full message content. + * + * @param aMsgHdr - nsIMsgDBHdr object whose text body will be read. + * @returns {Promise<string>} full message contents. + */ +function getContentFromMessage(aMsgHdr) { + let msgFolder = aMsgHdr.folder; + let msgUri = msgFolder.getUriForMsg(aMsgHdr); + + return new Promise((resolve, reject) => { + let streamListener = { + QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]), + sis: Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ), + content: "", + onDataAvailable(request, inputStream, offset, count) { + this.sis.init(inputStream); + this.content += this.sis.read(count); + }, + onStartRequest(request) {}, + onStopRequest(request, statusCode) { + this.sis.close(); + if (Components.isSuccessCode(statusCode)) { + resolve(this.content); + } else { + reject(new Error(statusCode)); + } + }, + }; + // Pass true for aLocalOnly since message should be in offline store. + MailServices.messageServiceFromURI(msgUri).streamMessage( + msgUri, + streamListener, + null, + null, + false, + "", + true + ); + }); +} diff --git a/comm/mailnews/imap/test/unit/test_imapAuthMethods.js b/comm/mailnews/imap/test/unit/test_imapAuthMethods.js new file mode 100644 index 0000000000..18e5d54396 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_imapAuthMethods.js @@ -0,0 +1,165 @@ +/** + * Login tests for IMAP + * + * Test code <copied from="test_mailboxes.js"> + * and <copied from="test_pop3AuthMethods.js"> + * + * BUGS: + * - cleanup after each test doesn't seem to work correctly. Effects: + * - one more "lsub" per test, e.g. "capability", "auth...", "lsub", "lsub", "lsub", "list" in the 3. test., + * - root folder check succeeds although login failed + * - removeIncomingServer(..., true); (cleanup files) fails. + */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +/* import-globals-from ../../../test/resources/alertTestUtils.js */ +load("../../../resources/alertTestUtils.js"); + +// const kUsername = "fred"; +// const kPassword = "wilma"; + +var thisTest; + +var tests = [ + { + title: "Cleartext password, with server only supporting old-style login", + clientAuthMethod: Ci.nsMsgAuthMethod.passwordCleartext, + serverAuthMethods: [], + expectSuccess: true, + transaction: ["CAPABILITY", "LOGIN", "CAPABILITY", "LIST", "LSUB"], + }, + { + // Just to make sure we clean up properly - in the test and in TB, e.g. don't cache stuff + title: + "Second time Cleartext password, with server only supporting old-style login", + clientAuthMethod: Ci.nsMsgAuthMethod.passwordCleartext, + serverAuthMethods: [], + expectSuccess: true, + transaction: ["CAPABILITY", "LOGIN", "CAPABILITY", "LIST", "LSUB"], + }, + { + title: + "Cleartext password, with server supporting AUTH PLAIN, LOGIN and CRAM", + clientAuthMethod: Ci.nsMsgAuthMethod.passwordCleartext, + serverAuthMethods: ["PLAIN", "LOGIN", "CRAM-MD5"], + expectSuccess: true, + transaction: [ + "CAPABILITY", + "AUTHENTICATE PLAIN", + "CAPABILITY", + "LIST", + "LSUB", + ], + }, + { + title: "Cleartext password, with server supporting only AUTH LOGIN", + clientAuthMethod: Ci.nsMsgAuthMethod.passwordCleartext, + serverAuthMethods: ["LOGIN"], + expectSuccess: true, + transaction: [ + "CAPABILITY", + "AUTHENTICATE LOGIN", + "CAPABILITY", + "LIST", + "LSUB", + ], + }, + { + title: "Encrypted password, with server supporting PLAIN and CRAM", + clientAuthMethod: Ci.nsMsgAuthMethod.passwordEncrypted, + serverAuthMethods: ["PLAIN", "LOGIN", "CRAM-MD5"], + expectSuccess: true, + transaction: [ + "CAPABILITY", + "AUTHENTICATE CRAM-MD5", + "CAPABILITY", + "LIST", + "LSUB", + ], + }, + { + title: + "Encrypted password, with server only supporting AUTH PLAIN and LOGIN (must fail)", + clientAuthMethod: Ci.nsMsgAuthMethod.passwordEncrypted, + serverAuthMethods: ["PLAIN", "LOGIN"], + expectSuccess: false, + transaction: ["CAPABILITY"], + }, +]; + +function nextTest() { + try { + thisTest = tests.shift(); + if (!thisTest) { + endTest(); + return; + } + + dump("NEXT test: " + thisTest.title + "\n"); + + // (re)create fake server + var daemon = new ImapDaemon(); + var server = makeServer(daemon, "", { + kAuthSchemes: thisTest.serverAuthMethods, + }); + server.setDebugLevel(fsDebugAll); + + // If Mailnews ever caches server capabilities, delete and re-create the incomingServer here + var incomingServer = createLocalIMAPServer(server.port); + + let msgServer = incomingServer; + msgServer.QueryInterface(Ci.nsIMsgIncomingServer); + msgServer.authMethod = thisTest.clientAuthMethod; + + // connect + incomingServer.performExpand(null); + server.performTest("LSUB"); + + dump("should " + (thisTest.expectSuccess ? "" : "not ") + "be logged in\n"); + Assert.equal(true, incomingServer instanceof Ci.nsIImapServerSink); + do_check_transaction(server.playTransaction(), thisTest.transaction, false); + + do { + incomingServer.closeCachedConnections(); + } while (incomingServer.serverBusy); + incomingServer.shutdown(); + deleteIMAPServer(incomingServer); + incomingServer = null; + MailServices.accounts.closeCachedConnections(); + MailServices.accounts.shutdownServers(); + MailServices.accounts.unloadAccounts(); + server.stop(); + } catch (e) { + // server.stop(); + // endTest(); + do_throw(e); + } + + nextTest(); +} + +function deleteIMAPServer(incomingServer) { + if (!incomingServer) { + return; + } + MailServices.accounts.removeIncomingServer(incomingServer, true); +} + +function run_test() { + do_test_pending(); + + registerAlertTestUtils(); + + nextTest(); +} + +function endTest() { + var thread = gThreadManager.currentThread; + while (thread.hasPendingEvents()) { + thread.processNextEvent(true); + } + + do_test_finished(); +} diff --git a/comm/mailnews/imap/test/unit/test_imapAutoSync.js b/comm/mailnews/imap/test/unit/test_imapAutoSync.js new file mode 100644 index 0000000000..1f49a408cc --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_imapAutoSync.js @@ -0,0 +1,239 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This tests various features of imap autosync +// N.B. We need to beware of MessageInjection, since it turns off +// imap autosync. + +// Our general approach is to attach an nsIAutoSyncMgrListener to the +// autoSyncManager, and listen for the expected events. We simulate idle +// by directly poking the nsIAutoSyncManager QI'd to nsIObserver with app +// idle events. If we really go idle, duplicate idle events are ignored. + +// We test that checking non-inbox folders for new messages isn't +// interfering with autoSync's detection of new messages. + +// We also test that folders that have messages added to them via move/copy +// get put in the front of the queue. + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { MessageGenerator } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageGenerator.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +var msgFlagOffline = Ci.nsMsgMessageFlags.Offline; +var nsIAutoSyncMgrListener = Ci.nsIAutoSyncMgrListener; + +var gAutoSyncManager = Cc["@mozilla.org/imap/autosyncmgr;1"].getService( + Ci.nsIAutoSyncManager +); +var gTargetFolder; + +add_setup(function () { + setupIMAPPump(); + addMessageToFolder(IMAPPump.inbox); +}); + +add_task(async function test_createTargetFolder() { + gAutoSyncManager.addListener(gAutoSyncListener); + + IMAPPump.incomingServer.rootFolder.createSubfolder("targetFolder", null); + await PromiseTestUtils.promiseFolderAdded("targetFolder"); + gTargetFolder = + IMAPPump.incomingServer.rootFolder.getChildNamed("targetFolder"); + Assert.ok(gTargetFolder instanceof Ci.nsIMsgImapMailFolder); + // set folder to be checked for new messages when inbox is checked. + gTargetFolder.setFlag(Ci.nsMsgFolderFlags.CheckNew); +}); + +add_task(function test_checkForNewMessages() { + addMessageToFolder(gTargetFolder); + // This will update the INBOX and STATUS targetFolder. We only care about + // the latter. + IMAPPump.inbox.getNewMessages(null, null); + IMAPPump.server.performTest("STATUS"); + // Now we'd like to make autosync update folders it knows about, to + // get the initial autosync out of the way. +}); + +add_task(function test_triggerAutoSyncIdle() { + // wait for both folders to get updated. + gAutoSyncListener._waitingForDiscoveryList.push(IMAPPump.inbox); + gAutoSyncListener._waitingForDiscoveryList.push(gTargetFolder); + gAutoSyncListener._waitingForDiscovery = true; + let observer = gAutoSyncManager.QueryInterface(Ci.nsIObserver); + observer.observe(null, "mail-startup-done", ""); + observer.observe(null, "mail:appIdle", "idle"); +}); + +// move the message to a diffent folder +add_task(async function test_moveMessageToTargetFolder() { + let observer = gAutoSyncManager.QueryInterface(Ci.nsIObserver); + observer.observe(null, "mail:appIdle", "back"); + let msgHdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox); + Assert.ok(msgHdr !== null); + + let listener = new PromiseTestUtils.PromiseCopyListener(); + // Now move this message to the target folder. + MailServices.copy.copyMessages( + IMAPPump.inbox, + [msgHdr], + gTargetFolder, + true, + listener, + null, + false + ); + await listener.promise; +}); + +add_task(async function test_waitForTargetUpdate() { + // After the copy, now we expect to get notified of the gTargetFolder + // getting updated, after we simulate going idle. + gAutoSyncListener._waitingForUpdate = true; + gAutoSyncListener._waitingForUpdateList.push(gTargetFolder); + gAutoSyncManager + .QueryInterface(Ci.nsIObserver) + .observe(null, "mail:appIdle", "idle"); + await gAutoSyncListener.promiseOnDownloadCompleted; + await gAutoSyncListener.promiseOnDiscoveryQProcessed; +}); + +// Cleanup +add_task(function endTest() { + let numMsgs = 0; + for (let header of gTargetFolder.messages) { + numMsgs++; + Assert.notEqual(header.flags & Ci.nsMsgMessageFlags.Offline, 0); + } + Assert.equal(2, numMsgs); + Assert.equal(gAutoSyncListener._waitingForUpdateList.length, 0); + Assert.ok(!gAutoSyncListener._waitingForDiscovery); + Assert.ok(!gAutoSyncListener._waitingForUpdate); + teardownIMAPPump(); +}); + +function autoSyncListenerPromise() { + this._inQFolderList = []; + this._runnning = false; + this._lastMessage = {}; + this._waitingForUpdateList = []; + this._waitingForUpdate = false; + this._waitingForDiscoveryList = []; + this._waitingForDiscovery = false; + + this._promiseOnDownloadCompleted = new Promise(resolve => { + this._resolveOnDownloadCompleted = resolve; + }); + this._promiseOnDiscoveryQProcessed = new Promise(resolve => { + this._resolveOnDiscoveryQProcessed = resolve; + }); +} +autoSyncListenerPromise.prototype = { + onStateChanged(running) { + this._runnning = running; + }, + + onFolderAddedIntoQ(queue, folder) { + dump("Folder added into Q " + this.qName(queue) + " " + folder.URI + "\n"); + }, + onFolderRemovedFromQ(queue, folder) { + dump( + "Folder removed from Q " + this.qName(queue) + " " + folder.URI + "\n" + ); + }, + onDownloadStarted(folder, numOfMessages, totalPending) { + dump("Folder download started" + folder.URI + "\n"); + }, + + onDownloadCompleted(folder) { + dump("Folder download completed" + folder.URI + "\n"); + if (folder instanceof Ci.nsIMsgFolder) { + let index = mailTestUtils.non_strict_index_of( + this._waitingForUpdateList, + folder + ); + if (index != -1) { + this._waitingForUpdateList.splice(index, 1); + } + if (this._waitingForUpdate && this._waitingForUpdateList.length == 0) { + dump("Got last folder update looking for.\n"); + this._waitingForUpdate = false; + this._resolveOnDownloadCompleted(); + } + } + }, + + onDownloadError(folder) { + if (folder instanceof Ci.nsIMsgFolder) { + dump("OnDownloadError: " + folder.prettyName + "\n"); + } + }, + + onDiscoveryQProcessed(folder, numOfHdrsProcessed, leftToProcess) { + dump("onDiscoveryQProcessed: " + folder.prettyName + "\n"); + let index = mailTestUtils.non_strict_index_of( + this._waitingForDiscoveryList, + folder + ); + if (index != -1) { + this._waitingForDiscoveryList.splice(index, 1); + } + if ( + this._waitingForDiscovery && + this._waitingForDiscoveryList.length == 0 + ) { + dump("Got last folder discovery looking for\n"); + this._waitingForDiscovery = false; + this._resolveOnDiscoveryQProcessed(); + } + }, + + onAutoSyncInitiated(folder) {}, + qName(queueType) { + if (queueType == Ci.nsIAutoSyncMgrListener.PriorityQueue) { + return "priorityQ"; + } + if (queueType == Ci.nsIAutoSyncMgrListener.UpdateQueue) { + return "updateQ"; + } + if (queueType == Ci.nsIAutoSyncMgrListener.DiscoveryQueue) { + return "discoveryQ"; + } + return ""; + }, + get promiseOnDownloadCompleted() { + return this._promiseOnDownloadCompleted; + }, + get promiseOnDiscoveryQProcessed() { + return this._promiseOnDiscoveryQProcessed; + }, +}; +var gAutoSyncListener = new autoSyncListenerPromise(); + +/* + * helper functions + */ + +// load and update a message in the imap fake server +function addMessageToFolder(folder) { + let messages = []; + let gMessageGenerator = new MessageGenerator(); + messages = messages.concat(gMessageGenerator.makeMessage()); + + let msgURI = Services.io.newURI( + "data:text/plain;base64," + btoa(messages[0].toMessageString()) + ); + let ImapMailbox = IMAPPump.daemon.getMailbox(folder.name); + // We add messages with \Seen flag set so that we won't accidentally + // trigger the code that updates imap folders that have unread messages moved + // into them. + let message = new ImapMessage(msgURI.spec, ImapMailbox.uidnext++, ["\\Seen"]); + ImapMailbox.addMessage(message); +} diff --git a/comm/mailnews/imap/test/unit/test_imapChunks.js b/comm/mailnews/imap/test/unit/test_imapChunks.js new file mode 100644 index 0000000000..4de0d6c2b3 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_imapChunks.js @@ -0,0 +1,114 @@ +/** + * Test bug 92111 - imap download-by-chunks doesn't download complete file if the + * server lies about rfc822.size (known to happen for Exchange and gmail) + */ + +var gIMAPDaemon, gServer, gIMAPIncomingServer, gSavedMsgFile; + +var gIMAPService = Cc[ + "@mozilla.org/messenger/messageservice;1?type=imap" +].getService(Ci.nsIMsgMessageService); + +var gFileName = "bug92111"; +var gMsgFile = do_get_file("../../../data/" + gFileName); + +add_task(async function run_the_test() { + /* + * Set up an IMAP server. The bug is only triggered when nsMsgSaveAsListener + * is used (i.e., for IMAP and NNTP). + */ + gIMAPDaemon = new ImapDaemon(); + gServer = makeServer(gIMAPDaemon, ""); + gIMAPIncomingServer = createLocalIMAPServer(gServer.port); + + // pref tuning: one connection only, turn off notifications + Services.prefs.setBoolPref( + "mail.server.server1.autosync_offline_stores", + false + ); + Services.prefs.setIntPref("mail.server.server1.max_cached_connections", 1); + Services.prefs.setBoolPref("mail.biff.play_sound", false); + Services.prefs.setBoolPref("mail.biff.show_alert", false); + Services.prefs.setBoolPref("mail.biff.show_tray_icon", false); + Services.prefs.setBoolPref("mail.biff.animate_dock_icon", false); + + // Crank down the message chunk size to make test cases easier + Services.prefs.setBoolPref("mail.server.default.fetch_by_chunks", true); + Services.prefs.setIntPref("mail.imap.chunk_size", 1000); + Services.prefs.setIntPref("mail.imap.min_chunk_size_threshold", 1500); + Services.prefs.setIntPref("mail.imap.chunk_add", 0); + + var inbox = gIMAPDaemon.getMailbox("INBOX"); + + /* + * Ok, prelude done. Read the original message from disk + * (through a file URI), and add it to the Inbox. + */ + var msgfileuri = Services.io + .newFileURI(gMsgFile) + .QueryInterface(Ci.nsIFileURL); + + let message = new ImapMessage(msgfileuri.spec, inbox.uidnext++, []); + // report an artificially low size, like gmail and Exchange do + message.setSize(gMsgFile.fileSize - 100); + inbox.addMessage(message); + + // Save the message to a local file. IMapMD corresponds to + // <profile_dir>/mailtest/ImapMail (where fakeserver puts the IMAP mailbox + // files). If we pass the test, we'll remove the file afterwards + // (cf. UrlListener), otherwise it's kept in IMapMD. + gSavedMsgFile = Services.dirsvc.get("IMapMD", Ci.nsIFile); + gSavedMsgFile.append(gFileName + ".eml"); + + do_test_pending(); + do_timeout(10000, function () { + do_throw( + "SaveMessageToDisk did not complete within 10 seconds" + + "(incorrect messageURI?). ABORTING." + ); + }); + + // Enforcing canonicalLineEnding (i.e., CRLF) makes sure that the + // test also runs successfully on platforms not using CRLF by default. + let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener(); + gIMAPService.SaveMessageToDisk( + "imap-message://user@localhost/INBOX#" + (inbox.uidnext - 1), + gSavedMsgFile, + false, + promiseUrlListener, + {}, + true, + null + ); + await promiseUrlListener.promise; + + let msgFileContent = await IOUtils.readUTF8(gMsgFile.path); + let savedMsgFileContent = await IOUtils.readUTF8(gSavedMsgFile.path); + // File contents should not have been modified. + Assert.equal(msgFileContent, savedMsgFileContent); + + // The file doesn't get closed straight away, but does after a little bit. + // So wait, and then remove it. We need to test this to ensure we don't + // indefinitely lock the file. + do_timeout(1000, endTest); +}); + +function endTest() { + gIMAPIncomingServer.closeCachedConnections(); + gServer.stop(); + var thread = gThreadManager.currentThread; + while (thread.hasPendingEvents()) { + thread.processNextEvent(true); + } + + try { + gSavedMsgFile.remove(false); + } catch (ex) { + dump(ex); + do_throw(ex); + } + do_test_finished(); +} + +// XXX IRVING we need a separate check somehow to make sure we store the correct +// content size for chunked messages where the server lied diff --git a/comm/mailnews/imap/test/unit/test_imapClientid.js b/comm/mailnews/imap/test/unit/test_imapClientid.js new file mode 100644 index 0000000000..bceb7c05bf --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_imapClientid.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +var incomingServer, server; + +const kUserName = "user"; +const kValidPassword = "password"; + +var gTests = [ + { + title: "Cleartext password, with server only supporting old-style login", + clientAuthMethod: Ci.nsMsgAuthMethod.passwordCleartext, + serverAuthMethods: [], + expectSuccess: true, + transaction: [ + "capability", + "CLIENTID", + "authenticate PLAIN", + "capability", + "list", + "lsub", + ], + }, +]; + +add_task(async function () { + let daemon = new ImapDaemon(); + server = makeServer(daemon, "", { + // Make username of server match the singons.txt file + // (pw there is intentionally invalid) + kUsername: kUserName, + kPassword: kValidPassword, + }); + server.setDebugLevel(fsDebugAll); + incomingServer = createLocalIMAPServer(server.port); + + // Turn on CLIENTID and populate the clientid with a uuid. + incomingServer.clientidEnabled = true; + incomingServer.clientid = "4d8776ca-0251-11ea-8d71-362b9e155667"; + + // Connect. + incomingServer.performExpand(null); + server.performTest("LSUB"); + + do_check_transaction(server.playTransaction(), gTests[0].transaction, false); + + server.resetTest(); +}); + +registerCleanupFunction(function () { + incomingServer.closeCachedConnections(); + server.stop(); + + var thread = gThreadManager.currentThread; + while (thread.hasPendingEvents()) { + thread.processNextEvent(true); + } +}); diff --git a/comm/mailnews/imap/test/unit/test_imapContentLength.js b/comm/mailnews/imap/test/unit/test_imapContentLength.js new file mode 100644 index 0000000000..acb5001242 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_imapContentLength.js @@ -0,0 +1,98 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Test content length for the IMAP protocol. This focuses on necko URLs + * that are run externally. + */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +var gMsgHdr = null; + +// Take a multipart message as we're testing attachment URLs as well +var gFile = do_get_file("../../../data/multipart-complex2"); + +add_setup(function () { + setupIMAPPump(); + + // Set up nsIMsgFolderListener to get the header when it's received + MailServices.mfn.addListener(msgAddedListener, MailServices.mfn.msgAdded); + + IMAPPump.inbox.clearFlag(Ci.nsMsgFolderFlags.Offline); +}); + +// Adds some messages directly to a mailbox (eg new mail) +add_task(async function addMessageToServer() { + let URI = Services.io.newFileURI(gFile).QueryInterface(Ci.nsIFileURL); + IMAPPump.mailbox.addMessage( + new ImapMessage(URI.spec, IMAPPump.mailbox.uidnext++, []) + ); + + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, listener); + await listener.promise; +}); + +function MsgAddedListener() { + this._promise = new Promise(resolve => { + this._resolve = resolve; + }); +} +MsgAddedListener.prototype = { + msgAdded(aMsgHdr) { + gMsgHdr = aMsgHdr; + this._resolve(); + }, + get promise() { + return this._promise; + }, +}; +var msgAddedListener = new MsgAddedListener(); + +add_task(async function verifyContentLength() { + await msgAddedListener.promise; + let messageUri = IMAPPump.inbox.getUriForMsg(gMsgHdr); + // Convert this to a URI that necko can run + let messageService = MailServices.messageServiceFromURI(messageUri); + let neckoURL = messageService.getUrlForUri(messageUri); + // Don't use the necko URL directly. Instead, get the spec and create a new + // URL using the IO service + let urlToRun = Services.io.newURI(neckoURL.spec); + + // Get a channel from this URI, and check its content length + let channel = Services.io.newChannelFromURI( + urlToRun, + null, + Services.scriptSecurityManager.getSystemPrincipal(), + null, + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER + ); + Assert.equal(channel.contentLength, gFile.fileSize); + + // Now try an attachment. &part=1.2 + let attachmentURL = Services.io.newURI(neckoURL.spec + "&part=1.2"); + let attachmentChannel = Services.io.newChannelFromURI( + attachmentURL, + null, + Services.scriptSecurityManager.getSystemPrincipal(), + null, + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER + ); + // Currently attachments have their content length set to the length of the + // entire message + Assert.equal(attachmentChannel.contentLength, gFile.fileSize); +}); + +add_task(function endTest() { + MailServices.mfn.removeListener(msgAddedListener); + teardownIMAPPump(); +}); diff --git a/comm/mailnews/imap/test/unit/test_imapCopyTimeout.js b/comm/mailnews/imap/test/unit/test_imapCopyTimeout.js new file mode 100644 index 0000000000..75d13159f1 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_imapCopyTimeout.js @@ -0,0 +1,120 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This tests our handling of server timeouts during online move of +// an imap message. The move is done as an offline operation and then +// played back, to copy what the apps do. + +Services.prefs.setIntPref("mailnews.tcptimeout", 2); + +/* import-globals-from ../../../test/resources/alertTestUtils.js */ +load("../../../resources/alertTestUtils.js"); + +var { MessageGenerator } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageGenerator.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +var gTargetFolder; +var alertResolve; +var alertPromise = new Promise(resolve => { + alertResolve = resolve; +}); + +function alertPS(parent, aDialogTitle, aText) { + alertResolve(aText); +} + +add_setup(function () { + registerAlertTestUtils(); + setupIMAPPump(); + Services.prefs.setBoolPref( + "mail.server.default.autosync_offline_stores", + false + ); +}); + +add_task(async function createTargetFolder() { + IMAPPump.daemon.copySleep = 5000; + IMAPPump.incomingServer.rootFolder.createSubfolder("targetFolder", null); + await PromiseTestUtils.promiseFolderAdded("targetFolder"); + gTargetFolder = + IMAPPump.incomingServer.rootFolder.getChildNamed("targetFolder"); + Assert.ok(gTargetFolder instanceof Ci.nsIMsgImapMailFolder); + let listener = new PromiseTestUtils.PromiseUrlListener(); + gTargetFolder.updateFolderWithListener(null, listener); + await listener.promise; +}); + +// load and update a message in the imap fake server +add_task(async function loadImapMessage() { + let messages = []; + let gMessageGenerator = new MessageGenerator(); + messages = messages.concat(gMessageGenerator.makeMessage()); + + let msgURI = Services.io.newURI( + "data:text/plain;base64," + btoa(messages[0].toMessageString()) + ); + let imapInbox = IMAPPump.daemon.getMailbox("INBOX"); + var gMessage = new ImapMessage(msgURI.spec, imapInbox.uidnext++, []); + IMAPPump.mailbox.addMessage(gMessage); + + let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, promiseUrlListener); + await promiseUrlListener.promise; + Assert.equal(1, IMAPPump.inbox.getTotalMessages(false)); + let msgHdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox); + Assert.ok(msgHdr instanceof Ci.nsIMsgDBHdr); +}); + +// move the message to a diffent folder +add_task(async function moveMessageToTargetFolder() { + let msgHdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox); + // This should cause the move to be done as an offline imap operation + // that's played back immediately. + let copyListener = new PromiseTestUtils.PromiseCopyListener(); + MailServices.copy.copyMessages( + IMAPPump.inbox, + [msgHdr], + gTargetFolder, + true, + copyListener, + gDummyMsgWindow, + true + ); + await copyListener.promise; +}); + +add_task(async function waitForOfflinePlayback() { + // Just wait for the alert about timed out connection. + let alertText = await alertPromise; + Assert.ok(alertText.startsWith("Connection to server localhost timed out.")); +}); + +add_task(async function updateTargetFolderAndInbox() { + let urlListenerTargetFolder = new PromiseTestUtils.PromiseUrlListener(); + gTargetFolder.updateFolderWithListener(null, urlListenerTargetFolder); + await urlListenerTargetFolder.promise; + let urlListenerInbox = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, urlListenerInbox); + await urlListenerInbox.promise; +}); + +// Cleanup +add_task(async function endTest() { + // Make sure neither source nor target folder have offline events. + Assert.ok(!IMAPPump.inbox.getFlag(Ci.nsMsgFolderFlags.OfflineEvents)); + Assert.ok(!gTargetFolder.getFlag(Ci.nsMsgFolderFlags.OfflineEvents)); + + // fake server does the copy, but then times out, so make sure the target + // folder has only 1 message, not the multiple ones it would have if we + // retried. + Assert.equal(gTargetFolder.getTotalMessages(false), 1); + teardownIMAPPump(); +}); diff --git a/comm/mailnews/imap/test/unit/test_imapFilterActions.js b/comm/mailnews/imap/test/unit/test_imapFilterActions.js new file mode 100644 index 0000000000..21cd2d01aa --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_imapFilterActions.js @@ -0,0 +1,597 @@ +/* + * This file tests imap filter actions, particularly as affected by the + * addition of body searches in bug 127250. Actions that involves sending + * mail are not tested. The tests check various counts, and the effects + * on the message database of the filters. Effects on IMAP server + * flags, if any, are not tested. + * + * Original author: Kent James <kent@caspia.com> + * adapted from test_localToImapFilter.js + */ + +var Is = Ci.nsMsgSearchOp.Is; +var Contains = Ci.nsMsgSearchOp.Contains; +var Subject = Ci.nsMsgSearchAttrib.Subject; +var Body = Ci.nsMsgSearchAttrib.Body; + +// Globals +var gSubfolder; // a local message folder used as a target for moves and copies +var gFilter; // a message filter with a subject search +var gAction; // current message action (reused) +var gBodyFilter; // a message filter with a body search +var gInboxListener; // database listener object +var gHeader; // the current message db header +var gInboxCount; // the previous number of messages in the Inbox +var gSubfolderCount; // the previous number of messages in the subfolder +var gMessage = "image-attach-test"; // message file used as the test message + +// subject of the test message +var gMessageSubject = "image attach test"; + +// a string in the body of the test message +var gMessageInBody = "01234567890test"; + +// various object references +var gDbService = Cc["@mozilla.org/msgDatabase/msgDBService;1"].getService( + Ci.nsIMsgDBService +); + +var gSmtpServerD = setupSmtpServerDaemon(); + +function setupSmtpServer() { + gSmtpServerD.start(); + var gSmtpServer = localAccountUtils.create_outgoing_server( + gSmtpServerD.port, + "user", + "password", + "localhost" + ); + MailServices.accounts.defaultAccount.defaultIdentity.email = + "from@tinderbox.invalid"; + MailServices.accounts.defaultAccount.defaultIdentity.smtpServerKey = + gSmtpServer.key; + + registerCleanupFunction(() => { + gSmtpServerD.stop(); + Services.prefs.clearUserPref("mail.forward_message_mode"); + }); +} + +// Definition of tests. The test function name is the filter action +// being tested, with "Body" appended to tests that use delayed +// application of filters due to a body search +var gTestArray = [ + setupIMAPPump, + // optionally set server parameters, here enabling debug messages + // function serverParms() { + // IMAPPump.server.setDebugLevel(fsDebugAll); + // }, + setupSmtpServer, + setupFilters, + // The initial tests do not result in new messages added. + async function MoveToFolder() { + gAction.type = Ci.nsMsgFilterAction.MoveToFolder; + gAction.targetFolderUri = gSubfolder.URI; + gInboxCount = folderCount(IMAPPump.inbox); + gSubfolderCount = folderCount(gSubfolder); + await setupTest(gFilter, gAction); + + testCounts(false, 0, 0, 0); + Assert.equal(gInboxCount, folderCount(IMAPPump.inbox)); + Assert.equal(gSubfolderCount + 1, folderCount(gSubfolder)); + }, + // do it again, sometimes that causes multiple downloads + async function MoveToFolder2() { + gAction.type = Ci.nsMsgFilterAction.MoveToFolder; + gAction.targetFolderUri = gSubfolder.URI; + gInboxCount = folderCount(IMAPPump.inbox); + gSubfolderCount = folderCount(gSubfolder); + await setupTest(gFilter, gAction); + + testCounts(false, 0, 0, 0); + Assert.equal(gInboxCount, folderCount(IMAPPump.inbox)); + Assert.equal(gSubfolderCount + 1, folderCount(gSubfolder)); + }, + /**/ + async function MoveToFolderBody() { + gAction.type = Ci.nsMsgFilterAction.MoveToFolder; + gAction.targetFolderUri = gSubfolder.URI; + gInboxCount = folderCount(IMAPPump.inbox); + gSubfolderCount = folderCount(gSubfolder); + await setupTest(gBodyFilter, gAction); + + testCounts(false, 0, 0, 0); + Assert.equal(gSubfolderCount + 1, folderCount(gSubfolder)); + // no net messages were added to the inbox + Assert.equal(gInboxCount, folderCount(IMAPPump.inbox)); + }, + async function MoveToFolderBody2() { + gAction.type = Ci.nsMsgFilterAction.MoveToFolder; + gAction.targetFolderUri = gSubfolder.URI; + gInboxCount = folderCount(IMAPPump.inbox); + gSubfolderCount = folderCount(gSubfolder); + await setupTest(gBodyFilter, gAction); + + testCounts(false, 0, 0, 0); + Assert.equal(gSubfolderCount + 1, folderCount(gSubfolder)); + // no net messages were added to the inbox + Assert.equal(gInboxCount, folderCount(IMAPPump.inbox)); + }, + async function MarkRead() { + gAction.type = Ci.nsMsgFilterAction.MarkRead; + gInboxCount = folderCount(IMAPPump.inbox); + await setupTest(gFilter, gAction); + testCounts(false, 0, 0, 0); + Assert.ok(gHeader.isRead); + Assert.equal(gInboxCount + 1, folderCount(IMAPPump.inbox)); + }, + async function MarkReadBody() { + gAction.type = Ci.nsMsgFilterAction.MarkRead; + gInboxCount = folderCount(IMAPPump.inbox); + await setupTest(gBodyFilter, gAction); + + testCounts(false, 0, 0, 0); + Assert.ok(gHeader.isRead); + Assert.equal(gInboxCount + 1, folderCount(IMAPPump.inbox)); + }, + async function KillThread() { + gAction.type = Ci.nsMsgFilterAction.KillThread; + await setupTest(gFilter, gAction); + + testCounts(false, 0, 0, 0); + let thread = db().getThreadContainingMsgHdr(gHeader); + Assert.notEqual(0, thread.flags & Ci.nsMsgMessageFlags.Ignored); + }, + async function KillThreadBody() { + gAction.type = Ci.nsMsgFilterAction.KillThread; + await setupTest(gBodyFilter, gAction); + + testCounts(false, 0, 0, 0); + let thread = db().getThreadContainingMsgHdr(gHeader); + Assert.notEqual(0, thread.flags & Ci.nsMsgMessageFlags.Ignored); + }, + async function KillSubthread() { + gAction.type = Ci.nsMsgFilterAction.KillSubthread; + await setupTest(gFilter, gAction); + + testCounts(false, 0, 0, 0); + Assert.notEqual(0, gHeader.flags & Ci.nsMsgMessageFlags.Ignored); + }, + async function KillSubthreadBody() { + gAction.type = Ci.nsMsgFilterAction.KillSubthread; + await setupTest(gBodyFilter, gAction); + + testCounts(false, 0, 0, 0); + Assert.notEqual(0, gHeader.flags & Ci.nsMsgMessageFlags.Ignored); + }, + async function DoNothing() { + gAction.type = Ci.nsMsgFilterAction.StopExecution; + await setupTest(gFilter, gAction); + + testCounts(true, 1, 1, 1); + }, + async function DoNothingBody() { + gAction.type = Ci.nsMsgFilterAction.StopExecution; + await setupTest(gFilter, gAction); + + testCounts(true, 1, 1, 1); + }, + // this tests for marking message as junk + async function JunkScore() { + gAction.type = Ci.nsMsgFilterAction.JunkScore; + gAction.junkScore = 100; + await setupTest(gFilter, gAction); + + // marking as junk resets new but not unread + testCounts(false, 1, 0, 0); + Assert.equal(gHeader.getStringProperty("junkscore"), "100"); + Assert.equal(gHeader.getStringProperty("junkscoreorigin"), "filter"); + }, + // this tests for marking message as junk + async function JunkScoreBody() { + gAction.type = Ci.nsMsgFilterAction.JunkScore; + gAction.junkScore = 100; + await setupTest(gBodyFilter, gAction); + + // marking as junk resets new but not unread + testCounts(false, 1, 0, 0); + Assert.equal(gHeader.getStringProperty("junkscore"), "100"); + Assert.equal(gHeader.getStringProperty("junkscoreorigin"), "filter"); + }, + // The remaining tests add new messages + async function MarkUnread() { + gAction.type = Ci.nsMsgFilterAction.MarkUnread; + await setupTest(gFilter, gAction); + + testCounts(true, 1, 1, 1); + Assert.ok(!gHeader.isRead); + }, + async function MarkUnreadBody() { + gAction.type = Ci.nsMsgFilterAction.MarkUnread; + await setupTest(gBodyFilter, gAction); + + testCounts(true, 1, 1, 1); + Assert.ok(!gHeader.isRead); + }, + async function WatchThread() { + gAction.type = Ci.nsMsgFilterAction.WatchThread; + await setupTest(gFilter, gAction); + + testCounts(true, 1, 1, 1); + let thread = db().getThreadContainingMsgHdr(gHeader); + Assert.notEqual(0, thread.flags & Ci.nsMsgMessageFlags.Watched); + }, + async function WatchThreadBody() { + gAction.type = Ci.nsMsgFilterAction.WatchThread; + await setupTest(gBodyFilter, gAction); + + testCounts(true, 1, 1, 1); + let thread = db().getThreadContainingMsgHdr(gHeader); + Assert.notEqual(0, thread.flags & Ci.nsMsgMessageFlags.Watched); + }, + async function MarkFlagged() { + gAction.type = Ci.nsMsgFilterAction.MarkFlagged; + await setupTest(gFilter, gAction); + + testCounts(true, 1, 1, 1); + Assert.ok(gHeader.isFlagged); + }, + async function MarkFlaggedBody() { + gAction.type = Ci.nsMsgFilterAction.MarkFlagged; + await setupTest(gBodyFilter, gAction); + + testCounts(true, 1, 1, 1); + Assert.ok(gHeader.isFlagged); + }, + async function ChangePriority() { + gAction.type = Ci.nsMsgFilterAction.ChangePriority; + gAction.priority = Ci.nsMsgPriority.highest; + await setupTest(gFilter, gAction); + + testCounts(true, 1, 1, 1); + Assert.equal(Ci.nsMsgPriority.highest, gHeader.priority); + }, + async function ChangePriorityBody() { + gAction.type = Ci.nsMsgFilterAction.ChangePriority; + gAction.priority = Ci.nsMsgPriority.highest; + await setupTest(gBodyFilter, gAction); + + testCounts(true, 1, 1, 1); + Assert.equal(Ci.nsMsgPriority.highest, gHeader.priority); + }, + async function AddTag() { + gAction.type = Ci.nsMsgFilterAction.AddTag; + gAction.strValue = "TheTag"; + await setupTest(gFilter, gAction); + + testCounts(true, 1, 1, 1); + Assert.equal(gHeader.getStringProperty("keywords"), "thetag"); + }, + async function AddTagBody() { + gAction.type = Ci.nsMsgFilterAction.AddTag; + gAction.strValue = "TheTag2"; + await setupTest(gBodyFilter, gAction); + + testCounts(true, 1, 1, 1); + Assert.equal(gHeader.getStringProperty("keywords"), "thetag2"); + }, + // this tests for marking message as good + async function JunkScoreAsGood() { + gAction.type = Ci.nsMsgFilterAction.JunkScore; + gAction.junkScore = 0; + await setupTest(gFilter, gAction); + + testCounts(true, 1, 1, 1); + Assert.equal(gHeader.getStringProperty("junkscore"), "0"); + Assert.equal(gHeader.getStringProperty("junkscoreorigin"), "filter"); + }, + // this tests for marking message as good + async function JunkScoreAsGoodBody() { + gAction.type = Ci.nsMsgFilterAction.JunkScore; + gAction.junkScore = 0; + await setupTest(gBodyFilter, gAction); + + testCounts(true, 1, 1, 1); + Assert.equal(gHeader.getStringProperty("junkscore"), "0"); + Assert.equal(gHeader.getStringProperty("junkscoreorigin"), "filter"); + }, + async function CopyToFolder() { + gAction.type = Ci.nsMsgFilterAction.CopyToFolder; + gAction.targetFolderUri = gSubfolder.URI; + gInboxCount = folderCount(IMAPPump.inbox); + gSubfolderCount = folderCount(gSubfolder); + await setupTest(gFilter, gAction); + + testCounts(true, 1, 1, 1); + Assert.equal(gInboxCount + 1, folderCount(IMAPPump.inbox)); + Assert.equal(gSubfolderCount + 1, folderCount(gSubfolder)); + }, + async function CopyToFolderBody() { + gAction.type = Ci.nsMsgFilterAction.CopyToFolder; + gAction.targetFolderUri = gSubfolder.URI; + gInboxCount = folderCount(IMAPPump.inbox); + gSubfolderCount = folderCount(gSubfolder); + await setupTest(gBodyFilter, gAction); + + testCounts(true, 1, 1, 1); + Assert.equal(gInboxCount + 1, folderCount(IMAPPump.inbox)); + Assert.equal(gSubfolderCount + 1, folderCount(gSubfolder)); + }, + async function ForwardInline() { + return testForward(2); + }, + async function ForwardAsAttachment() { + return testForward(0); + }, + /**/ + endTest, +]; + +function run_test() { + // Services.prefs.setBoolPref("mail.server.default.autosync_offline_stores", false); + gTestArray.forEach(x => add_task(x)); + run_next_test(); +} + +function setupFilters() { + // Create a non-body filter. + let filterList = IMAPPump.incomingServer.getFilterList(null); + gFilter = filterList.createFilter("subject"); + let searchTerm = gFilter.createTerm(); + searchTerm.attrib = Subject; + searchTerm.op = Is; + var value = searchTerm.value; + value.attrib = Subject; + value.str = gMessageSubject; + searchTerm.value = value; + searchTerm.booleanAnd = false; + gFilter.appendTerm(searchTerm); + gFilter.enabled = true; + + // Create a filter with a body term that that forces delayed application of + // filters until after body download. + gBodyFilter = filterList.createFilter("body"); + searchTerm = gBodyFilter.createTerm(); + searchTerm.attrib = Body; + searchTerm.op = Contains; + value = searchTerm.value; + value.attrib = Body; + value.str = gMessageInBody; + searchTerm.value = value; + searchTerm.booleanAnd = false; + gBodyFilter.appendTerm(searchTerm); + gBodyFilter.enabled = true; + + // an action that can be modified by tests + gAction = gFilter.createAction(); + + gSubfolder = localAccountUtils.rootFolder.createLocalSubfolder("Subfolder"); + + MailServices.mailSession.AddFolderListener( + FolderListener, + Ci.nsIFolderListener.event + ); + gPreviousUnread = 0; + + // When a message body is not downloaded, and then later a filter is + // applied that requires a download of message bodies, then the previous + // bodies are downloaded - and the message filters are applied twice! + // See bug 1116228, but for now workaround by always downloading bodies. + IMAPPump.incomingServer.downloadBodiesOnGetNewMail = true; +} + +/* + * functions used to support test setup and execution + */ + +// basic preparation done for each test +async function setupTest(aFilter, aAction) { + let filterList = IMAPPump.incomingServer.getFilterList(null); + while (filterList.filterCount) { + filterList.removeFilterAt(0); + } + if (aFilter) { + aFilter.clearActionList(); + if (aAction) { + aFilter.appendAction(aAction); + filterList.insertFilterAt(0, aFilter); + } + } + if (gInboxListener) { + gDbService.unregisterPendingListener(gInboxListener); + } + + IMAPPump.inbox.clearNewMessages(); + + gInboxListener = new DBListener(); + gDbService.registerPendingListener(IMAPPump.inbox, gInboxListener); + IMAPPump.mailbox.addMessage( + new ImapMessage(specForFileName(gMessage), IMAPPump.mailbox.uidnext++, []) + ); + let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, promiseUrlListener); + await promiseUrlListener.promise; + await PromiseTestUtils.promiseDelay(200); +} + +// Cleanup, null out everything, close all cached connections and stop the +// server +function endTest() { + if (gInboxListener) { + gDbService.unregisterPendingListener(gInboxListener); + } + gInboxListener = null; + MailServices.mailSession.RemoveFolderListener(FolderListener); + teardownIMAPPump(); +} + +/* + * listener objects + */ + +// nsIFolderListener implementation +var FolderListener = { + onFolderEvent(aEventFolder, aEvent) { + dump( + "received folder event " + aEvent + " folder " + aEventFolder.name + "\n" + ); + }, +}; + +// nsIDBChangeListener implementation. Counts of calls are kept, but not +// currently used in the tests. Current role is to provide a reference +// to the new message header (plus give some examples of using db listeners +// in javascript). +function DBListener() { + this.counts = {}; + let counts = this.counts; + counts.onHdrFlagsChanged = 0; + counts.onHdrDeleted = 0; + counts.onHdrAdded = 0; + counts.onParentChanged = 0; + counts.onAnnouncerGoingAway = 0; + counts.onReadChanged = 0; + counts.onJunkScoreChanged = 0; + counts.onHdrPropertyChanged = 0; + counts.onEvent = 0; +} + +DBListener.prototype = { + onHdrFlagsChanged(aHdrChanged, aOldFlags, aNewFlags, aInstigator) { + this.counts.onHdrFlagsChanged++; + }, + + onHdrDeleted(aHdrChanged, aParentKey, Flags, aInstigator) { + this.counts.onHdrDeleted++; + }, + + onHdrAdded(aHdrChanged, aParentKey, aFlags, aInstigator) { + this.counts.onHdrAdded++; + gHeader = aHdrChanged; + }, + + onParentChanged(aKeyChanged, oldParent, newParent, aInstigator) { + this.counts.onParentChanged++; + }, + + onAnnouncerGoingAway(instigator) { + if (gInboxListener) { + try { + IMAPPump.inbox.msgDatabase.removeListener(gInboxListener); + } catch (e) { + dump(" listener not found\n"); + } + } + this.counts.onAnnouncerGoingAway++; + }, + + onReadChanged(aInstigator) { + this.counts.onReadChanged++; + }, + + onJunkScoreChanged(aInstigator) { + this.counts.onJunkScoreChanged++; + }, + + onHdrPropertyChanged(aHdrToChange, aPreChange, aStatus, aInstigator) { + this.counts.onHdrPropertyChanged++; + }, + + onEvent(aDB, aEvent) { + this.counts.onEvent++; + }, +}; + +/* + * helper functions + */ + +// return the number of messages in a folder (and check that the +// folder counts match the database counts) +function folderCount(folder) { + // count using the database + let dbCount = [...folder.msgDatabase.enumerateMessages()].length; + + // count using the folder + let count = folder.getTotalMessages(false); + + // compare the two + Assert.equal(dbCount, count); + return dbCount; +} + +// given a test file, return the file uri spec +function specForFileName(aFileName) { + let file = do_get_file("../../../data/" + aFileName); + let msgfileuri = Services.io.newFileURI(file).QueryInterface(Ci.nsIFileURL); + return msgfileuri.spec; +} + +// shorthand for the inbox message summary database +function db() { + return IMAPPump.inbox.msgDatabase; +} + +// static variables used in testCounts +var gPreviousUnread = 0; + +// Test various counts. +// +// aHasNew: folder hasNew flag +// aUnreadDelta: change in unread count for the folder +// aFolderNewDelta: change in new count for the folder +// aDbNewDelta: change in new count for the database +// +function testCounts(aHasNew, aUnreadDelta, aFolderNewDelta, aDbNewDelta) { + try { + let folderNew = IMAPPump.inbox.getNumNewMessages(false); + let hasNew = IMAPPump.inbox.hasNewMessages; + let unread = IMAPPump.inbox.getNumUnread(false); + let arrayOut = db().getNewList(); + let dbNew = arrayOut.length; + dump( + " hasNew: " + + hasNew + + " unread: " + + unread + + " folderNew: " + + folderNew + + " dbNew: " + + dbNew + + " prevUnread " + + gPreviousUnread + + "\n" + ); + Assert.equal(aHasNew, hasNew); + Assert.equal(aUnreadDelta, unread - gPreviousUnread); + gPreviousUnread = unread; + // This seems to be reset for each folder update. + // + // This check seems to be failing in SeaMonkey builds, yet I can see no ill + // effects of this in the actual program. Fixing this is complex because of + // the messiness of new count management (see bug 507638 for a + // refactoring proposal, and attachment 398899 on bug 514801 for one possible + // fix to this particular test). So I am disabling this. + // Assert.equal(aFolderNewDelta, folderNew); + Assert.equal(aDbNewDelta, dbNew); + } catch (e) { + dump(e); + } +} + +/** + * Test that Ci.nsMsgFilterAction.Forward works. + * + * @param {number} mode - 0 means forward as attachment, 2 means forward inline. + */ +async function testForward(mode) { + Services.prefs.setIntPref("mail.forward_message_mode", mode); + + gSmtpServerD.resetTest(); + gAction.type = Ci.nsMsgFilterAction.Forward; + gAction.strValue = "to@local"; + await setupTest(gFilter, gAction); + let msgData = gSmtpServerD._daemon.post; + Assert.ok(msgData.includes(`Subject: Fwd: ${gMessageSubject}`)); + Assert.ok(msgData.includes(`${gMessageInBody}`)); +} diff --git a/comm/mailnews/imap/test/unit/test_imapFilterActionsPostplugin.js b/comm/mailnews/imap/test/unit/test_imapFilterActionsPostplugin.js new file mode 100644 index 0000000000..2d7e00efcc --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_imapFilterActionsPostplugin.js @@ -0,0 +1,428 @@ +/* + * This file tests imap filter actions post-plugin, which uses nsMsgFilterAfterTheFact + * + * Original author: Kent James <kent@caspia.com> + * adapted from test_imapFilterActions.js + */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +var Is = Ci.nsMsgSearchOp.Is; +var Subject = Ci.nsMsgSearchAttrib.Subject; + +// Globals +var gSubfolder; // a local message folder used as a target for moves and copies +var gFilter; // a message filter with a subject search +var gAction; // current message action (reused) +var gInboxListener; // database listener object +var gHeader; // the current message db header +var gInboxCount; // the previous number of messages in the Inbox +var gSubfolderCount; // the previous number of messages in the subfolder +var gMessage = "draft1"; // message file used as the test message + +// subject of the test message +var gMessageSubject = "Hello, did you receive my bugmail?"; + +// various object references +var gDbService = Cc["@mozilla.org/msgDatabase/msgDBService;1"].getService( + Ci.nsIMsgDBService +); + +// Definition of tests. The test function name is the filter action +// being tested, with "Body" appended to tests that use delayed +// application of filters due to a body search +var gTestArray = [ + setupIMAPPump, + setupFilters, + async function DoNothing() { + gAction.type = Ci.nsMsgFilterAction.StopExecution; + gInboxCount = folderCount(IMAPPump.inbox); + await setupTest(gFilter, gAction); + testCounts(false, 1, 0, 0); + Assert.equal(gInboxCount + 1, folderCount(IMAPPump.inbox)); + }, + async function Delete() { + gAction.type = Ci.nsMsgFilterAction.Delete; + gInboxCount = folderCount(IMAPPump.inbox); + await setupTest(gFilter, gAction); + testCounts(false, 0, 0, 0); + Assert.equal(gInboxCount, folderCount(IMAPPump.inbox)); + }, + async function MoveToFolder() { + gAction.type = Ci.nsMsgFilterAction.MoveToFolder; + gAction.targetFolderUri = gSubfolder.URI; + gInboxCount = folderCount(IMAPPump.inbox); + gSubfolderCount = folderCount(gSubfolder); + await setupTest(gFilter, gAction); + + testCounts(false, 0, 0, 0); + Assert.equal(gSubfolderCount + 1, folderCount(gSubfolder)); + // no net messages were added to the inbox + Assert.equal(gInboxCount, folderCount(IMAPPump.inbox)); + }, + async function MarkRead() { + gAction.type = Ci.nsMsgFilterAction.MarkRead; + await setupTest(gFilter, gAction); + testCounts(false, 0, 0, 0); + Assert.ok(gHeader.isRead); + }, + async function KillThread() { + gAction.type = Ci.nsMsgFilterAction.KillThread; + await setupTest(gFilter, gAction); + // In non-postplugin, count here is 0 and not 1. Need to investigate. + testCounts(false, 1, 0, 0); + let thread = db().getThreadContainingMsgHdr(gHeader); + Assert.notEqual(0, thread.flags & Ci.nsMsgMessageFlags.Ignored); + }, + async function WatchThread() { + gAction.type = Ci.nsMsgFilterAction.WatchThread; + await setupTest(gFilter, gAction); + // In non-postplugin, count here is 0 and not 1. Need to investigate. + testCounts(false, 1, 0, 0); + let thread = db().getThreadContainingMsgHdr(gHeader); + Assert.notEqual(0, thread.flags & Ci.nsMsgMessageFlags.Watched); + }, + async function KillSubthread() { + gAction.type = Ci.nsMsgFilterAction.KillSubthread; + await setupTest(gFilter, gAction); + // In non-postplugin, count here is 0 and not 1. Need to investigate. + testCounts(false, 1, 0, 0); + Assert.notEqual(0, gHeader.flags & Ci.nsMsgMessageFlags.Ignored); + }, + // this tests for marking message as junk + async function JunkScore() { + gAction.type = Ci.nsMsgFilterAction.JunkScore; + gAction.junkScore = 100; + await setupTest(gFilter, gAction); + // marking as junk resets new but not unread + testCounts(false, 1, 0, 0); + Assert.equal(gHeader.getStringProperty("junkscore"), "100"); + Assert.equal(gHeader.getStringProperty("junkscoreorigin"), "filter"); + }, + async function MarkUnread() { + gAction.type = Ci.nsMsgFilterAction.MarkUnread; + await setupTest(gFilter, gAction); + testCounts(true, 1, 1, 1); + Assert.ok(!gHeader.isRead); + }, + async function MarkFlagged() { + gAction.type = Ci.nsMsgFilterAction.MarkFlagged; + await setupTest(gFilter, gAction); + testCounts(true, 1, 1, 1); + Assert.ok(gHeader.isFlagged); + }, + async function ChangePriority() { + gAction.type = Ci.nsMsgFilterAction.ChangePriority; + gAction.priority = Ci.nsMsgPriority.highest; + await setupTest(gFilter, gAction); + testCounts(true, 1, 1, 1); + Assert.equal(Ci.nsMsgPriority.highest, gHeader.priority); + }, + async function AddTag() { + gAction.type = Ci.nsMsgFilterAction.AddTag; + gAction.strValue = "TheTag"; + await setupTest(gFilter, gAction); + testCounts(true, 1, 1, 1); + Assert.equal(gHeader.getStringProperty("keywords"), "thetag"); + }, + // this tests for marking message as good + async function JunkScoreAsGood() { + gAction.type = Ci.nsMsgFilterAction.JunkScore; + gAction.junkScore = 0; + await setupTest(gFilter, gAction); + testCounts(true, 1, 1, 1); + Assert.equal(gHeader.getStringProperty("junkscore"), "0"); + Assert.equal(gHeader.getStringProperty("junkscoreorigin"), "filter"); + }, + async function CopyToFolder() { + gAction.type = Ci.nsMsgFilterAction.CopyToFolder; + gAction.targetFolderUri = gSubfolder.URI; + gInboxCount = folderCount(IMAPPump.inbox); + gSubfolderCount = folderCount(gSubfolder); + await setupTest(gFilter, gAction); + testCounts(true, 1, 1, 1); + Assert.equal(gInboxCount + 1, folderCount(IMAPPump.inbox)); + Assert.equal(gSubfolderCount + 1, folderCount(gSubfolder)); + }, + async function Custom() { + gAction.type = Ci.nsMsgFilterAction.Custom; + gAction.customId = "mailnews@mozilla.org#testOffline"; + gAction.strValue = "true"; + actionTestOffline.needsBody = true; + await setupTest(gFilter, gAction); + testCounts(true, 1, 1, 1); + }, + /**/ + endTest, +]; + +function run_test() { + Services.prefs.setBoolPref( + "mail.server.default.autosync_offline_stores", + false + ); + gTestArray.forEach(x => add_task(x)); + run_next_test(); +} + +function setupFilters() { + // Create a non-body filter. + let filterList = IMAPPump.incomingServer.getFilterList(null); + gFilter = filterList.createFilter("subject"); + let searchTerm = gFilter.createTerm(); + searchTerm.attrib = Subject; + searchTerm.op = Is; + var value = searchTerm.value; + value.attrib = Subject; + value.str = gMessageSubject; + searchTerm.value = value; + searchTerm.booleanAnd = false; + gFilter.appendTerm(searchTerm); + gFilter.filterType = Ci.nsMsgFilterType.PostPlugin; + gFilter.enabled = true; + + // an action that can be modified by tests + gAction = gFilter.createAction(); + + MailServices.filters.addCustomAction(actionTestOffline); + MailServices.mailSession.AddFolderListener( + FolderListener, + Ci.nsIFolderListener.event + ); + gSubfolder = localAccountUtils.rootFolder.createLocalSubfolder("Subfolder"); + gPreviousUnread = 0; +} + +/* + * functions used to support test setup and execution + */ + +// basic preparation done for each test +async function setupTest(aFilter, aAction) { + let filterList = IMAPPump.incomingServer.getFilterList(null); + while (filterList.filterCount) { + filterList.removeFilterAt(0); + } + if (aFilter) { + aFilter.clearActionList(); + if (aAction) { + aFilter.appendAction(aAction); + filterList.insertFilterAt(0, aFilter); + } + } + if (gInboxListener) { + gDbService.unregisterPendingListener(gInboxListener); + } + + gInboxListener = new DBListener(); + gDbService.registerPendingListener(IMAPPump.inbox, gInboxListener); + IMAPPump.mailbox.addMessage( + new ImapMessage(specForFileName(gMessage), IMAPPump.mailbox.uidnext++, []) + ); + let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, promiseUrlListener); + await promiseUrlListener.promise; + await PromiseTestUtils.promiseDelay(200); +} + +// Cleanup, null out everything, close all cached connections and stop the +// server +function endTest() { + if (gInboxListener) { + gDbService.unregisterPendingListener(gInboxListener); + } + gInboxListener = null; + MailServices.mailSession.RemoveFolderListener(FolderListener); + teardownIMAPPump(); +} + +/* + * listener objects + */ + +// nsIFolderListener implementation +var FolderListener = { + onFolderEvent(aEventFolder, aEvent) { + dump( + "received folder event " + aEvent + " folder " + aEventFolder.name + "\n" + ); + }, +}; + +// nsIDBChangeListener implementation. Counts of calls are kept, but not +// currently used in the tests. Current role is to provide a reference +// to the new message header (plus give some examples of using db listeners +// in javascript). +function DBListener() { + this.counts = {}; + let counts = this.counts; + counts.onHdrFlagsChanged = 0; + counts.onHdrDeleted = 0; + counts.onHdrAdded = 0; + counts.onParentChanged = 0; + counts.onAnnouncerGoingAway = 0; + counts.onReadChanged = 0; + counts.onJunkScoreChanged = 0; + counts.onHdrPropertyChanged = 0; + counts.onEvent = 0; +} + +DBListener.prototype = { + onHdrFlagsChanged(aHdrChanged, aOldFlags, aNewFlags, aInstigator) { + this.counts.onHdrFlagsChanged++; + }, + + onHdrDeleted(aHdrChanged, aParentKey, Flags, aInstigator) { + this.counts.onHdrDeleted++; + }, + + onHdrAdded(aHdrChanged, aParentKey, aFlags, aInstigator) { + this.counts.onHdrAdded++; + gHeader = aHdrChanged; + }, + + onParentChanged(aKeyChanged, oldParent, newParent, aInstigator) { + this.counts.onParentChanged++; + }, + + onAnnouncerGoingAway(instigator) { + if (gInboxListener) { + try { + IMAPPump.inbox.msgDatabase.removeListener(gInboxListener); + } catch (e) { + dump(" listener not found\n"); + } + } + this.counts.onAnnouncerGoingAway++; + }, + + onReadChanged(aInstigator) { + this.counts.onReadChanged++; + }, + + onJunkScoreChanged(aInstigator) { + this.counts.onJunkScoreChanged++; + }, + + onHdrPropertyChanged(aHdrToChange, aPreChange, aStatus, aInstigator) { + this.counts.onHdrPropertyChanged++; + }, + + onEvent(aDB, aEvent) { + this.counts.onEvent++; + }, +}; + +/* + * helper functions + */ + +// return the number of messages in a folder (and check that the +// folder counts match the database counts) +function folderCount(folder) { + // count using the database + let dbCount = [...folder.msgDatabase.enumerateMessages()].length; + + // count using the folder + let count = folder.getTotalMessages(false); + + // compare the two + Assert.equal(dbCount, count); + return dbCount; +} + +// given a test file, return the file uri spec +function specForFileName(aFileName) { + let file = do_get_file("../../../data/" + aFileName); + let msgfileuri = Services.io.newFileURI(file).QueryInterface(Ci.nsIFileURL); + return msgfileuri.spec; +} + +// shorthand for the inbox message summary database +function db() { + return IMAPPump.inbox.msgDatabase; +} + +// static variables used in testCounts +var gPreviousUnread; + +// Test various counts. +// +// aHasNew: folder hasNew flag +// aUnreadDelta: change in unread count for the folder +// aFolderNewDelta: change in new count for the folder +// aDbNewDelta: change in new count for the database +// +function testCounts(aHasNew, aUnreadDelta, aFolderNewDelta, aDbNewDelta) { + try { + let folderNew = IMAPPump.inbox.getNumNewMessages(false); + let hasNew = IMAPPump.inbox.hasNewMessages; + let unread = IMAPPump.inbox.getNumUnread(false); + let arrayOut = db().getNewList(); + let dbNew = arrayOut.length; + dump( + " hasNew: " + + hasNew + + " unread: " + + unread + + " folderNew: " + + folderNew + + " dbNew: " + + dbNew + + " prevUnread " + + gPreviousUnread + + "\n" + ); + // Assert.equal(aHasNew, hasNew); + Assert.equal(aUnreadDelta, unread - gPreviousUnread); + gPreviousUnread = unread; + // This seems to be reset for each folder update. + // + // This check seems to be failing in SeaMonkey builds, yet I can see no ill + // effects of this in the actual program. Fixing this is complex because of + // the messiness of new count management (see bug 507638 for a + // refactoring proposal, and attachment 398899 on bug 514801 for one possible + // fix to this particular test). So I am disabling this. + // Assert.equal(aFolderNewDelta, folderNew); + // Assert.equal(aDbNewDelta, dbNew - gPreviousDbNew); + // gPreviousDbNew = dbNew; + } catch (e) { + dump(e); + } +} + +// custom action to test offline status +var actionTestOffline = { + id: "mailnews@mozilla.org#testOffline", + name: "test if offline", + applyAction(aMsgHdrs, aActionValue, aListener, aType, aMsgWindow) { + for (let msgHdr of aMsgHdrs) { + let isOffline = !!(msgHdr.flags & Ci.nsMsgMessageFlags.Offline); + dump( + "in actionTestOffline, flags are " + + msgHdr.flags + + " subject is " + + msgHdr.subject + + " isOffline is " + + isOffline + + "\n" + ); + // XXX TODO: the offline flag is not set here when it should be in postplugin filters + // Assert.equal(isOffline, aActionValue == 'true'); + Assert.equal(msgHdr.subject, gMessageSubject); + } + }, + isValidForType(type, scope) { + return true; + }, + + validateActionValue(value, folder, type) { + return null; + }, + + allowDuplicates: false, + + needsBody: true, // set during test setup +}; diff --git a/comm/mailnews/imap/test/unit/test_imapFlagChange.js b/comm/mailnews/imap/test/unit/test_imapFlagChange.js new file mode 100644 index 0000000000..332e434527 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_imapFlagChange.js @@ -0,0 +1,216 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Test to ensure that imap flag changes made from a different profile/machine + * are stored in db. + */ + +var { MessageGenerator } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageGenerator.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +var gMessage; +var gSecondFolder; +var gSynthMessage; + +add_setup(async function () { + Services.prefs.setBoolPref( + "mail.server.default.autosync_offline_stores", + false + ); + + setupIMAPPump(); + + IMAPPump.daemon.createMailbox("secondFolder", { subscribed: true }); + + // build up a diverse list of messages + let messages = []; + let gMessageGenerator = new MessageGenerator(); + messages = messages.concat(gMessageGenerator.makeMessage()); + gSynthMessage = messages[0]; + + let msgURI = Services.io.newURI( + "data:text/plain;base64," + btoa(gSynthMessage.toMessageString()) + ); + gMessage = new ImapMessage(msgURI.spec, IMAPPump.mailbox.uidnext++, []); + IMAPPump.mailbox.addMessage(gMessage); + + // update folder to download header. + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, listener); + await listener.promise; +}); + +add_task(async function switchAwayFromInbox() { + let rootFolder = IMAPPump.incomingServer.rootFolder; + gSecondFolder = rootFolder + .getChildNamed("secondFolder") + .QueryInterface(Ci.nsIMsgImapMailFolder); + + // Selecting the second folder will close the cached connection + // on the inbox because fake server only supports one connection at a time. + // Then, we can poke at the message on the imap server directly, which + // simulates the user changing the message from a different machine, + // and Thunderbird discovering the change when it does a flag sync + // upon reselecting the Inbox. + let listener = new PromiseTestUtils.PromiseUrlListener(); + gSecondFolder.updateFolderWithListener(null, listener); + await listener.promise; +}); + +add_task(async function simulateForwardFlagSet() { + gMessage.setFlag("$Forwarded"); + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, listener); + await listener.promise; +}); + +add_task(async function checkForwardedFlagSet() { + let msgHdr = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID( + gSynthMessage.messageId + ); + Assert.equal( + msgHdr.flags & Ci.nsMsgMessageFlags.Forwarded, + Ci.nsMsgMessageFlags.Forwarded + ); + let listener = new PromiseTestUtils.PromiseUrlListener(); + gSecondFolder.updateFolderWithListener(null, listener); + await listener.promise; +}); + +add_task(async function clearForwardedFlag() { + gMessage.clearFlag("$Forwarded"); + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, listener); + await listener.promise; +}); + +add_task(async function checkForwardedFlagCleared() { + let msgHdr = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID( + gSynthMessage.messageId + ); + Assert.equal(msgHdr.flags & Ci.nsMsgMessageFlags.Forwarded, 0); + let listener = new PromiseTestUtils.PromiseUrlListener(); + gSecondFolder.updateFolderWithListener(null, listener); + await listener.promise; +}); + +add_task(async function setSeenFlag() { + gMessage.setFlag("\\Seen"); + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, listener); + await listener.promise; +}); + +add_task(async function checkSeenFlagSet() { + let msgHdr = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID( + gSynthMessage.messageId + ); + Assert.equal( + msgHdr.flags & Ci.nsMsgMessageFlags.Read, + Ci.nsMsgMessageFlags.Read + ); + let listener = new PromiseTestUtils.PromiseUrlListener(); + gSecondFolder.updateFolderWithListener(null, listener); + await listener.promise; +}); + +add_task(async function simulateRepliedFlagSet() { + gMessage.setFlag("\\Answered"); + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, listener); + await listener.promise; +}); + +add_task(async function checkRepliedFlagSet() { + let msgHdr = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID( + gSynthMessage.messageId + ); + Assert.equal( + msgHdr.flags & Ci.nsMsgMessageFlags.Replied, + Ci.nsMsgMessageFlags.Replied + ); + let listener = new PromiseTestUtils.PromiseUrlListener(); + gSecondFolder.updateFolderWithListener(null, listener); + await listener.promise; +}); + +add_task(async function simulateTagAdded() { + gMessage.setFlag("randomtag"); + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, listener); + await listener.promise; +}); + +add_task(async function checkTagSet() { + let msgHdr = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID( + gSynthMessage.messageId + ); + let keywords = msgHdr.getStringProperty("keywords"); + Assert.ok(keywords.includes("randomtag")); + let listener = new PromiseTestUtils.PromiseUrlListener(); + gSecondFolder.updateFolderWithListener(null, listener); + await listener.promise; +}); + +/** Test that the NonJunk tag from the server is noticed. */ +add_task(async function checkNonJunkTagSet() { + gMessage.clearFlag("NotJunk"); + gMessage.setFlag("NonJunk"); + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, listener); + await listener.promise; + + let msgHdr = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID( + gSynthMessage.messageId + ); + let junkScore = msgHdr.getStringProperty("junkscore"); + Assert.equal( + junkScore, + Ci.nsIJunkMailPlugin.IS_HAM_SCORE, + "NonJunk flag on server should mark as ham" + ); +}); + +/** Test that the NotJunk tag from the server is noticed. */ +add_task(async function checkNotJunkTagSet() { + gMessage.clearFlag("NonJunk"); + gMessage.setFlag("NotJunk"); + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, listener); + await listener.promise; + + let msgHdr = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID( + gSynthMessage.messageId + ); + let junkScore = msgHdr.getStringProperty("junkscore"); + Assert.equal( + junkScore, + Ci.nsIJunkMailPlugin.IS_HAM_SCORE, + "NotJunk flag on server should mark as ham" + ); +}); + +add_task(async function clearTag() { + gMessage.clearFlag("randomtag"); + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, listener); + await listener.promise; +}); + +add_task(function checkTagCleared() { + let msgHdr = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID( + gSynthMessage.messageId + ); + let keywords = msgHdr.getStringProperty("keywords"); + Assert.ok(!keywords.includes("randomtag")); +}); + +add_task(function endTest() { + teardownIMAPPump(); +}); diff --git a/comm/mailnews/imap/test/unit/test_imapFolderCopy.js b/comm/mailnews/imap/test/unit/test_imapFolderCopy.js new file mode 100644 index 0000000000..4b7e1bdb18 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_imapFolderCopy.js @@ -0,0 +1,137 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests the folder copying with IMAP. In particular, we're +// going to test copying local folders to imap servers, but other tests +// could be added. + +var { MessageGenerator } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageGenerator.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +var gEmptyLocal1, gEmptyLocal2, gEmptyLocal3, gNotEmptyLocal4; + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +add_setup(function () { + setupIMAPPump(); + + gEmptyLocal1 = localAccountUtils.rootFolder.createLocalSubfolder("empty 1"); + gEmptyLocal2 = localAccountUtils.rootFolder.createLocalSubfolder("empty 2"); + gEmptyLocal3 = localAccountUtils.rootFolder.createLocalSubfolder("empty 3"); + gNotEmptyLocal4 = + localAccountUtils.rootFolder.createLocalSubfolder("not empty 4"); + + let messageGenerator = new MessageGenerator(); + let message = messageGenerator.makeMessage(); + gNotEmptyLocal4.QueryInterface(Ci.nsIMsgLocalMailFolder); + gNotEmptyLocal4.addMessage(message.toMboxString()); + + // 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. + IMAPPump.inbox.hierarchyDelimiter = "/"; + IMAPPump.inbox.verifiedAsOnlineFolder = true; +}); + +add_task(async function copyFolder1() { + let copyListener = new PromiseTestUtils.PromiseCopyListener(); + MailServices.copy.copyFolder( + gEmptyLocal1, + IMAPPump.inbox, + false, + copyListener, + null + ); + await copyListener.promise; +}); + +add_task(async function copyFolder2() { + let copyListener = new PromiseTestUtils.PromiseCopyListener(); + MailServices.copy.copyFolder( + gEmptyLocal2, + IMAPPump.inbox, + false, + copyListener, + null + ); + await copyListener.promise; +}); + +add_task(async function copyFolder3() { + let copyListener = new PromiseTestUtils.PromiseCopyListener(); + MailServices.copy.copyFolder( + gEmptyLocal3, + IMAPPump.inbox, + false, + copyListener, + null + ); + await copyListener.promise; +}); + +add_task(function verifyFolders() { + let folder1 = IMAPPump.inbox.getChildNamed("empty 1"); + let folder2 = IMAPPump.inbox.getChildNamed("empty 2"); + let folder3 = IMAPPump.inbox.getChildNamed("empty 3"); + Assert.ok(folder1 !== null); + Assert.ok(folder2 !== null); + Assert.ok(folder3 !== null); +}); + +add_task(async function moveImapFolder1() { + let folder1 = IMAPPump.inbox.getChildNamed("empty 1"); + let folder2 = IMAPPump.inbox.getChildNamed("empty 2"); + let copyListener = new PromiseTestUtils.PromiseCopyListener(); + MailServices.copy.copyFolder(folder2, folder1, true, copyListener, null); + await copyListener.promise; +}); + +add_task(async function moveImapFolder2() { + let folder1 = IMAPPump.inbox.getChildNamed("empty 1"); + let folder3 = IMAPPump.inbox.getChildNamed("empty 3"); + let copyListener = new PromiseTestUtils.PromiseCopyListener(); + MailServices.copy.copyFolder(folder3, folder1, true, copyListener, null); + await copyListener.promise; +}); + +add_task(function verifyImapFolders() { + let folder1 = IMAPPump.inbox.getChildNamed("empty 1"); + let folder2 = folder1.getChildNamed("empty 2"); + let folder3 = folder1.getChildNamed("empty 3"); + Assert.ok(folder1 !== null); + Assert.ok(folder2 !== null); + Assert.ok(folder3 !== null); +}); + +add_task(async function testImapFolderCopyFailure() { + IMAPPump.daemon.commandToFail = "APPEND"; + // we expect NS_MSG_ERROR_IMAP_COMMAND_FAILED; + const NS_MSG_ERROR_IMAP_COMMAND_FAILED = 0x80550021; + let copyListener = new PromiseTestUtils.PromiseCopyListener(); + MailServices.copy.copyFolder( + gNotEmptyLocal4, + IMAPPump.inbox, + false, + copyListener, + null + ); + await Assert.rejects( + copyListener.promise, + e => { + return e === NS_MSG_ERROR_IMAP_COMMAND_FAILED; + }, + "NS_MSG_ERROR_IMAP_COMMAND_FAILED should be the cause of the error" + ); +}); + +add_task(function teardown() { + teardownIMAPPump(); +}); diff --git a/comm/mailnews/imap/test/unit/test_imapHdrChunking.js b/comm/mailnews/imap/test/unit/test_imapHdrChunking.js new file mode 100644 index 0000000000..8fc23e8502 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_imapHdrChunking.js @@ -0,0 +1,168 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Tests imap msg header download chunking + */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { MessageGenerator, MessageScenarioFactory } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageGenerator.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); +var { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); + +/** + * Keep it so that OVERALL_MESSAGES % CHUNKING_SIZE !== 0. + * With a modulo operator for CHUNKING_SIZE and a prime number for + * OVERALL_MESSAGES this should prove that there have been a + * chunking process without being depended on the first chunk. + */ +const CHUNKING_SIZE = 3; +const OVERALL_MESSAGES = 137; + +// Dummy message window so we can say the inbox is open in a window. +var dummyMsgWindow = Cc["@mozilla.org/messenger/msgwindow;1"].createInstance( + Ci.nsIMsgWindow +); + +function FolderIntPropertyChangedListener() { + this._promise = new Promise(resolve => { + this._resolve = resolve; + }); + this._gotNewMailBiff = false; +} + +FolderIntPropertyChangedListener.prototype = { + onFolderIntPropertyChanged(aItem, aProperty, aOldValue, aNewValue) { + if ( + aProperty == "BiffState" && + aNewValue == Ci.nsIMsgFolder.nsMsgBiffState_NewMail + ) { + this._gotNewMailBiff = true; + this._resolve(); + } + }, + get promise() { + return this._promise; + }, + get gotNewMailBiff() { + return this._gotNewMailBiff; + }, +}; + +var gFolderListener = new FolderIntPropertyChangedListener(); +/** Used to store a listener between tasks for inspecting chunking behaviour. */ +var gListener = new PromiseTestUtils.PromiseUrlListener(); + +add_setup(async function () { + Assert.equal( + OVERALL_MESSAGES % CHUNKING_SIZE !== 0, + true, + "const sanity check" + ); + setupIMAPPump(); + // We need to register the dummyMsgWindow so that we'll think the + // Inbox is open in a folder and fetch headers in chunks. + dummyMsgWindow.openFolder = IMAPPump.inbox; + MailServices.mailSession.AddMsgWindow(dummyMsgWindow); + MailServices.mailSession.AddFolderListener( + gFolderListener, + Ci.nsIFolderListener.intPropertyChanged + ); + + // Set chunk size to CHUNKING_SIZE, so we'll have to chain several requests to get + // OVERALL_MESSAGES headers. + Services.prefs.setIntPref("mail.imap.hdr_chunk_size", CHUNKING_SIZE); + // Turn off offline sync to avoid complications in verifying that we can + // run a url after the first header chunk. + Services.prefs.setBoolPref( + "mail.server.server1.autosync_offline_stores", + false + ); +}); + +// Upload messages to the imap fake server Inbox. +add_task(async function uploadImapMessages() { + // make OVERALL_MESSAGES messages + let messageGenerator = new MessageGenerator(); + let scenarioFactory = new MessageScenarioFactory(messageGenerator); + + // build up a list of messages + let messages = []; + messages = messages.concat(scenarioFactory.directReply(OVERALL_MESSAGES)); + + // Add OVERALL_MESSAGES messages with uids 1,2,3...,OVERALL_MESSAGES. + let imapInbox = IMAPPump.daemon.getMailbox("INBOX"); + // Create the ImapMessages and store them on the mailbox. + messages.forEach(function (message) { + let dataUri = Services.io.newURI( + "data:text/plain;base64," + btoa(message.toMessageString()) + ); + imapInbox.addMessage( + new ImapMessage(dataUri.spec, imapInbox.uidnext++, []) + ); + }); + // Do not wait for the listener to finish. + // We want to observe the message batches in the update process. + // updateFolderWithListener with null for nsIMsgWindow makes biff notify. + IMAPPump.inbox.updateFolderWithListener(null, gListener); +}); + +add_task(async function testMessageFetched() { + // If we're really chunking, then the message fetch should have started before + // we finished the updateFolder URL. + await TestUtils.waitForCondition(() => { + return gFolderListener.gotNewMailBiff === true; + }); + Assert.ok(gFolderListener.gotNewMailBiff); + + // We do not check for the first chunk as this is unreliable without explicit + // listeners/events. + // Instead we are checking if there's no rest of the division with + // CHUNKING_SIZE while the chunking process is ongoing. + // It's important that the chunking is intact and as well not failing + // randomly in the test infrastructure. + // See at the CHUNKING_SIZE and OVERALL_MESSAGES declarations. + // + // HINT: + // If this causes future problems because stuff getting faster, + // try to increase the overall message count. + await TestUtils.waitForCondition(() => { + let messagesDBFolder = IMAPPump.inbox.msgDatabase.dBFolderInfo.numMessages; + if (messagesDBFolder !== 0) { + Assert.equal( + messagesDBFolder % CHUNKING_SIZE, + 0, + `${messagesDBFolder} messages in folder should be of chunk size ${CHUNKING_SIZE}` + ); // This is the primary test. + return true; + } else if (messagesDBFolder === OVERALL_MESSAGES) { + throw new Error( + `Batching failed in sizes of ${CHUNKING_SIZE} found instead ${OVERALL_MESSAGES} immediately` + ); + } + return false; // Rerun waitForCondition. + }, 50); +}); + +add_task(async function testHdrsDownloaded() { + await gListener.promise; // Now we wait for the finished update of the Folder. + // Make sure that we got all OVERALL_MESSAGES headers. + Assert.equal( + IMAPPump.inbox.msgDatabase.dBFolderInfo.numMessages, + OVERALL_MESSAGES + ); +}); + +// Cleanup +add_task(async function endTest() { + teardownIMAPPump(); +}); diff --git a/comm/mailnews/imap/test/unit/test_imapHdrStreaming.js b/comm/mailnews/imap/test/unit/test_imapHdrStreaming.js new file mode 100644 index 0000000000..ca148dace6 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_imapHdrStreaming.js @@ -0,0 +1,74 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This test checks if the imap message service code streams headers correctly. + * It checks that streaming headers for messages stored for offline use works. + * It doesn't test streaming messages that haven't been stored for offline use + * because that's not implemented yet, and it's unclear if anyone will want it. + */ + +var { MessageGenerator } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageGenerator.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +setupIMAPPump(); + +var gMsgFile1 = do_get_file("../../../data/bugmail10"); +var gMsgId1 = "200806061706.m56H6RWT004933@mrapp54.mozilla.org"; + +// Adds some messages directly to a mailbox (e.g. new mail). +function addMessagesToServer(messages, mailbox) { + // For every message we have, we need to convert it to a file:/// URI. + messages.forEach(function (message) { + let URI = Services.io + .newFileURI(message.file) + .QueryInterface(Ci.nsIFileURL); + // Create the ImapMessage and store it on the mailbox. + mailbox.addMessage(new ImapMessage(URI.spec, mailbox.uidnext++, [])); + }); +} + +add_setup(async function () { + // Add a couple of messages to the INBOX + // this is synchronous, afaik. + addMessagesToServer( + [{ file: gMsgFile1, messageId: gMsgId1 }], + IMAPPump.daemon.getMailbox("INBOX") + ); + Services.prefs.setBoolPref( + "mail.server.server1.autosync_offline_stores", + false + ); + // Update IMAP Folder. + let listenerUpdate = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, listenerUpdate); + await listenerUpdate.promise; + // Download all for offline. + let listenerDownload = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.downloadAllForOffline(listenerDownload, null); + await listenerDownload.promise; +}); + +add_task(async function test_streamHeaders() { + let newMsgHdr = IMAPPump.inbox.GetMessageHeader(1); + let msgURI = newMsgHdr.folder.getUriForMsg(newMsgHdr); + let msgServ = MailServices.messageServiceFromURI(msgURI); + // We use this as a display consumer + let streamListener = new PromiseTestUtils.PromiseStreamListener(); + msgServ.streamHeaders(msgURI, streamListener, null, true); + let data = await streamListener.promise; + Assert.ok(data.includes("Content-Type")); +}); + +add_task(async function endTest() { + teardownIMAPPump(); +}); diff --git a/comm/mailnews/imap/test/unit/test_imapHighWater.js b/comm/mailnews/imap/test/unit/test_imapHighWater.js new file mode 100644 index 0000000000..3fcd4bcc23 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_imapHighWater.js @@ -0,0 +1,194 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from ../../../test/resources/alertTestUtils.js */ +load("../../../resources/alertTestUtils.js"); + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { MessageGenerator, MessageScenarioFactory } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageGenerator.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +var gIMAPDaemon, gServer, gIMAPIncomingServer; + +var gIMAPInbox; +var gFolder1, gRootFolder; + +// Adds some messages directly to a mailbox (eg new mail) +function addMessagesToServer(messages, mailbox) { + // Create the ImapMessages and store them on the mailbox + messages.forEach(function (message) { + let dataUri = Services.io.newURI( + "data:text/plain;base64," + btoa(message.toMessageString()) + ); + mailbox.addMessage(new ImapMessage(dataUri.spec, mailbox.uidnext++, [])); + }); +} + +add_setup(function () { + localAccountUtils.loadLocalMailAccount(); + + /* + * Set up an IMAP server. + */ + gIMAPDaemon = new ImapDaemon(); + gServer = makeServer(gIMAPDaemon, ""); + gIMAPDaemon.createMailbox("folder 1", { subscribed: true }); + gIMAPIncomingServer = createLocalIMAPServer(gServer.port); + gIMAPIncomingServer.maximumConnectionsNumber = 1; + + // We need an identity so that updateFolder doesn't fail + let localAccount = MailServices.accounts.createAccount(); + let identity = MailServices.accounts.createIdentity(); + localAccount.addIdentity(identity); + localAccount.defaultIdentity = identity; + localAccount.incomingServer = localAccountUtils.incomingServer; + + // Let's also have another account, using the same identity + let imapAccount = MailServices.accounts.createAccount(); + imapAccount.addIdentity(identity); + imapAccount.defaultIdentity = identity; + imapAccount.incomingServer = gIMAPIncomingServer; + MailServices.accounts.defaultAccount = imapAccount; + + // pref tuning: one connection only, turn off notifications + Services.prefs.setBoolPref("mail.biff.play_sound", false); + Services.prefs.setBoolPref("mail.biff.show_alert", false); + Services.prefs.setBoolPref("mail.biff.show_tray_icon", false); + Services.prefs.setBoolPref("mail.biff.animate_dock_icon", false); + Services.prefs.setBoolPref( + "mail.server.default.autosync_offline_stores", + false + ); + // Don't prompt about offline download when going offline + Services.prefs.setIntPref("offline.download.download_messages", 2); +}); + +add_setup(function () { + // make 10 messages + let messageGenerator = new MessageGenerator(); + let scenarioFactory = new MessageScenarioFactory(messageGenerator); + + // build up a list of messages + let messages = []; + messages = messages.concat(scenarioFactory.directReply(10)); + + // Add 10 messages with uids 1-10. + let imapInbox = gIMAPDaemon.getMailbox("INBOX"); + addMessagesToServer(messages, imapInbox); + messages = []; + messages = messages.concat(messageGenerator.makeMessage()); + // Add a single message to move target folder. + addMessagesToServer(messages, gIMAPDaemon.getMailbox("folder 1")); + + // Get the IMAP inbox... + gRootFolder = gIMAPIncomingServer.rootFolder; + gIMAPInbox = gRootFolder + .getFolderWithFlags(Ci.nsMsgFolderFlags.Inbox) + .QueryInterface(Ci.nsIMsgImapMailFolder); +}); + +add_task(async function doMoves() { + // update folders to download headers. + let urlListenerInbox = new PromiseTestUtils.PromiseUrlListener(); + gIMAPInbox.updateFolderWithListener(null, urlListenerInbox); + await urlListenerInbox.promise; + gFolder1 = gRootFolder + .getChildNamed("folder 1") + .QueryInterface(Ci.nsIMsgImapMailFolder); + let urlListenerFolder1 = new PromiseTestUtils.PromiseUrlListener(); + gFolder1.updateFolderWithListener(null, urlListenerFolder1); + await urlListenerFolder1.promise; + // get five messages to move from Inbox to folder 1. + let headers1 = []; + let count = 0; + for (let header of gIMAPInbox.msgDatabase.enumerateMessages()) { + if (count >= 5) { + break; + } + if (header instanceof Ci.nsIMsgDBHdr) { + headers1.push(header); + } + count++; + } + // this will add dummy headers with keys > 0xffffff80 + let copyListenerDummyHeaders = new PromiseTestUtils.PromiseCopyListener(); + MailServices.copy.copyMessages( + gIMAPInbox, + headers1, + gFolder1, + true, + copyListenerDummyHeaders, + gDummyMsgWindow, + true + ); + await copyListenerDummyHeaders.promise; + + let urlListenerInboxAfterDummy = new PromiseTestUtils.PromiseUrlListener(); + gIMAPInbox.updateFolderWithListener(null, urlListenerInboxAfterDummy); + await urlListenerInboxAfterDummy.promise; + + let urlListenerFolder1AfterDummy = new PromiseTestUtils.PromiseUrlListener(); + gFolder1.updateFolderWithListener( + gDummyMsgWindow, + urlListenerFolder1AfterDummy + ); + await urlListenerFolder1AfterDummy.promise; + + // Check that playing back offline events gets rid of dummy + // headers, and thus highWater is recalculated. + Assert.equal(gFolder1.msgDatabase.dBFolderInfo.highWater, 6); + headers1 = []; + count = 0; + for (let header of gIMAPInbox.msgDatabase.enumerateMessages()) { + if (count >= 5) { + break; + } + if (header instanceof Ci.nsIMsgDBHdr) { + headers1.push(header); + } + count++; + } + // Check that copyMessages will handle having a high highwater mark. + // It will thrown an exception if it can't. + let msgHdr = gFolder1.msgDatabase.createNewHdr(0xfffffffd); + gFolder1.msgDatabase.addNewHdrToDB(msgHdr, false); + let copyListenerHighWater = new PromiseTestUtils.PromiseCopyListener(); + MailServices.copy.copyMessages( + gIMAPInbox, + headers1, + gFolder1, + true, + copyListenerHighWater, + gDummyMsgWindow, + true + ); + await copyListenerHighWater.promise; + gServer.performTest("UID COPY"); + + gFolder1.msgDatabase.deleteHeader(msgHdr, null, true, false); + let urlListenerInboxAfterDelete = new PromiseTestUtils.PromiseUrlListener(); + gIMAPInbox.updateFolderWithListener(null, urlListenerInboxAfterDelete); + await urlListenerInboxAfterDelete.promise; + // this should clear the dummy headers. + let urlListenerFolder1AfterDelete = new PromiseTestUtils.PromiseUrlListener(); + gFolder1.updateFolderWithListener( + gDummyMsgWindow, + urlListenerFolder1AfterDelete + ); + await urlListenerFolder1AfterDelete.promise; + Assert.equal(gFolder1.msgDatabase.dBFolderInfo.highWater, 11); +}); + +add_task(function endTest() { + Services.io.offline = true; + gServer.performTest("LOGOUT"); + gIMAPIncomingServer.closeCachedConnections(); + gServer.stop(); +}); diff --git a/comm/mailnews/imap/test/unit/test_imapID.js b/comm/mailnews/imap/test/unit/test_imapID.js new file mode 100644 index 0000000000..fe1f70fbdf --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_imapID.js @@ -0,0 +1,40 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Test to ensure that we handle the RFC2197 ID command. + */ + +/* import-globals-from ../../../test/resources/logHelper.js */ +load("../../../resources/logHelper.js"); + +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +var kIDResponse = + '("name" "GImap" "vendor" "Google, Inc." "support-url" "http://mail.google.com/support")'; + +add_setup(async function () { + setupIMAPPump("GMail"); + IMAPPump.daemon.idResponse = kIDResponse; + + // update folder to kick start tests. + let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, promiseUrlListener); + await promiseUrlListener.promise; +}); + +add_task(async function updateInbox() { + let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, promiseUrlListener); + await promiseUrlListener.promise; +}); + +add_task(function checkIDHandling() { + Assert.equal(IMAPPump.daemon.clientID, '("name" "xpcshell" "version" "1")'); + Assert.equal(IMAPPump.incomingServer.serverIDPref, kIDResponse); +}); + +add_task(teardownIMAPPump); diff --git a/comm/mailnews/imap/test/unit/test_imapMove.js b/comm/mailnews/imap/test/unit/test_imapMove.js new file mode 100644 index 0000000000..e19896ef90 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_imapMove.js @@ -0,0 +1,88 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This tests that we use IMAP move if the IMAP server supports it. + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +/* import-globals-from ../../../test/resources/logHelper.js */ +/* import-globals-from ../../../test/resources/MessageGenerator.jsm */ +load("../../../resources/logHelper.js"); +load("../../../resources/MessageGenerator.jsm"); + +var gFolder1; + +var tests = [setupCUSTOM1, startTest, doMove, testMove, teardownIMAPPump]; + +function setupCUSTOM1() { + setupIMAPPump("CUSTOM1"); + Services.prefs.setBoolPref( + "mail.server.default.autosync_offline_stores", + false + ); +} + +async function startTest() { + IMAPPump.incomingServer.rootFolder.createSubfolder("folder 1", null); + await PromiseTestUtils.promiseFolderAdded("folder 1"); + + addImapMessage(); + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, listener); + await listener.promise; + + // ...and download for offline use. + let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.downloadAllForOffline(promiseUrlListener, null); + await promiseUrlListener.promise; +} + +async function doMove() { + let rootFolder = IMAPPump.incomingServer.rootFolder; + gFolder1 = rootFolder + .getChildNamed("folder 1") + .QueryInterface(Ci.nsIMsgImapMailFolder); + let msg = IMAPPump.inbox.msgDatabase.getMsgHdrForKey( + IMAPPump.mailbox.uidnext - 1 + ); + IMAPPump.server._test = true; + let listener = new PromiseTestUtils.PromiseCopyListener(); + MailServices.copy.copyMessages( + IMAPPump.inbox, + [msg], + gFolder1, + true, + listener, + null, + false + ); + IMAPPump.server.performTest("UID MOVE"); + await listener.promise; +} + +async function testMove() { + Assert.equal(IMAPPump.inbox.getTotalMessages(false), 0); + let listener = new PromiseTestUtils.PromiseUrlListener(); + gFolder1.updateFolderWithListener(null, listener); + await listener.promise; + Assert.equal(gFolder1.getTotalMessages(false), 1); + + // maildir should also delete the files. + if (IMAPPump.inbox.msgStore.storeType == "maildir") { + let curDir = IMAPPump.inbox.filePath.clone(); + curDir.append("cur"); + Assert.ok(curDir.exists()); + Assert.ok(curDir.isDirectory()); + let curEnum = curDir.directoryEntries; + // the directory should be empty, fails from bug 771643 + Assert.ok(!curEnum.hasMoreElements()); + } +} + +function run_test() { + tests.forEach(x => add_task(x)); + run_next_test(); +} diff --git a/comm/mailnews/imap/test/unit/test_imapPasswordFailure.js b/comm/mailnews/imap/test/unit/test_imapPasswordFailure.js new file mode 100644 index 0000000000..b28c646907 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_imapPasswordFailure.js @@ -0,0 +1,179 @@ +/** + * This test checks to see if the imap password failure is handled correctly. + * The steps are: + * - Have an invalid password in the password database. + * - Check we get a prompt asking what to do. + * - Check retry does what it should do. + * - Check cancel does what it should do. + * - Re-initiate connection, this time select enter new password, check that + * we get a new password prompt and can enter the password. + */ + +/* import-globals-from ../../../test/resources/alertTestUtils.js */ +/* import-globals-from ../../../test/resources/passwordStorage.js */ +load("../../../resources/alertTestUtils.js"); +load("../../../resources/passwordStorage.js"); + +var kUserName = "user"; +var kInvalidPassword = "imaptest"; +var kValidPassword = "password"; + +var incomingServer, server; +var attempt = 0; + +function confirmExPS( + aDialogTitle, + aText, + aButtonFlags, + aButton0Title, + aButton1Title, + aButton2Title, + aCheckMsg, + aCheckState +) { + switch (++attempt) { + // First attempt, retry. + case 1: + dump("\nAttempting retry\n"); + return 0; + // Second attempt, cancel. + case 2: + dump("\nCancelling login attempt\n"); + return 1; + // Third attempt, retry. + case 3: + dump("\nAttempting Retry\n"); + return 0; + // Fourth attempt, enter a new password. + case 4: + dump("\nEnter new password\n"); + return 2; + default: + do_throw("unexpected attempt number " + attempt); + return 1; + } +} + +function promptPasswordPS( + aParent, + aDialogTitle, + aText, + aPassword, + aCheckMsg, + aCheckState +) { + if (attempt == 4) { + aPassword.value = kValidPassword; + aCheckState.value = true; + return true; + } + return false; +} + +add_task(async function () { + do_test_pending(); + + // Prepare files for passwords (generated by a script in bug 1018624). + await setupForPassword("signons-mailnews1.8-imap.json"); + + registerAlertTestUtils(); + + let daemon = new ImapDaemon(); + daemon.createMailbox("Subscribed", { subscribed: true }); + server = makeServer(daemon, "", { + // Make username of server match the singons.txt file + // (pw there is intentionally invalid) + kUsername: kUserName, + kPassword: kValidPassword, + }); + server.setDebugLevel(fsDebugAll); + + incomingServer = createLocalIMAPServer(server.port); + + // PerformExpand expects us to already have a password loaded into the + // incomingServer when we call it, so force a get password call to get it + // out of the signons file (first removing the value that + // createLocalIMAPServer puts in there). + incomingServer.password = ""; + let password = incomingServer.getPasswordWithUI( + "Prompt Message", + "Prompt Title" + ); + + // The fake server expects one password, but we're feeding it an invalid one + // initially so that we can check what happens when password is denied. + Assert.equal(password, kInvalidPassword); + + // First step, try and perform a subscribe where we won't be able to log in. + // This covers attempts 1 and 2 in confirmEx. + dump("\nperformExpand 1\n\n"); + + incomingServer.performExpand(gDummyMsgWindow); + server.performTest("SUBSCRIBE"); + + dump("\nfinished subscribe 1\n\n"); + + Assert.equal(attempt, 2); + + let rootFolder = incomingServer.rootFolder; + Assert.ok(rootFolder.containsChildNamed("Inbox")); + Assert.ok(!rootFolder.containsChildNamed("Subscribed")); + + // Check that we haven't forgotten the login even though we've retried and cancelled. + let logins = Services.logins.findLogins( + "imap://localhost", + null, + "imap://localhost" + ); + + Assert.equal(logins.length, 1); + Assert.equal(logins[0].username, kUserName); + Assert.equal(logins[0].password, kInvalidPassword); + + server.resetTest(); + + dump("\nperformExpand 2\n\n"); + + incomingServer.performExpand(gDummyMsgWindow); + server.performTest("SUBSCRIBE"); + + dump("\nfinished subscribe 2\n"); + + Assert.ok(rootFolder.containsChildNamed("Inbox")); + Assert.ok(rootFolder.containsChildNamed("Subscribed")); + + // Now check the new one has been saved. + logins = Services.logins.findLogins( + "imap://localhost", + null, + "imap://localhost" + ); + + Assert.equal(logins.length, 1); + Assert.equal(logins[0].username, kUserName); + Assert.equal(logins[0].password, kValidPassword); + + // Remove the login via the incoming server. + incomingServer.forgetPassword(); + logins = Services.logins.findLogins( + "imap://localhost", + null, + "imap://localhost" + ); + + Assert.equal(logins.length, 0); + + do_timeout(500, endTest); +}); + +function endTest() { + incomingServer.closeCachedConnections(); + server.stop(); + + var thread = gThreadManager.currentThread; + while (thread.hasPendingEvents()) { + thread.processNextEvent(true); + } + + do_test_finished(); +} diff --git a/comm/mailnews/imap/test/unit/test_imapProtocols.js b/comm/mailnews/imap/test/unit/test_imapProtocols.js new file mode 100644 index 0000000000..2043c44567 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_imapProtocols.js @@ -0,0 +1,59 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* + * Test suite for IMAP nsIProtocolHandler implementations. + */ + +var defaultProtocolFlags = + Ci.nsIProtocolHandler.URI_NORELATIVE | + Ci.nsIProtocolHandler.URI_FORBIDS_AUTOMATIC_DOCUMENT_REPLACEMENT | + Ci.nsIProtocolHandler.URI_DANGEROUS_TO_LOAD | + Ci.nsIProtocolHandler.ALLOWS_PROXY | + Ci.nsIProtocolHandler.URI_FORBIDS_COOKIE_ACCESS | + Ci.nsIProtocolHandler.ORIGIN_IS_FULL_SPEC; + +var protocols = [ + { + protocol: "imap", + urlSpec: "imap://user@localhost/", + defaultPort: Ci.nsIImapUrl.DEFAULT_IMAP_PORT, + }, + // XXX Imaps protocol not available via nsIProtocolHandler yet. + // { + // protocol: "imaps", + // urlSpec: "iamps://user@localhost/", + // defaultPort: Ci.nsIImapUrl.DEFAULT_IMAPS_PORT, + // }, +]; + +function run_test() { + // We need a server to match the urlSpecs above. + createLocalIMAPServer(); + + for (var part = 0; part < protocols.length; ++part) { + print("protocol: " + protocols[part].protocol); + + var pH = Cc[ + "@mozilla.org/network/protocol;1?name=" + protocols[part].protocol + ].createInstance(Ci.nsIProtocolHandler); + + Assert.equal(pH.scheme, protocols[part].protocol); + Assert.equal( + Services.io.getDefaultPort(pH.scheme), + protocols[part].defaultPort + ); + Assert.equal(Services.io.getProtocolFlags(pH.scheme), defaultProtocolFlags); + + // Whip through some of the ports to check we get the right results. + // IMAP allows connecting to any port. + for (let i = 0; i < 1024; ++i) { + Assert.ok(pH.allowPort(i, "")); + } + + // Check we get a URI when we ask for one + var uri = Services.io.newURI(protocols[part].urlSpec); + + uri.QueryInterface(Ci.nsIImapUrl); + + Assert.equal(uri.spec, protocols[part].urlSpec); + } +} diff --git a/comm/mailnews/imap/test/unit/test_imapProxy.js b/comm/mailnews/imap/test/unit/test_imapProxy.js new file mode 100644 index 0000000000..aa935fff68 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_imapProxy.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +// Test that IMAP over a SOCKS proxy works. + +var { NetworkTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/NetworkTestUtils.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); +/* import-globals-from ../../../test/resources/MessageGenerator.jsm */ +load("../../../resources/MessageGenerator.jsm"); + +var server, daemon, incomingServer; + +const PORT = 143; + +add_setup(async function () { + // Disable new mail notifications + Services.prefs.setBoolPref("mail.biff.play_sound", false); + Services.prefs.setBoolPref("mail.biff.show_alert", false); + Services.prefs.setBoolPref("mail.biff.show_tray_icon", false); + Services.prefs.setBoolPref("mail.biff.animate_dock_icon", false); + + daemon = new ImapDaemon(); + server = makeServer(daemon, ""); + + let messages = []; + let messageGenerator = new MessageGenerator(); + messages = messages.concat(messageGenerator.makeMessage()); + let dataUri = Services.io.newURI( + "data:text/plain;base64," + btoa(messages[0].toMessageString()) + ); + let imapMsg = new ImapMessage(dataUri.spec, daemon.inbox.uidnext++, []); + daemon.inbox.addMessage(imapMsg); + + NetworkTestUtils.configureProxy("imap.tinderbox.invalid", PORT, server.port); + + // Set up the basic accounts and folders + incomingServer = createLocalIMAPServer(PORT, "imap.tinderbox.invalid"); + let identity = MailServices.accounts.createIdentity(); + let imapAccount = MailServices.accounts.createAccount(); + imapAccount.addIdentity(identity); + imapAccount.defaultIdentity = identity; + imapAccount.incomingServer = incomingServer; +}); + +add_task(async function downloadEmail() { + let inboxFolder = incomingServer.rootFolder.getChildNamed("INBOX"); + + // Check that we haven't got any messages in the folder, if we have its a test + // setup issue. + Assert.equal(inboxFolder.getTotalMessages(false), 0); + + // Now get the mail + let asyncUrlListener = new PromiseTestUtils.PromiseUrlListener(); + inboxFolder.getNewMessages(null, asyncUrlListener); + await asyncUrlListener.promise; + + // We downloaded a message, so it works! + Assert.equal(inboxFolder.getTotalMessages(false), 1); +}); + +add_task(async function cleanUp() { + NetworkTestUtils.shutdownServers(); + incomingServer.closeCachedConnections(); + server.stop(); +}); diff --git a/comm/mailnews/imap/test/unit/test_imapRename.js b/comm/mailnews/imap/test/unit/test_imapRename.js new file mode 100644 index 0000000000..f346729a92 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_imapRename.js @@ -0,0 +1,43 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This tests that renaming non-ASCII name folder works. + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +add_setup(async function () { + setupIMAPPump(); + Services.prefs.setBoolPref( + "mail.server.default.autosync_offline_stores", + false + ); + IMAPPump.incomingServer.rootFolder.createSubfolder("folder 1", null); + await PromiseTestUtils.promiseFolderAdded("folder 1"); + + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, listener); + await listener.promise; +}); + +add_task(async function test_rename() { + let rootFolder = IMAPPump.incomingServer.rootFolder; + let targetFolder = rootFolder.getChildNamed("folder 1"); + + targetFolder.rename("folder \u00e1", null); + + IMAPPump.server.performTest("RENAME"); + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, listener); + await listener.promise; + + let folder = rootFolder.getChildNamed("folder \u00e1"); + Assert.ok(folder.msgDatabase.summaryValid); + Assert.equal("folder &AOE-", folder.filePath.leafName); + Assert.equal("folder \u00e1", folder.prettyName); +}); diff --git a/comm/mailnews/imap/test/unit/test_imapSearch.js b/comm/mailnews/imap/test/unit/test_imapSearch.js new file mode 100644 index 0000000000..975d1f2dae --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_imapSearch.js @@ -0,0 +1,348 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* Tests traditional (non-gloda) search on IMAP folders. + * Derived from a combination of test_imapPump.js and test_search.js + * Original author: Kent James <kent@caspia.com> + */ + +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +// headers we will store in db +// set value of headers we want parsed into the db +Services.prefs.setCharPref( + "mailnews.customDBHeaders", + "x-spam-status oneliner twoliner threeliner nospace withspace" +); +Assert.equal( + Services.prefs.getCharPref("mailnews.customDBHeaders"), + "x-spam-status oneliner twoliner threeliner nospace withspace" +); + +// set customHeaders, which post-bug 363238 should get added to the db. Note that all headers but the last +// seem to end in colon. +Services.prefs.setCharPref( + "mailnews.customHeaders", + "x-uidl: x-bugzilla-watch-reason: x-bugzilla-component: received: x-spam-checker-version" +); + +// Messages to load must have CRLF line endings, that is Windows style +var gMessage = "bugmail12"; // message file used as the test message + +/* +/* + * Testing of general mail search features. + * + * This tests some search attributes not tested by other specific tests, + * e.g., test_searchTag.js or test_searchJunk.js + */ +/* import-globals-from ../../../test/resources/searchTestUtils.js */ +load("../../../resources/searchTestUtils.js"); + +var Isnt = Ci.nsMsgSearchOp.Isnt; +var Is = Ci.nsMsgSearchOp.Is; +var Contains = Ci.nsMsgSearchOp.Contains; +var BeginsWith = Ci.nsMsgSearchOp.BeginsWith; +var EndsWith = Ci.nsMsgSearchOp.EndsWith; + +var OtherHeader = Ci.nsMsgSearchAttrib.OtherHeader; +var From = Ci.nsMsgSearchAttrib.Sender; +var Subject = Ci.nsMsgSearchAttrib.Subject; + +var searchTests = [ + // test the To: header + { + testString: "PrimaryEmail1@test.invalid", + testAttribute: From, + op: Is, + count: 1, + }, + { + testString: "PrimaryEmail1@test.invalid", + testAttribute: From, + op: Isnt, + count: 0, + }, + { + testString: "PrimaryEmail", + testAttribute: From, + op: BeginsWith, + count: 1, + }, + { + testString: "invalid", + testAttribute: From, + op: BeginsWith, + count: 0, + }, + { + testString: "invalid", + testAttribute: From, + op: EndsWith, + count: 1, + }, + { + testString: "Primary", + testAttribute: From, + op: EndsWith, + count: 0, + }, + { + testString: "QAContact", + testAttribute: OtherHeader, + op: BeginsWith, + count: 1, + }, + { + testString: "filters", + testAttribute: OtherHeader, + op: BeginsWith, + count: 0, + }, + { + testString: "mail.bugs", + testAttribute: OtherHeader, + op: EndsWith, + count: 1, + }, + { + testString: "QAContact", + testAttribute: OtherHeader, + op: EndsWith, + count: 0, + }, + { + testString: "QAcontact filters@mail.bugs", + testAttribute: OtherHeader, + op: Is, + count: 1, + }, + { + testString: "filters@mail.bugs", + testAttribute: OtherHeader, + op: Is, + count: 0, + }, + { + testString: "QAcontact filters@mail.bugs", + testAttribute: OtherHeader, + op: Isnt, + count: 0, + }, + { + testString: "QAcontact", + testAttribute: OtherHeader, + op: Isnt, + count: 1, + }, + { + testString: "filters", + testAttribute: OtherHeader, + op: Contains, + count: 1, + }, + { + testString: "foobar", + testAttribute: OtherHeader, + op: Contains, + count: 0, + }, + // test accumulation of received header + { + // only in first received + testString: "caspiaco", + testAttribute: OtherHeader, + op: Contains, + customHeader: "Received", + count: 1, + }, + { + // only in second + testString: "webapp01.sj.mozilla.com", + testAttribute: OtherHeader, + op: Contains, + customHeader: "received", + count: 1, + }, + { + // in neither + testString: "not there", + testAttribute: OtherHeader, + op: Contains, + customHeader: "received", + count: 0, + }, + // test multiple line arbitrary headers + { + // in the first line + testString: "SpamAssassin 3.2.3", + testAttribute: OtherHeader, + op: Contains, + customHeader: "X-Spam-Checker-Version", + count: 1, + }, + { + // in the second line + testString: "host29.example.com", + testAttribute: OtherHeader, + op: Contains, + customHeader: "X-Spam-Checker-Version", + count: 1, + }, + { + // spans two lines with space + testString: "on host29.example.com", + testAttribute: OtherHeader, + op: Contains, + customHeader: "X-Spam-Checker-Version", + count: 1, + }, + // subject spanning several lines + { + // on the first line + testString: "A filter will", + testAttribute: Subject, + op: Contains, + count: 1, + }, + { + testString: "I do not exist", + testAttribute: Subject, + op: Contains, + count: 0, + }, + { + // on the second line + testString: "this message", + testAttribute: Subject, + op: Contains, + count: 1, + }, + { + // spanning second and third line + testString: "over many", + testAttribute: Subject, + op: Contains, + count: 1, + }, + // tests of custom headers db values + { + testString: "a one line header", + dbHeader: "oneliner", + }, + { + testString: "a two line header", + dbHeader: "twoliner", + }, + { + testString: "a three line header with lotsa space and tabs", + dbHeader: "threeliner", + }, + { + testString: "I have no space", + dbHeader: "nospace", + }, + { + testString: "too much space", + dbHeader: "withspace", + }, + // tests of custom db headers in a search + { + testString: "one line", + testAttribute: OtherHeader, + op: Contains, + customHeader: "oneliner", + count: 1, + }, + { + testString: "two line header", + testAttribute: OtherHeader, + op: Contains, + customHeader: "twoliner", + count: 1, + }, + { + testString: "three line header with lotsa", + testAttribute: OtherHeader, + op: Contains, + customHeader: "threeliner", + count: 1, + }, + { + testString: "I have no space", + testAttribute: OtherHeader, + op: Contains, + customHeader: "nospace", + count: 1, + }, + { + testString: "too much space", + testAttribute: OtherHeader, + op: Contains, + customHeader: "withspace", + count: 1, + }, +]; + +add_setup(async function () { + setupIMAPPump(); + + // don't use offline store + IMAPPump.inbox.clearFlag(Ci.nsMsgFolderFlags.Offline); + + // Load imap message. + IMAPPump.mailbox.addMessage( + new ImapMessage(specForFileName(gMessage), IMAPPump.mailbox.uidnext++, []) + ); + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, listener); + await listener.promise; + + Assert.equal(1, IMAPPump.inbox.getTotalMessages(false)); +}); + +// process each test from queue, calls itself upon completion of each search +add_task(async function testSearch() { + while (searchTests.length) { + let test = searchTests.shift(); + if (test.dbHeader) { + // Test of a custom db header. + let customValue = mailTestUtils + .firstMsgHdr(IMAPPump.inbox) + .getStringProperty(test.dbHeader); + Assert.equal(customValue, test.testString); + } else { + await new Promise(resolve => { + new TestSearch( + IMAPPump.inbox, + test.testString, + test.testAttribute, + test.op, + test.count, + resolve, + null, + test.customHeader ? test.customHeader : "X-Bugzilla-Watch-Reason" + ); + }); + } + } +}); + +// Cleanup at end +add_task(function endTest() { + teardownIMAPPump(); +}); + +/* + * helper function + */ + +// given a test file, return the file uri spec +function specForFileName(aFileName) { + let file = do_get_file("../../../data/" + aFileName); + let msgfileuri = Services.io.newFileURI(file).QueryInterface(Ci.nsIFileURL); + return msgfileuri.spec; +} diff --git a/comm/mailnews/imap/test/unit/test_imapStatusCloseDBs.js b/comm/mailnews/imap/test/unit/test_imapStatusCloseDBs.js new file mode 100644 index 0000000000..e81e5a120d --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_imapStatusCloseDBs.js @@ -0,0 +1,49 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests that checking folders for new mail with STATUS +// doesn't leave db's open. + +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +var gFolder1, gFolder2; + +add_setup(function () { + Services.prefs.setBoolPref("mail.check_all_imap_folders_for_new", true); + Services.prefs.setIntPref("mail.server.server1.max_cached_connections", 2); + + setupIMAPPump(); + + IMAPPump.daemon.createMailbox("folder 1", { subscribed: true }); + IMAPPump.daemon.createMailbox("folder 2", { subscribed: true }); + + IMAPPump.server.performTest("SUBSCRIBE"); + + let rootFolder = IMAPPump.incomingServer.rootFolder; + gFolder1 = rootFolder.getChildNamed("folder 1"); + gFolder2 = rootFolder.getChildNamed("folder 2"); + + IMAPPump.inbox.getNewMessages(null, null); + IMAPPump.server.performTest("STATUS"); + Assert.ok(IMAPPump.server.isTestFinished()); + // don't know if this will work, but we'll try. Wait for + // second status response + IMAPPump.server.performTest("STATUS"); + Assert.ok(IMAPPump.server.isTestFinished()); +}); + +add_task(function check() { + const gDbService = Cc["@mozilla.org/msgDatabase/msgDBService;1"].getService( + Ci.nsIMsgDBService + ); + Assert.ok(gDbService.cachedDBForFolder(IMAPPump.inbox) !== null); + Assert.ok(gDbService.cachedDBForFolder(gFolder1) === null); + Assert.ok(gDbService.cachedDBForFolder(gFolder2) === null); +}); + +add_task(function endTest() { + teardownIMAPPump(); +}); diff --git a/comm/mailnews/imap/test/unit/test_imapStoreMsgOffline.js b/comm/mailnews/imap/test/unit/test_imapStoreMsgOffline.js new file mode 100644 index 0000000000..4601d4b509 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_imapStoreMsgOffline.js @@ -0,0 +1,221 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * 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. + */ + +var { MessageGenerator } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageGenerator.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +var gMessageGenerator = new MessageGenerator(); + +var gMsgFile1 = do_get_file("../../../data/bugmail10"); +var gMsgId1 = "200806061706.m56H6RWT004933@mrapp54.mozilla.org"; +var gMsgFile2 = do_get_file("../../../data/image-attach-test"); +var gMsgId2 = "4A947F73.5030709@example.com"; +var gMsgFile3 = do_get_file("../../../data/external-attach-test"); +var gMsgId3 = "876TY.5030709@example.com"; + +var gFirstNewMsg; +var gFirstMsgSize; +var gImapInboxOfflineStoreSize; + +// Adds some messages directly to a mailbox (e.g. new mail). +function addMessagesToServer(messages, mailbox) { + // For every message we have, we need to convert it to a file:/// URI. + messages.forEach(function (message) { + let URI = Services.io + .newFileURI(message.file) + .QueryInterface(Ci.nsIFileURL); + // Create the ImapMessage and store it on the mailbox. + mailbox.addMessage(new ImapMessage(URI.spec, mailbox.uidnext++, [])); + }); +} + +add_setup(async function () { + // We aren't interested in downloading messages automatically. + Services.prefs.setBoolPref( + "mail.server.server1.autosync_offline_stores", + false + ); + Services.prefs.setBoolPref("mail.server.server1.offline_download", true); + + setupIMAPPump(); + + // 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. + IMAPPump.inbox.hierarchyDelimiter = "/"; + IMAPPump.inbox.verifiedAsOnlineFolder = true; + + addMessagesToServer( + [ + { file: gMsgFile1, messageId: gMsgId1 }, + { file: gMsgFile2, messageId: gMsgId2 }, + { file: gMsgFile3, messageId: gMsgId3 }, + ], + IMAPPump.daemon.getMailbox("INBOX") + ); + + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, listener); + await listener.promise; +}); + +var gIMAPService; + +add_task(async 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 = IMAPPump.inbox.msgDatabase; + let msg1 = db.getMsgHdrForMessageID(gMsgId1); + let listener = new PromiseTestUtils.PromiseUrlListener({ + OnStopRunningUrl: (aUrl, aExitCode) => { + Assert.equal(aExitCode, 0); + }, + }); + // We use the streamListener as a display consumer. + let streamListener = new PromiseTestUtils.PromiseStreamListener(); + gIMAPService.loadMessage( + IMAPPump.inbox.getUriForMsg(msg1), + streamListener, + null, + listener, + false + ); + await listener.promise; +}); + +add_task(async function select2ndMsg() { + let msg1 = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID(gMsgId1); + Assert.notEqual(msg1.flags & Ci.nsMsgMessageFlags.Offline, 0); + let db = IMAPPump.inbox.msgDatabase; + let msg2 = db.getMsgHdrForMessageID(gMsgId2); + let listener = new PromiseTestUtils.PromiseUrlListener({ + OnStopRunningUrl: (aUrl, aExitCode) => { + Assert.equal(aExitCode, 0); + }, + }); + // We use the streamListener as a display consumer. + let streamListener = new PromiseTestUtils.PromiseStreamListener(); + gIMAPService.loadMessage( + IMAPPump.inbox.getUriForMsg(msg2), + streamListener, + null, + listener, + false + ); + await listener.promise; +}); + +add_task(async function select3rdMsg() { + let msg2 = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID(gMsgId2); + Assert.notEqual(msg2.flags & Ci.nsMsgMessageFlags.Offline, 0); + let db = IMAPPump.inbox.msgDatabase; + let msg3 = db.getMsgHdrForMessageID(gMsgId3); + let listener = new PromiseTestUtils.PromiseUrlListener(); + // We use the streamListener as a display consumer. + let streamListener = new PromiseTestUtils.PromiseStreamListener(); + gIMAPService.loadMessage( + IMAPPump.inbox.getUriForMsg(msg3), + streamListener, + null, + listener, + false + ); + await listener.promise; +}); + +add_task( + { + // Can't turn this on because our fake server doesn't support body structure. + skip_if: () => true, + }, + function verify3rdMsg() { + let db = IMAPPump.inbox.msgDatabase; + let msg3 = db.getMsgHdrForMessageID(gMsgId3); + Assert.equal(msg3.flags & Ci.nsMsgMessageFlags.Offline, 0); + } +); + +add_task(async function addNewMsgs() { + let mbox = IMAPPump.daemon.getMailbox("INBOX"); + // Make a couple of messages. + let messages = []; + let bodyString = ""; + for (let i = 0; i < 100; i++) { + bodyString += + "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890\r\n"; + } + + gMessageGenerator = new MessageGenerator(); + messages = messages.concat( + gMessageGenerator.makeMessage({ + body: { body: bodyString, contentType: "text/plain" }, + }) + ); + + gFirstNewMsg = mbox.uidnext; + // Need to account for x-mozilla-status, status2, and envelope. + gFirstMsgSize = messages[0].toMessageString().length + 102; + + messages.forEach(function (message) { + let dataUri = Services.io.newURI( + "data:text/plain;base64," + btoa(message.toMessageString()) + ); + mbox.addMessage(new ImapMessage(dataUri.spec, mbox.uidnext++, [])); + }); + + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, listener); + await listener.promise; +}); +add_task(async function test_queuedOfflineDownload() { + // Make sure that streaming the same message and then trying to download + // it for offline use doesn't end up in it getting added to the offline + // store twice. + gImapInboxOfflineStoreSize = IMAPPump.inbox.filePath.fileSize + gFirstMsgSize; + let newMsgHdr = IMAPPump.inbox.GetMessageHeader(gFirstNewMsg); + let msgURI = newMsgHdr.folder.getUriForMsg(newMsgHdr); + let msgServ = MailServices.messageServiceFromURI(msgURI); + let listener = new PromiseTestUtils.PromiseStreamListener(); + msgServ.streamMessage(msgURI, listener, null, null, false, "", false); + await listener.promise; +}); +add_task(async function firstStreamFinished() { + // nsIMsgFolder.downloadMessagesForOffline does not take a listener, so + // we invoke nsIImapService.downloadMessagesForOffline directly + // with a listener. + let listener = new PromiseTestUtils.PromiseUrlListener(); + MailServices.imap.downloadMessagesForOffline( + gFirstNewMsg, + IMAPPump.inbox, + listener, + null + ); + await listener.promise; +}); +add_task(function checkOfflineStoreSize() { + Assert.ok(IMAPPump.inbox.filePath.fileSize <= gImapInboxOfflineStoreSize); +}); + +add_task(function endTest() { + teardownIMAPPump(); +}); diff --git a/comm/mailnews/imap/test/unit/test_imapUndo.js b/comm/mailnews/imap/test/unit/test_imapUndo.js new file mode 100644 index 0000000000..8db0ad6903 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_imapUndo.js @@ -0,0 +1,160 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests undoing of an imap delete to the trash. +// There are three main cases: +// 1. Normal undo +// 2. Undo after the source folder has been compacted. +// 2.1 Same, but the server doesn't support COPYUID (GMail case) +// +// Original Author: David Bienvenu <bienvenu@nventure.com> + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +var gRootFolder; +var gMessages = []; +var gMsgWindow; + +var gMsgFile1 = do_get_file("../../../data/bugmail10"); +var gMsgFile2 = do_get_file("../../../data/bugmail11"); +// var gMsgFile3 = do_get_file("../../../data/draft1"); +var gMsgFile4 = do_get_file("../../../data/bugmail7"); +var gMsgFile5 = do_get_file("../../../data/bugmail6"); + +// Copied straight from the example files +var gMsgId1 = "200806061706.m56H6RWT004933@mrapp54.mozilla.org"; +var gMsgId2 = "200804111417.m3BEHTk4030129@mrapp51.mozilla.org"; +// var gMsgId3 = "4849BF7B.2030800@example.com"; +var gMsgId4 = "bugmail7.m47LtAEf007542@mrapp51.mozilla.org"; +var gMsgId5 = "bugmail6.m47LtAEf007542@mrapp51.mozilla.org"; + +// Adds some messages directly to a mailbox (eg new mail) +function addMessagesToServer(messages, mailbox) { + // For every message we have, we need to convert it to a file:/// URI + messages.forEach(function (message) { + let URI = Services.io + .newFileURI(message.file) + .QueryInterface(Ci.nsIFileURL); + // Create the ImapMessage and store it on the mailbox. + mailbox.addMessage(new ImapMessage(URI.spec, mailbox.uidnext++, [])); + }); +} + +function alertListener() {} + +alertListener.prototype = { + reset() {}, + + onAlert(aMessage, aMsgWindow) { + throw new Error("got alert - TEST FAILED " + aMessage); + }, +}; + +add_setup(function () { + setupIMAPPump(); + + var listener1 = new alertListener(); + + MailServices.mailSession.addUserFeedbackListener(listener1); + + Services.prefs.setBoolPref( + "mail.server.server1.autosync_offline_stores", + false + ); + Services.prefs.setBoolPref("mail.server.server1.offline_download", false); + + gMsgWindow = Cc["@mozilla.org/messenger/msgwindow;1"].createInstance( + Ci.nsIMsgWindow + ); + + gRootFolder = IMAPPump.incomingServer.rootFolder; + // 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. + IMAPPump.inbox.hierarchyDelimiter = "/"; + IMAPPump.inbox.verifiedAsOnlineFolder = true; + + // Add a couple of messages to the INBOX + // this is synchronous, afaik + addMessagesToServer( + [ + { file: gMsgFile1, messageId: gMsgId1 }, + { file: gMsgFile4, messageId: gMsgId4 }, + { file: gMsgFile5, messageId: gMsgId5 }, + { file: gMsgFile2, messageId: gMsgId2 }, + ], + IMAPPump.mailbox + ); +}); + +add_task(async function updateFolder() { + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, listener); + await listener.promise; +}); + +add_task(async function deleteMessage() { + let msgToDelete = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID(gMsgId1); + let copyListener = new PromiseTestUtils.PromiseCopyListener(); + gMessages.push(msgToDelete); + IMAPPump.inbox.deleteMessages( + gMessages, + gMsgWindow, + false, + true, + copyListener, + true + ); + await copyListener.promise; +}); + +add_task(async function expunge() { + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.expunge(listener, gMsgWindow); + await listener.promise; + + // Ensure that the message has been surely deleted. + Assert.equal(IMAPPump.inbox.msgDatabase.dBFolderInfo.numMessages, 3); +}); + +add_task(async function undoDelete() { + gMsgWindow.transactionManager.undoTransaction(); + // after undo, we select the trash and then the inbox, so that we sync + // up with the server, and clear out the effects of having done the + // delete offline. + let listener = new PromiseTestUtils.PromiseUrlListener(); + let trash = gRootFolder.getChildNamed("Trash"); + trash + .QueryInterface(Ci.nsIMsgImapMailFolder) + .updateFolderWithListener(null, listener); + await listener.promise; +}); + +add_task(async function goBackToInbox() { + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(gMsgWindow, listener); + await listener.promise; +}); + +add_task(function verifyFolders() { + let msgRestored = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID(gMsgId1); + Assert.ok(msgRestored !== null); + Assert.equal(IMAPPump.inbox.msgDatabase.dBFolderInfo.numMessages, 4); +}); + +add_task(function endTest() { + // Cleanup, null out everything, close all cached connections and stop the + // server + gMessages = []; + gMsgWindow.closeWindow(); + gMsgWindow = null; + gRootFolder = null; + teardownIMAPPump(); +}); diff --git a/comm/mailnews/imap/test/unit/test_imapUrls.js b/comm/mailnews/imap/test/unit/test_imapUrls.js new file mode 100644 index 0000000000..e310705169 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_imapUrls.js @@ -0,0 +1,31 @@ +/* -*- Mode: JavaScript; 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 ***** */ + +// IMAP pump + +setupIMAPPump(); + +/* + * Test parsing of imap uri's with very large UID's. + */ + +function run_test() { + let imapS = Cc[ + "@mozilla.org/messenger/messageservice;1?type=imap" + ].getService(Ci.nsIMsgMessageService); + let uri = imapS.getUrlForUri( + "imap-message://user@localhost/INBOX#4294967168" + ); + Assert.equal( + uri.spec, + "imap://user@localhost:" + + IMAPPump.server.port + + "/fetch%3EUID%3E%5EINBOX%3E4294967168" + ); + teardownIMAPPump(); +} diff --git a/comm/mailnews/imap/test/unit/test_largeOfflineStore.js b/comm/mailnews/imap/test/unit/test_largeOfflineStore.js new file mode 100644 index 0000000000..6e0d2e9dd1 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_largeOfflineStore.js @@ -0,0 +1,141 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Test to ensure that downloadAllForOffline works correctly for large imap + * stores, i.e., over 4 GiB. + */ + +var { MessageGenerator, MessageScenarioFactory } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageGenerator.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +Services.prefs.setCharPref( + "mail.serverDefaultStoreContractID", + "@mozilla.org/msgstore/berkeleystore;1" +); + +var gOfflineStoreSize; + +add_setup(async function () { + setupIMAPPump(); + + // Figure out the name of the IMAP inbox + let inboxFile = IMAPPump.incomingServer.rootMsgFolder.filePath; + inboxFile.append("INBOX"); + if (!inboxFile.exists()) { + inboxFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("0644", 8)); + } + + let neededFreeSpace = 0x200000000; + // On Windows, check whether the drive is NTFS. If it is, mark the file as + // sparse. If it isn't, then bail out now, because in all probability it is + // FAT32, which doesn't support file sizes greater than 4 GB. + if ( + "@mozilla.org/windows-registry-key;1" in Cc && + mailTestUtils.get_file_system(inboxFile) != "NTFS" + ) { + throw new Error("On Windows, this test only works on NTFS volumes.\n"); + } + + let isFileSparse = mailTestUtils.mark_file_region_sparse( + inboxFile, + 0, + 0x10000000f + ); + let freeDiskSpace = inboxFile.diskSpaceAvailable; + Assert.ok( + isFileSparse && freeDiskSpace > neededFreeSpace, + "This test needs " + + mailTestUtils.toMiBString(neededFreeSpace) + + " free space to run." + ); +}); + +add_task(async function addOfflineMessages() { + // Create a couple test messages on the IMAP server. + let messages = []; + let messageGenerator = new MessageGenerator(); + let scenarioFactory = new MessageScenarioFactory(messageGenerator); + + messages = messages.concat(scenarioFactory.directReply(2)); + let dataUri = Services.io.newURI( + "data:text/plain;base64," + btoa(messages[0].toMessageString()) + ); + let imapMsg = new ImapMessage(dataUri.spec, IMAPPump.mailbox.uidnext++, []); + IMAPPump.mailbox.addMessage(imapMsg); + + dataUri = Services.io.newURI( + "data:text/plain;base64," + btoa(messages[1].toMessageString()) + ); + imapMsg = new ImapMessage(dataUri.spec, IMAPPump.mailbox.uidnext++, []); + IMAPPump.mailbox.addMessage(imapMsg); + + // Extend local IMAP inbox to over 4 GiB. + let outputStream = Cc["@mozilla.org/network/file-output-stream;1"] + .createInstance(Ci.nsIFileOutputStream) + .QueryInterface(Ci.nsISeekableStream); + // Open in write-only mode, no truncate. + outputStream.init(IMAPPump.inbox.filePath, 0x02, -1, 0); + // seek to 15 bytes past 4GB. + outputStream.seek(0, 0x10000000f); + // Write an empty "from" line. + outputStream.write("from\r\n", 6); + outputStream.close(); + + // Save initial file size. + gOfflineStoreSize = IMAPPump.inbox.filePath.fileSize; + dump( + "Offline store size (before 1st downloadAllForOffline()) = " + + gOfflineStoreSize + + "\n" + ); + + // Download for offline use, to append created messages to local IMAP inbox. + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.downloadAllForOffline(listener, null); + await listener.promise; +}); + +add_task(async function check_result() { + // Call downloadAllForOffline() a second time. + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.downloadAllForOffline(listener, null); + await listener.promise; + + // Make sure offline store grew (i.e., we were not writing over data). + let offlineStoreSize = IMAPPump.inbox.filePath.fileSize; + dump( + "Offline store size (after 2nd downloadAllForOffline()): " + + offlineStoreSize + + " Msg hdr offsets should be close to it.\n" + ); + Assert.ok(offlineStoreSize > gOfflineStoreSize); + + // Verify that the message headers have the offline flag set. + for (let header of IMAPPump.inbox.msgDatabase.enumerateMessages()) { + // Verify that each message has been downloaded and looks OK. + Assert.ok( + header instanceof Ci.nsIMsgDBHdr && + header.flags & Ci.nsMsgMessageFlags.Offline, + "Message downloaded for offline use" + ); + + // Make sure we don't fall over if we ask to read the message. + IMAPPump.inbox.getLocalMsgStream(header).close(); + } +}); + +add_task(function teardown() { + // Free up disk space - if you want to look at the file after running + // this test, comment out this line. + if (IMAPPump.inbox) { + IMAPPump.inbox.filePath.remove(false); + } + + teardownIMAPPump(); +}); diff --git a/comm/mailnews/imap/test/unit/test_listClosesDB.js b/comm/mailnews/imap/test/unit/test_listClosesDB.js new file mode 100644 index 0000000000..63ab5b70d4 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_listClosesDB.js @@ -0,0 +1,58 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests that listing folders on startup because we're not using +// subscription doesn't leave db's open. + +var { MessageGenerator } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageGenerator.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +var gSub3; + +add_setup(async function () { + setupIMAPPump(); + + IMAPPump.daemon.createMailbox("folder1", { subscribed: true }); + IMAPPump.daemon.createMailbox("folder1/sub1", "", { subscribed: true }); + IMAPPump.daemon.createMailbox("folder1/sub1/sub2", "", { subscribed: true }); + IMAPPump.daemon.createMailbox("folder1/sub1/sub2/sub3", "", { + subscribed: true, + }); + + IMAPPump.incomingServer.usingSubscription = false; + + let rootFolder = IMAPPump.incomingServer.rootFolder.QueryInterface( + Ci.nsIMsgImapMailFolder + ); + rootFolder.hierarchyDelimiter = "/"; + IMAPPump.inbox.hierarchyDelimiter = "/"; + let folder1 = rootFolder.addSubfolder("folder1"); + let sub1 = folder1.addSubfolder("sub1"); + let sub2 = sub1.addSubfolder("sub2"); + gSub3 = sub2.addSubfolder("sub3"); + IMAPPump.server.performTest("LIST"); + + await PromiseTestUtils.promiseDelay(1000); +}); + +add_task(async function updateInbox() { + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, listener); + await listener.promise; +}); + +add_task(function checkCachedDBForFolder() { + const gDbService = Cc["@mozilla.org/msgDatabase/msgDBService;1"].getService( + Ci.nsIMsgDBService + ); + Assert.equal(gDbService.cachedDBForFolder(gSub3), null); +}); + +add_task(function teardown() { + teardownIMAPPump(); +}); diff --git a/comm/mailnews/imap/test/unit/test_listSubscribed.js b/comm/mailnews/imap/test/unit/test_listSubscribed.js new file mode 100644 index 0000000000..0d536dd45c --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_listSubscribed.js @@ -0,0 +1,123 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** Test that listing subscribed mailboxes uses LIST (SUBSCRIBED) instead of LSUB + * for servers that have LIST-EXTENDED capability + */ +/* References: + * RFC 5258 - http://tools.ietf.org/html/rfc5258 + * Bug 495318 + * Bug 816028 + * http://bugzilla.zimbra.com/show_bug.cgi?id=78794 + */ + +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +add_setup(async function () { + // Zimbra is one of the servers that supports LIST-EXTENDED + // it also has a bug that causes a server crash in certain setups + setupIMAPPump("Zimbra"); + + Services.prefs.setBoolPref( + "mail.server.server1.autosync_offline_stores", + false + ); + + // Setup the mailboxes that will be used for this test. + IMAPPump.mailbox.subscribed = true; + IMAPPump.daemon.createMailbox("folder1", { + subscribed: true, + flags: ["\\Noselect"], + }); + IMAPPump.daemon.createMailbox("folder1/folder11", { + subscribed: true, + flags: ["\\Noinferiors"], + }); + IMAPPump.daemon.createMailbox("folder2", { + subscribed: true, + nonExistent: true, + }); + IMAPPump.daemon.createMailbox("folder3", {}); + + // select the inbox to force folder discovery, etc. + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, listener); + await listener.promise; +}); + +// tests that LIST (SUBSCRIBED) returns the proper response +add_task(async function testListSubscribed() { + // check that we have \Noselect and \Noinferiors flags - these would not have + // been returned if we had used LSUB instead of LIST(SUBSCRIBED) + let rootFolder = IMAPPump.incomingServer.rootFolder; + let folder1 = rootFolder.getChildNamed("folder1"); + Assert.ok(folder1.getFlag(Ci.nsMsgFolderFlags.ImapNoselect)); + Assert.ok(!folder1.getFlag(Ci.nsMsgFolderFlags.ImapNoinferiors)); + + // make sure the above test was not a fluke + let folder11 = folder1.getChildNamed("folder11"); + Assert.ok(!folder11.getFlag(Ci.nsMsgFolderFlags.ImapNoselect)); + Assert.ok(folder11.getFlag(Ci.nsMsgFolderFlags.ImapNoinferiors)); + + // test that \NonExistent implies \Noselect + rootFolder.getChildNamed("folder2"); + Assert.ok(folder1.getFlag(Ci.nsMsgFolderFlags.ImapNoselect)); + + // should not get a folder3 since it is not subscribed + let folder3; + try { + folder3 = rootFolder.getChildNamed("folder3"); + } catch (ex) {} + // do_check_false(folder1.getFlag(Ci.nsMsgFolderFlags.Subscribed)); + Assert.equal(null, folder3); +}); + +add_task(async function testZimbraServerVersions() { + if (Services.prefs.getBoolPref("mailnews.imap.jsmodule", false)) { + return; + } + + // older versions of Zimbra can crash if we send LIST (SUBSCRIBED) so we want + // to make sure that we are checking for versions + + let testValues = [ + { version: "6.3.1_GA_2790", expectedResult: false }, + { version: "7.2.2_GA_2790", expectedResult: false }, + { version: "7.2.3_GA_2790", expectedResult: true }, + { version: "8.0.2_GA_2790", expectedResult: false }, + { version: "8.0.3_GA_2790", expectedResult: true }, + { version: "9.0.0_GA_2790", expectedResult: true }, + ]; + + for (let i = 0; i < testValues.length; i++) { + IMAPPump.daemon.idResponse = + '("NAME" "Zimbra" ' + + '"VERSION" "' + + testValues[i].version + + '" ' + + '"RELEASE" "20120815212257" ' + + '"USER" "user@domain.com" ' + + '"SERVER" "14b63305-d002-4f1b-bcd9-23d402d4ef40")'; + IMAPPump.incomingServer.closeCachedConnections(); + IMAPPump.incomingServer.performExpand(null); + // select inbox is just to wait on performExpand since performExpand does not have listener + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, listener); + await listener.promise; + // if we send LSUB instead of LIST(SUBSCRIBED), then we should not have \NoSelect flag + let rootFolder = IMAPPump.incomingServer.rootFolder; + let folder1 = rootFolder.getChildNamed("folder1"); + Assert.equal( + folder1.getFlag(Ci.nsMsgFolderFlags.ImapNoselect), + testValues[i].expectedResult + ); + } +}); + +// Cleanup at end +add_task(function endTest() { + teardownIMAPPump(); +}); diff --git a/comm/mailnews/imap/test/unit/test_localToImapFilter.js b/comm/mailnews/imap/test/unit/test_localToImapFilter.js new file mode 100644 index 0000000000..b0a28aedda --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_localToImapFilter.js @@ -0,0 +1,159 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * This file tests copies of multiple messages using filters + * from incoming POP3, with filter actions copying and moving + * messages to IMAP folders. This test is adapted from + * test_imapFolderCopy.js + * + * Original author: Kent James <kent@caspia.com> + */ + +/** + * NOTE: + * There's a problem with this test in chaos mode (mach xpcshell-test --verify) + * with the filter applying. + * It's either a problem with the POP3Pump implementation (testing infrastructure failure) + * or a problem with the copy filter. + */ + +/* import-globals-from ../../../test/resources/POP3pump.js */ +load("../../../resources/POP3pump.js"); + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +var gEmptyLocal1, gEmptyLocal2; +var gFiles = ["../../../data/bugmail1", "../../../data/draft1"]; + +add_setup(async function () { + setupIMAPPump(); + let emptyFolder1Listener = PromiseTestUtils.promiseFolderAdded("empty 1"); + gEmptyLocal1 = localAccountUtils.rootFolder.createLocalSubfolder("empty 1"); + await emptyFolder1Listener; + let emptyFolder2Listener = PromiseTestUtils.promiseFolderAdded("empty 2"); + gEmptyLocal2 = localAccountUtils.rootFolder.createLocalSubfolder("empty 2"); + await emptyFolder2Listener; + + // 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. + IMAPPump.inbox.hierarchyDelimiter = "/"; + IMAPPump.inbox.verifiedAsOnlineFolder = true; +}); + +add_task(async function copyFolder1() { + let copyListener = new PromiseTestUtils.PromiseCopyListener(); + MailServices.copy.copyFolder( + gEmptyLocal1, + IMAPPump.inbox, + false, + copyListener, + null + ); + await copyListener.promise; +}); + +add_task(async function updateTrash() { + let trashFolder = IMAPPump.incomingServer.rootFolder + .getChildNamed("Trash") + .QueryInterface(Ci.nsIMsgImapMailFolder); + let listener = new PromiseTestUtils.PromiseUrlListener(); + // hack to force uid validity to get initialized for trash. + trashFolder.updateFolderWithListener(null, listener); + await listener.promise; +}); + +add_task(async function copyFolder2() { + let copyListener = new PromiseTestUtils.PromiseCopyListener(); + MailServices.copy.copyFolder( + gEmptyLocal2, + IMAPPump.inbox, + false, + copyListener, + null + ); + await copyListener.promise; +}); + +add_task(async function getLocalMessages() { + // setup copy then move mail filters on the inbox + let filterList = gPOP3Pump.fakeServer.getFilterList(null); + let filter = filterList.createFilter("copyThenMoveAll"); + let searchTerm = filter.createTerm(); + searchTerm.matchAll = true; + filter.appendTerm(searchTerm); + let copyAction = filter.createAction(); + copyAction.type = Ci.nsMsgFilterAction.CopyToFolder; + copyAction.targetFolderUri = IMAPPump.inbox.getChildNamed("empty 1").URI; + filter.appendAction(copyAction); + let moveAction = filter.createAction(); + moveAction.type = Ci.nsMsgFilterAction.MoveToFolder; + moveAction.targetFolderUri = IMAPPump.inbox.getChildNamed("empty 2").URI; + filter.appendAction(moveAction); + filter.enabled = true; + filterList.insertFilterAt(0, filter); + let resolveOnDone; + let promiseOnDone = new Promise(resolve => { + resolveOnDone = resolve; + }); + gPOP3Pump.files = gFiles; + gPOP3Pump.onDone = resolveOnDone; + gPOP3Pump.run(); + + await promiseOnDone; +}); + +add_task(async function test_update1_copyFilter() { + let listener = new PromiseTestUtils.PromiseUrlListener(); + let folder1 = IMAPPump.inbox + .getChildNamed("empty 1") + .QueryInterface(Ci.nsIMsgImapMailFolder); + folder1.updateFolderWithListener(null, listener); + await listener.promise; + Assert.ok(folder1 !== null); + Assert.equal( + folderCount(folder1), + 2, + "the two filtered messages should be in empty 1" + ); +}); + +add_task(async function test_update2_moveFilter() { + let listener = new PromiseTestUtils.PromiseUrlListener(); + let folder2 = IMAPPump.inbox + .getChildNamed("empty 2") + .QueryInterface(Ci.nsIMsgImapMailFolder); + folder2.updateFolderWithListener(null, listener); + await listener.promise; + Assert.ok(folder2 !== null); + Assert.equal( + folderCount(folder2), + 2, + "the two filtered messages should be in empty 2" + ); +}); + +add_task(async function verifyLocalFolder() { + // the local inbox folder should now be empty, since the second + // operation was a move + Assert.equal(folderCount(localAccountUtils.inboxFolder), 0); +}); + +add_task(function endTest() { + gEmptyLocal1 = null; + gEmptyLocal2 = null; + gPOP3Pump = null; + teardownIMAPPump(); +}); + +function folderCount(folder) { + return [...folder.msgDatabase.enumerateMessages()].length; +} diff --git a/comm/mailnews/imap/test/unit/test_localToImapFilterQuarantine.js b/comm/mailnews/imap/test/unit/test_localToImapFilterQuarantine.js new file mode 100644 index 0000000000..130a03e403 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_localToImapFilterQuarantine.js @@ -0,0 +1,121 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * This file tests copies of multiple messages using filters + * from incoming POP3, with filter actions copying and moving + * messages to an IMAP folder, when the POP3 message uses + * quarantining to help antivirus software. See bug 387361. + * + */ + +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +Services.prefs.setCharPref( + "mail.serverDefaultStoreContractID", + "@mozilla.org/msgstore/berkeleystore;1" +); + +/* import-globals-from ../../../test/resources/POP3pump.js */ +load("../../../resources/POP3pump.js"); + +var gSubfolder; + +add_setup(function () { + setupIMAPPump(); + // quarantine messages + Services.prefs.setBoolPref("mailnews.downloadToTempFile", true); +}); + +add_task(async function createSubfolder() { + let folderAddedListener = PromiseTestUtils.promiseFolderAdded("subfolder"); + IMAPPump.incomingServer.rootFolder.createSubfolder("subfolder", null); + await folderAddedListener; + gSubfolder = IMAPPump.incomingServer.rootFolder.getChildNamed("subfolder"); + Assert.ok(gSubfolder instanceof Ci.nsIMsgImapMailFolder); + let listener = new PromiseTestUtils.PromiseUrlListener(); + gSubfolder.updateFolderWithListener(null, listener); + await listener.promise; +}); + +add_task(async function getLocalMessages() { + // setup copy then move mail filters on the inbox + let filterList = gPOP3Pump.fakeServer.getFilterList(null); + let filter = filterList.createFilter("copyThenMoveAll"); + let searchTerm = filter.createTerm(); + searchTerm.matchAll = true; + filter.appendTerm(searchTerm); + let copyAction = filter.createAction(); + copyAction.type = Ci.nsMsgFilterAction.CopyToFolder; + copyAction.targetFolderUri = gSubfolder.URI; + filter.appendAction(copyAction); + filter.enabled = true; + filterList.insertFilterAt(0, filter); + + let resolveDone; + let promise = new Promise(resolve => { + resolveDone = resolve; + }); + gPOP3Pump.files = ["../../../data/bugmail1"]; + gPOP3Pump.onDone = resolveDone; + gPOP3Pump.run(); + await promise; +}); + +add_task(async function updateSubfolderAndTest() { + let listener = new PromiseTestUtils.PromiseUrlListener(); + let folderLoaded = PromiseTestUtils.promiseFolderEvent( + gSubfolder, + "FolderLoaded" + ); + gSubfolder.updateFolderWithListener(null, listener); + await listener.promise; + await folderLoaded; + + Assert.equal(folderCount(gSubfolder), 1); + Assert.equal(folderCount(localAccountUtils.inboxFolder), 1); +}); + +add_task(async function get2Messages() { + let resolveDone; + let promise = new Promise(resolve => { + resolveDone = resolve; + }); + gPOP3Pump.files = ["../../../data/bugmail10", "../../../data/draft1"]; + gPOP3Pump.onDone = resolveDone; + gPOP3Pump.run(); + await promise; +}); + +add_task(async function updateSubfolderAndTest2() { + let listener = new PromiseTestUtils.PromiseUrlListener(); + let folderLoaded = PromiseTestUtils.promiseFolderEvent( + gSubfolder, + "FolderLoaded" + ); + gSubfolder.updateFolderWithListener(null, listener); + await listener.promise; + await folderLoaded; + Assert.equal(folderCount(gSubfolder), 3); + Assert.equal(folderCount(localAccountUtils.inboxFolder), 3); +}); + +add_task(function endTest() { + // Cleanup, null out everything, close all cached connections and stop the + // server + gPOP3Pump = null; + teardownIMAPPump(); +}); + +// helper functions + +// count of messages in a folder, using the database +function folderCount(folder) { + return [...folder.msgDatabase.enumerateMessages()].length; +} diff --git a/comm/mailnews/imap/test/unit/test_lsub.js b/comm/mailnews/imap/test/unit/test_lsub.js new file mode 100644 index 0000000000..1d7f479a48 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_lsub.js @@ -0,0 +1,76 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Test that listing subscribed mailboxes uses LIST (SUBSCRIBED) instead of LSUB +// for servers that have LIST-EXTENDED capability +// see: bug 495318 +// see: RFC 5258 - http://tools.ietf.org/html/rfc5258 + +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +add_setup(function () { + setupIMAPPump(); + + Services.prefs.setBoolPref( + "mail.server.server1.autosync_offline_stores", + false + ); +}); + +// Setup the mailboxes that will be used for this test. +add_setup(async function () { + IMAPPump.mailbox.subscribed = true; + IMAPPump.daemon.createMailbox("folder1", { + subscribed: true, + flags: ["\\Noselect"], + }); + IMAPPump.daemon.createMailbox("folder1/folder11", { + subscribed: true, + flags: ["\\Noinferiors"], + }); + IMAPPump.daemon.createMailbox("folder2", { + subscribed: true, + nonExistent: true, + }); + IMAPPump.daemon.createMailbox("folder3", {}); + + // select the inbox to force folder discovery, etc. + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, listener); + await listener.promise; +}); + +// Tests that LSUB returns the proper response. +add_task(function testLsub() { + // Check that we have \Noselect and \Noinferiors flags - these would not have + // been returned if we had used LSUB instead of LIST(SUBSCRIBED). + let rootFolder = IMAPPump.incomingServer.rootFolder; + let folder1 = rootFolder.getChildNamed("folder1"); + Assert.ok(folder1.getFlag(Ci.nsMsgFolderFlags.ImapNoselect)); + Assert.ok(!folder1.getFlag(Ci.nsMsgFolderFlags.ImapNoinferiors)); + + // Make sure the above test was not a fluke. + let folder11 = folder1.getChildNamed("folder11"); + Assert.ok(!folder11.getFlag(Ci.nsMsgFolderFlags.ImapNoselect)); + Assert.ok(folder11.getFlag(Ci.nsMsgFolderFlags.ImapNoinferiors)); + + // Test that \NonExistent implies \Noselect. + rootFolder.getChildNamed("folder2"); + Assert.ok(folder1.getFlag(Ci.nsMsgFolderFlags.ImapNoselect)); + + // Should not get a folder3 since it is not subscribed. + let folder3; + try { + folder3 = rootFolder.getChildNamed("folder3"); + } catch (ex) {} + Assert.equal(false, folder1.getFlag(Ci.nsMsgFolderFlags.Subscribed)); + Assert.equal(null, folder3); +}); + +// Cleanup at end. +add_task(function endTest() { + teardownIMAPPump(); +}); diff --git a/comm/mailnews/imap/test/unit/test_mailboxes.js b/comm/mailnews/imap/test/unit/test_mailboxes.js new file mode 100644 index 0000000000..0f13f6b328 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_mailboxes.js @@ -0,0 +1,80 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tests basic mailbox handling of IMAP, like discovery, rename and empty folder. + */ + +var { MessageGenerator } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageGenerator.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +// The following folder names are not pure ASCII and will be MUTF-7 encoded. +const folderName1 = "I18N box\u00E1"; // I18N boxá +const folderName2 = "test \u00E4"; // test ä + +add_setup(async function () { + setupIMAPPump(); + + IMAPPump.daemon.createMailbox(folderName1, { subscribed: true }); + IMAPPump.daemon.createMailbox("Unsubscribed box"); + // Create an all upper case trash folder name to make sure + // we handle special folder names case-insensitively. + IMAPPump.daemon.createMailbox("TRASH", { subscribed: true }); + + // Get the server list... + IMAPPump.server.performTest("LIST"); + + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, listener); + await listener.promise; +}); + +add_task(async function checkDiscovery() { + let rootFolder = IMAPPump.incomingServer.rootFolder; + // Check that we've subscribed to the boxes returned by LSUB. We also get + // checking of proper i18n in mailboxes for free here. + Assert.ok(rootFolder.containsChildNamed("Inbox")); + Assert.ok(rootFolder.containsChildNamed("TRASH")); + // Make sure we haven't created an extra "Trash" folder. + let trashes = rootFolder.getFoldersWithFlags(Ci.nsMsgFolderFlags.Trash); + Assert.equal(trashes.length, 1); + Assert.equal(rootFolder.numSubFolders, 3); + Assert.ok(rootFolder.containsChildNamed(folderName1)); + // This is not a subscribed box, so we shouldn't be subscribing to it. + Assert.ok(!rootFolder.containsChildNamed("Unsubscribed box")); + + let i18nChild = rootFolder.getChildNamed(folderName1); + + let listener = new PromiseTestUtils.PromiseUrlListener(); + MailServices.imap.renameLeaf(i18nChild, folderName2, listener, null); + await listener.promise; +}); + +add_task(async function checkRename() { + let rootFolder = IMAPPump.incomingServer.rootFolder; + Assert.ok(rootFolder.containsChildNamed(folderName2)); + let newChild = rootFolder + .getChildNamed(folderName2) + .QueryInterface(Ci.nsIMsgImapMailFolder); + let listener = new PromiseTestUtils.PromiseUrlListener(); + newChild.updateFolderWithListener(null, listener); + await listener.promise; +}); + +add_task(function checkEmptyFolder() { + try { + let serverSink = IMAPPump.server.QueryInterface(Ci.nsIImapServerSink); + serverSink.possibleImapMailbox("/", "/", 0); + } catch (ex) { + // We expect this to fail, but not crash or assert. + } +}); + +add_task(function endTest() { + teardownIMAPPump(); +}); diff --git a/comm/mailnews/imap/test/unit/test_nsIMsgFolderListenerIMAP.js b/comm/mailnews/imap/test/unit/test_nsIMsgFolderListenerIMAP.js new file mode 100644 index 0000000000..7b733ab9e4 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_nsIMsgFolderListenerIMAP.js @@ -0,0 +1,363 @@ +/* -*- Mode: Javascript; 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 ***** */ + +/* + * Test suite for nsIMsgFolderListener events due to IMAP operations + * + * Currently tested + * - Adding new folders + * - Copying messages from files to mailboxes + * - Adding new messages directly to mailboxes + * + * NOTE (See Bug 1632022): + * Running this test by itself... + * + * $ ./mach xpcshell-test comm/mailnews/imap/test/unit/test_nsIMsgFolderListenerIMAP.js + * ...will fail. + * + * This is because all the IMAP tests run twice - once with mbox storage and + * once with maildir storage. For this test, the two parallel instances + * interact badly. + * + */ + +/* import-globals-from ../../../test/resources/msgFolderListenerSetup.js */ +load("../../../resources/msgFolderListenerSetup.js"); + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +// Globals +var gRootFolder; +var gIMAPInbox, gIMAPFolder2, gIMAPFolder3; +var gIMAPDaemon, gServer, gIMAPIncomingServer; +var gMsgFile1 = do_get_file("../../../data/bugmail10"); +var gMsgFile2 = do_get_file("../../../data/bugmail11"); +var gMsgFile3 = do_get_file("../../../data/draft1"); +var gMsgFile4 = do_get_file("../../../data/bugmail7"); +var gMsgFile5 = do_get_file("../../../data/bugmail6"); + +// Copied straight from the example files +var gMsgId1 = "200806061706.m56H6RWT004933@mrapp54.mozilla.org"; +var gMsgId2 = "200804111417.m3BEHTk4030129@mrapp51.mozilla.org"; +var gMsgId3 = "4849BF7B.2030800@example.com"; +var gMsgId4 = "bugmail7.m47LtAEf007542@mrapp51.mozilla.org"; +var gMsgId5 = "bugmail6.m47LtAEf007542@mrapp51.mozilla.org"; +var gMsgWindow = Cc["@mozilla.org/messenger/msgwindow;1"].createInstance( + Ci.nsIMsgWindow +); + +function addFolder(parent, folderName, storeIn) { + gExpectedEvents = [ + [MailServices.mfn.folderAdded, parent, folderName, storeIn], + ]; + // No copy listener notification for this + gCurrStatus |= kStatus.onStopCopyDone; + parent.createSubfolder(folderName, null); + gCurrStatus |= kStatus.functionCallDone; + gServer.performTest("LIST"); + if (gCurrStatus == kStatus.everythingDone) { + resetStatusAndProceed(); + } +} + +function copyFileMessage(file, messageId, destFolder) { + copyListener.mFolderStoredIn = destFolder; + + // This *needs* to be a draft (fourth parameter), as for non-UIDPLUS servers, + // nsImapProtocol::UploadMessageFromFile is hardcoded not to send a copy + // listener notification. The same function also asks for the message id from + // the copy listener, without which it will *not* send the notification. + + // ...but wait, nsImapProtocol.cpp requires SEARCH afterwards to retrieve the + // message header, and fakeserver doesn't implement it yet. So get it to fail + // earlier by *not* sending the message id. + // copyListener.mMessageId = messageId; + + // Instead store the message id in gExpectedEvents, so we can match that up + gExpectedEvents = [ + [MailServices.mfn.msgAdded, { expectedMessageId: messageId }], + [MailServices.mfn.msgsClassified, [messageId], false, false], + ]; + destFolder.updateFolder(null); + MailServices.copy.copyFileMessage( + file, + destFolder, + null, + true, + 0, + "", + copyListener, + null + ); + gCurrStatus |= kStatus.functionCallDone; + gServer.performTest("APPEND"); + // Allow some time for the append operation to complete, so update folder + // every second + gFolderBeingUpdated = destFolder; + doUpdateFolder(gTest); +} + +var gFolderBeingUpdated = null; +function doUpdateFolder(test) { + // In case we've moved on to the next test, exit + if (gTest > test) { + return; + } + + gFolderBeingUpdated.updateFolder(null); + + if (gCurrStatus == kStatus.everythingDone) { + resetStatusAndProceed(); + } else { + do_timeout(1000, function () { + doUpdateFolder(test); + }); + } +} + +// Adds some messages directly to a mailbox (eg new mail) +function addMessagesToServer(messages, mailbox, localFolder) { + // For every message we have, we need to convert it to a file:/// URI + messages.forEach(function (message) { + let URI = Services.io + .newFileURI(message.file) + .QueryInterface(Ci.nsIFileURL); + // Create the ImapMessage and store it on the mailbox. + mailbox.addMessage(new ImapMessage(URI.spec, mailbox.uidnext++, [])); + // We can't get the headers again, so just pass on the message id + gExpectedEvents.push([ + MailServices.mfn.msgAdded, + { expectedMessageId: message.messageId }, + ]); + }); + gExpectedEvents.push([ + MailServices.mfn.msgsClassified, + messages.map(hdr => hdr.messageId), + false, + false, + ]); + + // No copy listener notification for this + gCurrStatus |= kStatus.functionCallDone | kStatus.onStopCopyDone; + + gFolderBeingUpdated = localFolder; + doUpdateFolder(gTest); +} + +function copyMessages(messages, isMove, srcFolder, destFolder) { + gExpectedEvents = [ + [ + MailServices.mfn.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([ + MailServices.mfn.msgKeyChanged, + { expectedMessageId: message.messageId }, + ]); + gExpectedEvents.push([ + MailServices.mfn.msgAdded, + { expectedMessageId: message.messageId }, + ]); + }); + gExpectedEvents.push([ + MailServices.mfn.msgsClassified, + messages.map(hdr => hdr.messageId), + false, + false, + ]); + MailServices.copy.copyMessages( + srcFolder, + messages, + destFolder, + isMove, + copyListener, + gMsgWindow, + true + ); + gCurrStatus |= kStatus.functionCallDone; + + gServer.performTest("COPY"); + + gFolderBeingUpdated = destFolder; + doUpdateFolder(gTest); + if (gCurrStatus == kStatus.everythingDone) { + resetStatusAndProceed(); + } +} + +var gTestArray = [ + // Adding folders + // Create another folder to move and copy messages around, and force initialization. + function testAddFolder1() { + addFolder(gRootFolder, "folder2", function (folder) { + gIMAPFolder2 = folder; + }); + }, + function testAddFolder2() { + addFolder(gRootFolder, "folder3", function (folder) { + gIMAPFolder3 = folder; + }); + }, + + // Adding messages to folders + function testCopyFileMessage1() { + // Make sure the offline flag is not set for any of the folders + [gIMAPInbox, gIMAPFolder2, gIMAPFolder3].forEach(function (folder) { + folder.clearFlag(Ci.nsMsgFolderFlags.Offline); + }); + copyFileMessage(gMsgFile1, gMsgId1, gIMAPInbox); + }, + function testCopyFileMessage2() { + copyFileMessage(gMsgFile2, gMsgId2, gIMAPInbox); + }, + + // Add message straight to the server, so that we get a message added + // notification on the next folder update + function testNewMessageArrival1() { + addMessagesToServer( + [{ file: gMsgFile3, messageId: gMsgId3 }], + gIMAPDaemon.getMailbox("INBOX"), + gIMAPInbox + ); + }, + + // 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 run_test() { + // This is before any of the actual tests, so... + gTest = 0; + + gIMAPDaemon = new ImapDaemon(); + gServer = makeServer(gIMAPDaemon, ""); + + gIMAPIncomingServer = createLocalIMAPServer(gServer.port); + + // Also make sure a local folders server is created, as that's what is used + // for sent items + localAccountUtils.loadLocalMailAccount(); + + // We need an identity so that updateFolder doesn't fail + let localAccount = MailServices.accounts.createAccount(); + let identity = MailServices.accounts.createIdentity(); + localAccount.addIdentity(identity); + localAccount.defaultIdentity = identity; + localAccount.incomingServer = localAccountUtils.incomingServer; + + // Let's also have another account, using the same identity + let imapAccount = MailServices.accounts.createAccount(); + imapAccount.addIdentity(identity); + imapAccount.defaultIdentity = identity; + imapAccount.incomingServer = gIMAPIncomingServer; + MailServices.accounts.defaultAccount = imapAccount; + + // The server doesn't support more than one connection + Services.prefs.setIntPref("mail.server.server1.max_cached_connections", 1); + // Make sure no biff notifications happen + Services.prefs.setBoolPref("mail.biff.play_sound", false); + Services.prefs.setBoolPref("mail.biff.show_alert", false); + Services.prefs.setBoolPref("mail.biff.show_tray_icon", false); + Services.prefs.setBoolPref("mail.biff.animate_dock_icon", false); + // We aren't interested in downloading messages automatically + Services.prefs.setBoolPref("mail.server.server1.download_on_biff", false); + + // Add a listener so that we can check all folder events from this point. + MailServices.mfn.addListener(gMFListener, allTestedEvents); + + // Get the server list... + gIMAPIncomingServer.performExpand(null); + + // We get these notifications on initial discovery + gRootFolder = gIMAPIncomingServer.rootFolder; + gIMAPInbox = gRootFolder.getChildNamed("Inbox"); + gExpectedEvents = [ + [MailServices.mfn.folderAdded, gRootFolder, "Trash", function (folder) {}], + ]; + gCurrStatus |= kStatus.onStopCopyDone | kStatus.functionCallDone; + + gServer.performTest("SUBSCRIBE"); + + // "Master" do_test_pending(), paired with a do_test_finished() at the end of + // all the operations. + do_test_pending(); +} + +function doTest(test) { + // eslint-disable-line no-unused-vars + if (test <= gTestArray.length) { + let testFn = gTestArray[test - 1]; + + dump(`Doing test ${test} (${testFn.name})\n`); + + // Set a limit of ten seconds; if the notifications haven't arrived by then there's a problem. + do_timeout(10000, function () { + if (gTest == test) { + do_throw( + "Notifications not received in 10000 ms for operation " + + testFn.name + + ", current status is " + + gCurrStatus + ); + } + }); + testFn(); + } else { + MailServices.mfn.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; + do_timeout(1000, endTest); + } +} + +function endTest() { + gIMAPIncomingServer.closeCachedConnections(); + gServer.performTest(); + gServer.stop(); + let thread = gThreadManager.currentThread; + while (thread.hasPendingEvents()) { + thread.processNextEvent(true); + } + + do_test_finished(); // for the one in run_test() +} diff --git a/comm/mailnews/imap/test/unit/test_offlineCopy.js b/comm/mailnews/imap/test/unit/test_offlineCopy.js new file mode 100644 index 0000000000..0bbf80e352 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_offlineCopy.js @@ -0,0 +1,271 @@ +/* -*- Mode: JavaScript; 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 ***** */ + +/** + * This test checks pseudo-offline message copies (which is triggered + * by allowUndo == true in copyMessages). + */ + +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +/* import-globals-from ../../../test/resources/logHelper.js */ +load("../../../resources/logHelper.js"); + +var gMsgFile1 = do_get_file("../../../data/bugmail10"); +var gMsgId1 = "200806061706.m56H6RWT004933@mrapp54.mozilla.org"; +var gMsgFile2 = do_get_file("../../../data/image-attach-test"); +var gMsgId2 = "4A947F73.5030709@example.com"; +var gMsgFile3 = do_get_file("../../../data/SpamAssassinYes"); +var gMsg3Id = "bugmail7.m47LtAEf007543@mrapp51.mozilla.org"; +var gMsgFile4 = do_get_file("../../../data/bug460636"); +var gMsg4Id = "foo.12345@example"; + +var gFolder1; + +// Adds some messages directly to a mailbox (eg new mail) +function addMessagesToServer(messages, mailbox) { + // For every message we have, we need to convert it to a file:/// URI + messages.forEach(function (message) { + let URI = Services.io + .newFileURI(message.file) + .QueryInterface(Ci.nsIFileURL); + // Create the ImapMessage and store it on the mailbox. + mailbox.addMessage(new ImapMessage(URI.spec, mailbox.uidnext++, [])); + }); +} + +var tests = [ + async function setup() { + // Turn off autosync_offline_stores because + // fetching messages is invoked after copying the messages. + // (i.e. The fetching process will be invoked after OnStopCopy) + // It will cause crash with an assertion + // (ASSERTION: tried to add duplicate listener: 'index == -1') on teardown. + Services.prefs.setBoolPref( + "mail.server.default.autosync_offline_stores", + false + ); + + setupIMAPPump(); + + let promiseFolderAdded = PromiseTestUtils.promiseFolderAdded("folder 1"); + IMAPPump.incomingServer.rootFolder.createSubfolder("folder 1", null); + await promiseFolderAdded; + + gFolder1 = IMAPPump.incomingServer.rootFolder.getChildNamed("folder 1"); + Assert.ok(gFolder1 instanceof Ci.nsIMsgFolder); + + // 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. + IMAPPump.inbox.hierarchyDelimiter = "/"; + IMAPPump.inbox.verifiedAsOnlineFolder = true; + + // Add messages to the INBOX + // this is synchronous, afaik + addMessagesToServer( + [ + { file: gMsgFile1, messageId: gMsgId1 }, + { file: gMsgFile2, messageId: gMsgId2 }, + ], + IMAPPump.daemon.getMailbox("INBOX") + ); + }, + async function updateFolder() { + let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, promiseUrlListener); + await promiseUrlListener.promise; + }, + async function downloadAllForOffline() { + let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.downloadAllForOffline(promiseUrlListener, null); + await promiseUrlListener.promise; + }, + async function copyMessagesToInbox() { + let promiseCopyListener = new PromiseTestUtils.PromiseCopyListener(); + MailServices.copy.copyFileMessage( + gMsgFile3, + IMAPPump.inbox, + null, + false, + 0, + "", + promiseCopyListener, + null + ); + await promiseCopyListener.promise; + + promiseCopyListener = new PromiseTestUtils.PromiseCopyListener(); + MailServices.copy.copyFileMessage( + gMsgFile4, + IMAPPump.inbox, + null, + false, + 0, + "", + promiseCopyListener, + null + ); + await promiseCopyListener.promise; + + let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, promiseUrlListener); + await promiseUrlListener.promise; + + let db = IMAPPump.inbox.msgDatabase; + + // test the headers in the inbox + let count = 0; + for (let message of db.enumerateMessages()) { + count++; + message instanceof Ci.nsIMsgDBHdr; + dump( + "message <" + + message.subject + + "> storeToken: <" + + message.getStringProperty("storeToken") + + "> offset: <" + + message.messageOffset + + "> id: <" + + message.messageId + + ">\n" + ); + // This fails for file copies in bug 790912. Without this, messages that + // are copied are not visible in pre-pluggableStores versions of TB (pre TB 12) + if (IMAPPump.inbox.msgStore.storeType == "mbox") { + Assert.equal( + message.messageOffset, + parseInt(message.getStringProperty("storeToken")) + ); + } + } + Assert.equal(count, 4); + }, + function copyMessagesToSubfolder() { + // a message created from IMAP download + let db = IMAPPump.inbox.msgDatabase; + let msg1 = db.getMsgHdrForMessageID(gMsgId1); + // this is sync, I believe? + MailServices.copy.copyMessages( + IMAPPump.inbox, + [msg1], + gFolder1, + false, + null, + null, + true + ); + + // two messages originally created from file copies (like in Send) + let msg3 = db.getMsgHdrForMessageID(gMsg3Id); + Assert.ok(msg3 instanceof Ci.nsIMsgDBHdr); + MailServices.copy.copyMessages( + IMAPPump.inbox, + [msg3], + gFolder1, + false, + null, + null, + true + ); + + let msg4 = db.getMsgHdrForMessageID(gMsg4Id); + Assert.ok(msg4 instanceof Ci.nsIMsgDBHdr); + + // because bug 790912 created messages with correct storeToken but messageOffset=0, + // these messages may not copy correctly. Make sure that they do, as fixed in bug 790912 + msg4.messageOffset = 0; + MailServices.copy.copyMessages( + IMAPPump.inbox, + [msg4], + gFolder1, + false, + null, + null, + true + ); + + // test the db headers in folder1 + db = gFolder1.msgDatabase; + let count = 0; + for (let message of db.enumerateMessages()) { + count++; + message instanceof Ci.nsIMsgDBHdr; + dump( + "message <" + + message.subject + + "> storeToken: <" + + message.getStringProperty("storeToken") + + "> offset: <" + + message.messageOffset + + "> id: <" + + message.messageId + + ">\n" + ); + if (gFolder1.msgStore.storeType == "mbox") { + Assert.equal( + message.messageOffset, + parseInt(message.getStringProperty("storeToken")) + ); + } + } + Assert.equal(count, 3); + }, + async function test_headers() { + let msgIds = [gMsgId1, gMsg3Id, gMsg4Id]; + for (let msgId of msgIds) { + let newMsgHdr = gFolder1.msgDatabase.getMsgHdrForMessageID(msgId); + Assert.ok(newMsgHdr.flags & Ci.nsMsgMessageFlags.Offline); + let msgURI = newMsgHdr.folder.getUriForMsg(newMsgHdr); + let msgServ = MailServices.messageServiceFromURI(msgURI); + let promiseStreamListener = new PromiseTestUtils.PromiseStreamListener(); + msgServ.streamHeaders(msgURI, promiseStreamListener, null, true); + let data = await promiseStreamListener.promise; + dump("\nheaders for messageId " + msgId + "\n" + data + "\n\n"); + Assert.ok(data.includes(msgId)); + } + }, + function moveMessagesToSubfolder() { + let db = IMAPPump.inbox.msgDatabase; + let messages = [...db.enumerateMessages()]; + Assert.ok(messages.length > 0); + // this is sync, I believe? + MailServices.copy.copyMessages( + IMAPPump.inbox, + messages, + gFolder1, + true, + null, + null, + true + ); + + // the inbox should now be empty + Assert.ok([...db.enumerateMessages()].length == 0); + + // maildir should also delete the files. + if (IMAPPump.inbox.msgStore.storeType == "maildir") { + let curDir = IMAPPump.inbox.filePath.clone(); + curDir.append("cur"); + Assert.ok(curDir.exists()); + Assert.ok(curDir.isDirectory()); + let curEnum = curDir.directoryEntries; + // the directory should be empty, fails from bug 771643 + Assert.ok(!curEnum.hasMoreElements()); + } + }, + teardownIMAPPump, +]; + +function run_test() { + tests.forEach(x => add_task(x)); + run_next_test(); +} diff --git a/comm/mailnews/imap/test/unit/test_offlineDraftDataloss.js b/comm/mailnews/imap/test/unit/test_offlineDraftDataloss.js new file mode 100644 index 0000000000..d13e07a367 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_offlineDraftDataloss.js @@ -0,0 +1,152 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This file tests that a message saved as draft in an IMAP folder in offline + * mode is not lost when going back online + * See Bug 805626 + */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); +var { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); + +var gDraftsFolder; + +add_setup(function () { + setupIMAPPump(); + Services.prefs.setBoolPref( + "mail.server.default.autosync_offline_stores", + false + ); +}); + +add_task(async function createDraftsFolder() { + IMAPPump.incomingServer.rootFolder.createSubfolder("Drafts", null); + await PromiseTestUtils.promiseFolderAdded("Drafts"); + gDraftsFolder = IMAPPump.incomingServer.rootFolder.getChildNamed("Drafts"); + Assert.ok(gDraftsFolder instanceof Ci.nsIMsgImapMailFolder); + let listener = new PromiseTestUtils.PromiseUrlListener(); + gDraftsFolder.updateFolderWithListener(null, listener); + await listener.promise; +}); + +add_task(async function goOffline() { + // Don't prompt about offline download when going offline. + Services.prefs.setIntPref("offline.download.download_messages", 2); + + IMAPPump.incomingServer.closeCachedConnections(); + let thread = gThreadManager.currentThread; + while (thread.hasPendingEvents()) { + thread.processNextEvent(true); + } + IMAPPump.server.stop(); + Services.io.offline = true; +}); + +add_task(async function saveDraft() { + let msgCompose = Cc["@mozilla.org/messengercompose/compose;1"].createInstance( + Ci.nsIMsgCompose + ); + let fields = Cc[ + "@mozilla.org/messengercompose/composefields;1" + ].createInstance(Ci.nsIMsgCompFields); + fields.from = "Nobody <nobody@tinderbox.test>"; + + let params = Cc[ + "@mozilla.org/messengercompose/composeparams;1" + ].createInstance(Ci.nsIMsgComposeParams); + params.composeFields = fields; + msgCompose.initialize(params); + + // Set up the identity. + let identity = MailServices.accounts.createIdentity(); + identity.draftFolder = gDraftsFolder.URI; + + let progress = Cc["@mozilla.org/messenger/progress;1"].createInstance( + Ci.nsIMsgProgress + ); + let progressListener = new WebProgressListener(); + progress.registerListener(progressListener); + msgCompose.sendMsg( + Ci.nsIMsgSend.nsMsgSaveAsDraft, + identity, + "", + null, + progress + ); + await progressListener.promise; + // Verify that message is not on the server yet. + Assert.equal(IMAPPump.daemon.getMailbox("Drafts")._messages.length, 0); +}); + +add_task(async function goOnline() { + let offlineManager = Cc[ + "@mozilla.org/messenger/offline-manager;1" + ].getService(Ci.nsIMsgOfflineManager); + IMAPPump.daemon.closing = false; + Services.io.offline = false; + + IMAPPump.server.start(); + offlineManager.inProgress = true; + offlineManager.goOnline(false, true, null); + // There seem to be some untraceable postprocessing with 100ms. + // (Found through xpcshell-test --verify) + await PromiseTestUtils.promiseDelay(100); + await TestUtils.waitForCondition( + () => !offlineManager.inProgress, + "wait for offlineManager not in progress" + ); + // Verify that message is now on the server. + Assert.equal(IMAPPump.daemon.getMailbox("Drafts")._messages.length, 1); +}); + +add_task(function endTest() { + teardownIMAPPump(); +}); + +/* + * helper functions + */ + +function WebProgressListener() { + this._promise = new Promise(resolve => { + this._resolve = resolve; + }); +} +WebProgressListener.prototype = { + onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) { + if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) { + this._resolve(); + } + }, + + onProgressChange( + aWebProgress, + aRequest, + aCurSelfProgress, + aMaxSelfProgress, + aCurTotalProgress, + aMaxTotalProgress + ) {}, + onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {}, + onStatusChange(aWebProgress, aRequest, aStatus, aMessage) {}, + onSecurityChange(aWebProgress, aRequest, state) {}, + onContentBlockingEvent(aWebProgress, aRequest, aEvent) {}, + + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsISupportsWeakReference", + ]), + + get promise() { + return this._promise; + }, +}; diff --git a/comm/mailnews/imap/test/unit/test_offlineMoveLocalToIMAP.js b/comm/mailnews/imap/test/unit/test_offlineMoveLocalToIMAP.js new file mode 100644 index 0000000000..c98a26470a --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_offlineMoveLocalToIMAP.js @@ -0,0 +1,125 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test to check that offline IMAP operation for a local->IMAP message + * move completes correctly once we go back online. + */ + +// NOTE: PromiseTestUtils and MailServices already imported + +const { MessageGenerator } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageGenerator.jsm" +); + +function setupTest() { + // Turn off autosync_offline_stores because + // fetching messages is invoked after copying the messages. + // (i.e. The fetching process will be invoked after OnStopCopy) + // It will cause crash with an assertion + // (ASSERTION: tried to add duplicate listener: 'index == -1') on teardown. + + Services.prefs.setBoolPref( + "mail.server.default.autosync_offline_stores", + false + ); + + setupIMAPPump(); + + // 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. + IMAPPump.inbox.hierarchyDelimiter = "/"; + IMAPPump.inbox.verifiedAsOnlineFolder = true; +} + +function teardownTest() { + teardownIMAPPump(); +} + +function goOffline() { + IMAPPump.incomingServer.closeCachedConnections(); + IMAPPump.server.stop(); + Services.io.offline = true; +} + +// Go back into online mode, and wait until offline IMAP operations are completed. +async function goOnline() { + IMAPPump.daemon.closing = false; + Services.io.offline = false; + IMAPPump.server.start(); + + let offlineManager = Cc[ + "@mozilla.org/messenger/offline-manager;1" + ].getService(Ci.nsIMsgOfflineManager); + offlineManager.goOnline( + false, // sendUnsentMessages + true, // playbackOfflineImapOperations + null // msgWindow + ); + // No way to signal when offline IMAP operations are complete... so we + // just blindly wait and cross our fingers :-( + await PromiseTestUtils.promiseDelay(2000); +} + +async function loadTestMessage(folder) { + let copyListener = new PromiseTestUtils.PromiseCopyListener(); + let file = do_get_file("../../../data/bugmail1"); + MailServices.copy.copyFileMessage( + file, + folder, + null, + false, + 0, + "", + copyListener, + null + ); + await copyListener.promise; +} + +add_task(async function testOfflineMoveLocalToIMAP() { + setupTest(); + + // Install a test message in the local folder. + await loadTestMessage(localAccountUtils.inboxFolder); + + goOffline(); + + // Move messages in local folder to the IMAP inbox. + // We're offline so this should result in a queued-up offline IMAP + // operation, which will execute when we go back online. + let copyListener = new PromiseTestUtils.PromiseCopyListener(); + let msgs = [...localAccountUtils.inboxFolder.msgDatabase.enumerateMessages()]; + MailServices.copy.copyMessages( + localAccountUtils.inboxFolder, + msgs, + IMAPPump.inbox, // dest folder + true, // move + copyListener, + null, + false // undo? + ); + await copyListener.promise; + + // Now, go back online and see if the operation has been performed + + await goOnline(); + + let imapINBOX = IMAPPump.inbox.QueryInterface(Ci.nsIMsgImapMailFolder); + let listener = new PromiseTestUtils.PromiseUrlListener(); + imapINBOX.updateFolderWithListener(null, listener); + await listener.promise; + + // Local folder should be empty, contents now in IMAP inbox. + let localCount = [ + ...localAccountUtils.inboxFolder.msgDatabase.enumerateMessages(), + ].length; + let imapCount = [...IMAPPump.inbox.msgDatabase.enumerateMessages()].length; + Assert.equal(imapCount, msgs.length); + Assert.equal(localCount, 0); + + teardownTest(); +}); diff --git a/comm/mailnews/imap/test/unit/test_offlinePlayback.js b/comm/mailnews/imap/test/unit/test_offlinePlayback.js new file mode 100644 index 0000000000..ae4361e16b --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_offlinePlayback.js @@ -0,0 +1,187 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Test to ensure that changes made while offline are played back when we + * go back online. + */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +/* import-globals-from ../../../test/resources/MessageGenerator.jsm */ +load("../../../resources/MessageGenerator.jsm"); + +var gSecondFolder, gThirdFolder; +var gSynthMessage1, gSynthMessage2; +// the message id of bugmail10 +var gMsgId1 = "200806061706.m56H6RWT004933@mrapp54.mozilla.org"; +var gOfflineManager; + +var tests = [ + setupIMAPPump, + function serverParms() { + var { fsDebugAll } = ChromeUtils.import( + "resource://testing-common/mailnews/Maild.jsm" + ); + IMAPPump.server.setDebugLevel(fsDebugAll); + }, + setup, + + function prepareToGoOffline() { + let rootFolder = IMAPPump.incomingServer.rootFolder; + gSecondFolder = rootFolder + .getChildNamed("secondFolder") + .QueryInterface(Ci.nsIMsgImapMailFolder); + gThirdFolder = rootFolder + .getChildNamed("thirdFolder") + .QueryInterface(Ci.nsIMsgImapMailFolder); + IMAPPump.incomingServer.closeCachedConnections(); + }, + async function doOfflineOps() { + IMAPPump.server.stop(); + Services.io.offline = true; + + // Flag the two messages, and then copy them to different folders. Since + // we're offline, these operations are synchronous. + let msgHdr1 = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID( + gSynthMessage1.messageId + ); + let msgHdr2 = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID( + gSynthMessage2.messageId + ); + let headers1 = [msgHdr1]; + let headers2 = [msgHdr2]; + msgHdr1.folder.markMessagesFlagged(headers1, true); + msgHdr2.folder.markMessagesFlagged(headers2, true); + let promiseCopyListener1 = new PromiseTestUtils.PromiseCopyListener(); + MailServices.copy.copyMessages( + IMAPPump.inbox, + headers1, + gSecondFolder, + true, + promiseCopyListener1, + null, + true + ); + let promiseCopyListener2 = new PromiseTestUtils.PromiseCopyListener(); + MailServices.copy.copyMessages( + IMAPPump.inbox, + headers2, + gThirdFolder, + true, + promiseCopyListener2, + null, + true + ); + var file = do_get_file("../../../data/bugmail10"); + let promiseCopyListener3 = new PromiseTestUtils.PromiseCopyListener(); + MailServices.copy.copyFileMessage( + file, + IMAPPump.inbox, + null, + false, + 0, + "", + promiseCopyListener3, + null + ); + await Promise.all([ + promiseCopyListener1.promise, + promiseCopyListener2.promise, + promiseCopyListener3.promise, + ]); + }, + async function goOnline() { + gOfflineManager = Cc["@mozilla.org/messenger/offline-manager;1"].getService( + Ci.nsIMsgOfflineManager + ); + IMAPPump.daemon.closing = false; + Services.io.offline = false; + + IMAPPump.server.start(); + gOfflineManager.goOnline(false, true, null); + await PromiseTestUtils.promiseDelay(2000); + }, + async function updateSecondFolder() { + let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener(); + gSecondFolder.updateFolderWithListener(null, promiseUrlListener); + await promiseUrlListener.promise; + }, + async function updateThirdFolder() { + let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener(); + gThirdFolder.updateFolderWithListener(null, promiseUrlListener); + await promiseUrlListener.promise; + }, + async function updateInbox() { + let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, promiseUrlListener); + await promiseUrlListener.promise; + }, + function checkDone() { + let msgHdr1 = gSecondFolder.msgDatabase.getMsgHdrForMessageID( + gSynthMessage1.messageId + ); + let msgHdr2 = gThirdFolder.msgDatabase.getMsgHdrForMessageID( + gSynthMessage2.messageId + ); + let msgHdr3 = IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID(gMsgId1); + Assert.notEqual(msgHdr1, null); + Assert.notEqual(msgHdr2, null); + Assert.notEqual(msgHdr3, null); + }, + teardownIMAPPump, +]; + +async function setup() { + /* + * Set up an IMAP server. + */ + IMAPPump.daemon.createMailbox("secondFolder", { subscribed: true }); + IMAPPump.daemon.createMailbox("thirdFolder", { subscribed: true }); + Services.prefs.setBoolPref( + "mail.server.default.autosync_offline_stores", + false + ); + // Don't prompt about offline download when going offline + Services.prefs.setIntPref("offline.download.download_messages", 2); + + // make a couple of messages + let messages = []; + let gMessageGenerator = new MessageGenerator(); + messages = messages.concat(gMessageGenerator.makeMessage()); + messages = messages.concat(gMessageGenerator.makeMessage()); + gSynthMessage1 = messages[0]; + gSynthMessage2 = messages[1]; + + let msgURI = Services.io.newURI( + "data:text/plain;base64," + btoa(messages[0].toMessageString()) + ); + let imapInbox = IMAPPump.daemon.getMailbox("INBOX"); + let message = new ImapMessage(msgURI.spec, imapInbox.uidnext++, ["\\Seen"]); + imapInbox.addMessage(message); + msgURI = Services.io.newURI( + "data:text/plain;base64," + btoa(messages[1].toMessageString()) + ); + message = new ImapMessage(msgURI.spec, imapInbox.uidnext++, ["\\Seen"]); + imapInbox.addMessage(message); + + // update folder to download header. + let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, promiseUrlListener); + await promiseUrlListener.promise; +} + +function run_test() { + Services.prefs.setBoolPref( + "mail.server.default.autosync_offline_stores", + false + ); + tests.forEach(x => add_task(x)); + run_next_test(); +} diff --git a/comm/mailnews/imap/test/unit/test_offlineStoreLocking.js b/comm/mailnews/imap/test/unit/test_offlineStoreLocking.js new file mode 100644 index 0000000000..e3a5da62b2 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_offlineStoreLocking.js @@ -0,0 +1,258 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Test to ensure that code that writes to the imap offline store deals + * with offline store locking correctly. + */ + +/* import-globals-from ../../../test/resources/alertTestUtils.js */ +load("../../../resources/alertTestUtils.js"); + +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); +var { MessageGenerator } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageGenerator.jsm" +); + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +// Globals +var gIMAPTrashFolder, gMsgImapInboxFolder; +var gMovedMsgId; + +var gAlertResolve; +var gGotAlert = new Promise(resolve => { + gAlertResolve = resolve; +}); + +/* exported alert to alertTestUtils.js */ +function alertPS(parent, aDialogTitle, aText) { + gAlertResolve(aText); +} + +function addGeneratedMessagesToServer(messages, mailbox) { + // Create the ImapMessages and store them on the mailbox + messages.forEach(function (message) { + let dataUri = Services.io.newURI( + "data:text/plain;base64," + btoa(message.toMessageString()) + ); + mailbox.addMessage(new ImapMessage(dataUri.spec, mailbox.uidnext++, [])); + }); +} + +var gStreamedHdr = null; + +add_setup(async function () { + registerAlertTestUtils(); + Services.prefs.setBoolPref( + "mail.server.default.autosync_offline_stores", + false + ); + + setupIMAPPump(); + + gMsgImapInboxFolder = IMAPPump.inbox.QueryInterface(Ci.nsIMsgImapMailFolder); + // these hacks are required because we've created the inbox before + // running initial folder discovery, and adding the folder bails + // out before we set it as verified online, so we bail out, and + // then remove the INBOX folder since it's not verified. + gMsgImapInboxFolder.hierarchyDelimiter = "/"; + gMsgImapInboxFolder.verifiedAsOnlineFolder = true; + + let messageGenerator = new MessageGenerator(); + let messages = []; + let bodyString = ""; + for (let i = 0; i < 100; i++) { + bodyString += + "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890\r\n"; + } + + for (let i = 0; i < 50; i++) { + messages = messages.concat( + messageGenerator.makeMessage({ + body: { body: bodyString, contentType: "text/plain" }, + }) + ); + } + + addGeneratedMessagesToServer(messages, IMAPPump.daemon.getMailbox("INBOX")); + // ...and download for offline use. + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, listener); + await listener.promise; +}); + +add_task(async function downloadForOffline() { + // ...and download for offline use. + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.downloadAllForOffline(listener, null); + await listener.promise; +}); + +add_task(async function deleteOneMsg() { + let enumerator = IMAPPump.inbox.msgDatabase.enumerateMessages(); + let msgHdr = enumerator.getNext().QueryInterface(Ci.nsIMsgDBHdr); + let copyListener = new PromiseTestUtils.PromiseCopyListener(); + IMAPPump.inbox.deleteMessages( + [msgHdr], + null, + false, + true, + copyListener, + false + ); + await copyListener.promise; +}); + +add_task(async function compactOneFolder() { + let enumerator = IMAPPump.inbox.msgDatabase.enumerateMessages(); + let msgHdr = enumerator.getNext().QueryInterface(Ci.nsIMsgDBHdr); + gStreamedHdr = msgHdr; + // Mark the message as not being offline, and then we'll make sure that + // streaming the message while we're compacting doesn't result in the + // message being marked for offline use. + // Luckily, compaction compacts the offline store first, so it should + // lock the offline store. + IMAPPump.inbox.msgDatabase.markOffline(msgHdr.messageKey, false, null); + let msgURI = msgHdr.folder.getUriForMsg(msgHdr); + let msgServ = MailServices.messageServiceFromURI(msgURI); + // UrlListener will get called when both expunge and offline store + // compaction are finished. dummyMsgWindow is required to make the backend + // compact the offline store. + let compactUrlListener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.compact(compactUrlListener, gDummyMsgWindow); + // Stream the message w/o a stream listener in an attempt to get the url + // started more quickly, while the compact is still going on. + let urlListener = new PromiseTestUtils.PromiseUrlListener({}); + await PromiseTestUtils.promiseDelay(100); // But don't be too fast. + msgServ.streamMessage( + msgURI, + new PromiseTestUtils.PromiseStreamListener(), + null, + urlListener, + false, + "", + false + ); + await compactUrlListener.promise; + + // Because we're streaming the message while compaction is going on, + // we should not have stored it for offline use. + Assert.equal(false, gStreamedHdr.flags & Ci.nsMsgMessageFlags.Offline); + + await urlListener.promise; +}); + +add_task(async function deleteAnOtherMsg() { + let enumerator = IMAPPump.inbox.msgDatabase.enumerateMessages(); + let msgHdr = enumerator.getNext().QueryInterface(Ci.nsIMsgDBHdr); + let copyListener = new PromiseTestUtils.PromiseCopyListener(); + IMAPPump.inbox.deleteMessages( + [msgHdr], + null, + false, + true, + copyListener, + false + ); + await copyListener.promise; +}); + +add_task(async function updateTrash() { + gIMAPTrashFolder = IMAPPump.incomingServer.rootFolder + .getChildNamed("Trash") + .QueryInterface(Ci.nsIMsgImapMailFolder); + let listener = new PromiseTestUtils.PromiseUrlListener(); + // hack to force uid validity to get initialized for trash. + gIMAPTrashFolder.updateFolderWithListener(null, listener); + await listener.promise; +}); + +add_task(async function downloadTrashForOffline() { + // ...and download for offline use. + let listener = new PromiseTestUtils.PromiseUrlListener(); + gIMAPTrashFolder.downloadAllForOffline(listener, null); + await listener.promise; +}); + +add_task(async function testOfflineBodyCopy() { + // In order to check that offline copy of messages doesn't try to copy + // the body if the offline store is locked, we're going to go offline. + // Thunderbird itself does move/copies pseudo-offline, but that's too + // hard to test because of the half-second delay. + IMAPPump.server.stop(); + Services.io.offline = true; + let enumerator = gIMAPTrashFolder.msgDatabase.enumerateMessages(); + let msgHdr = enumerator.getNext().QueryInterface(Ci.nsIMsgDBHdr); + gMovedMsgId = msgHdr.messageId; + let compactionListener = new PromiseTestUtils.PromiseUrlListener(); + // NOTE: calling compact() even if msgStore doesn't support compaction. + // It should be a safe no-op, and we're just testing that the listener is + // still invoked. + IMAPPump.inbox.compact(compactionListener, gDummyMsgWindow); + let copyListener = new PromiseTestUtils.PromiseCopyListener(); + MailServices.copy.copyMessages( + gIMAPTrashFolder, + [msgHdr], + IMAPPump.inbox, + true, + copyListener, + null, + true + ); + + // Verify that the moved Msg is not offline. + try { + let movedMsg = + IMAPPump.inbox.msgDatabase.getMsgHdrForMessageID(gMovedMsgId); + Assert.equal(0, movedMsg.flags & Ci.nsMsgMessageFlags.Offline); + } catch (ex) { + throw new Error(ex); + } + await compactionListener.promise; + await copyListener.promise; +}); + +add_task(async function test_checkAlert() { + // Check if testing maildir which doesn't produce an the alert like mbox. + // If so, don't wait for an alert. + let storageCID = Services.prefs.getCharPref( + "mail.serverDefaultStoreContractID" + ); + if (storageCID == "@mozilla.org/msgstore/maildirstore;1") { + return; + } + + let alertText = await gGotAlert; + Assert.ok( + alertText.startsWith( + "The folder 'Inbox on Mail for ' cannot be compacted because another operation is in progress. Please try again later." + ) + ); +}); + +add_task(function teardown() { + gMsgImapInboxFolder = null; + gIMAPTrashFolder = null; + + // IMAPPump.server has already stopped, we do not need to IMAPPump.server.stop(). + IMAPPump.inbox = null; + try { + IMAPPump.incomingServer.closeCachedConnections(); + let serverSink = IMAPPump.incomingServer.QueryInterface( + Ci.nsIImapServerSink + ); + serverSink.abortQueuedUrls(); + } catch (ex) { + throw new Error(ex); + } + let thread = gThreadManager.currentThread; + while (thread.hasPendingEvents()) { + thread.processNextEvent(true); + } +}); diff --git a/comm/mailnews/imap/test/unit/test_preserveDataOnMove.js b/comm/mailnews/imap/test/unit/test_preserveDataOnMove.js new file mode 100644 index 0000000000..a1f08cc8f0 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_preserveDataOnMove.js @@ -0,0 +1,90 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This tests that arbitrary message header properties are preserved +// during online move of an imap message. + +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +var gMessage = "bugmail10"; // message file used as the test message +var gSubfolder; + +add_setup(function () { + setupIMAPPump(); + Services.prefs.setBoolPref( + "mail.server.default.autosync_offline_stores", + false + ); +}); + +add_task(async function createSubfolder() { + IMAPPump.incomingServer.rootFolder.createSubfolder("Subfolder", null); + await PromiseTestUtils.promiseFolderAdded("Subfolder"); + gSubfolder = IMAPPump.incomingServer.rootFolder.getChildNamed("Subfolder"); + Assert.ok(gSubfolder instanceof Ci.nsIMsgImapMailFolder); + let listener = new PromiseTestUtils.PromiseUrlListener(); + gSubfolder.updateFolderWithListener(null, listener); + await listener.promise; +}); + +// load and update a message in the imap fake server +add_task(async function loadImapMessage() { + IMAPPump.mailbox.addMessage( + new ImapMessage(specForFileName(gMessage), IMAPPump.mailbox.uidnext++, []) + ); + IMAPPump.inbox.updateFolder(null); + await PromiseTestUtils.promiseFolderNotification(IMAPPump.inbox, "msgAdded"); + Assert.equal(1, IMAPPump.inbox.getTotalMessages(false)); + let msgHdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox); + Assert.ok(msgHdr instanceof Ci.nsIMsgDBHdr); + + // set an arbitrary property + msgHdr.setStringProperty("testprop", "somevalue"); +}); + +// move the message to a subfolder +add_task(async function moveMessageToSubfolder() { + let msgHdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox); + let copyListener = new PromiseTestUtils.PromiseCopyListener(); + MailServices.copy.copyMessages( + IMAPPump.inbox, // srcFolder + [msgHdr], // messages + gSubfolder, // dstFolder + true, // isMove + copyListener, // listener + null, // msgWindow + false // allowUndo + ); + await copyListener.promise; +}); + +add_task(async function testPropertyOnMove() { + let listener = new PromiseTestUtils.PromiseUrlListener(); + gSubfolder.updateFolderWithListener(null, listener); + await PromiseTestUtils.promiseFolderNotification(gSubfolder, "msgAdded"); + await listener.promise; + let msgHdr = mailTestUtils.firstMsgHdr(gSubfolder); + Assert.equal(msgHdr.getStringProperty("testprop"), "somevalue"); +}); + +// Cleanup +add_task(function endTest() { + teardownIMAPPump(); +}); + +/* + * helper functions + */ + +// given a test file, return the file uri spec +function specForFileName(aFileName) { + let file = do_get_file("../../../data/" + aFileName); + let msgfileuri = Services.io.newFileURI(file).QueryInterface(Ci.nsIFileURL); + return msgfileuri.spec; +} diff --git a/comm/mailnews/imap/test/unit/test_saveImapDraft.js b/comm/mailnews/imap/test/unit/test_saveImapDraft.js new file mode 100644 index 0000000000..ec2b96b3de --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_saveImapDraft.js @@ -0,0 +1,119 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * This file tests that a message saved as draft in an IMAP folder is correctly + * marked as unread. + */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +var gDraftsFolder; + +add_setup(function () { + setupIMAPPump(); + Services.prefs.setBoolPref( + "mail.server.default.autosync_offline_stores", + false + ); +}); + +add_task(async function createDraftsFolder() { + IMAPPump.incomingServer.rootFolder.createSubfolder("Drafts", null); + await PromiseTestUtils.promiseFolderAdded("Drafts"); + gDraftsFolder = IMAPPump.incomingServer.rootFolder.getChildNamed("Drafts"); + Assert.ok(gDraftsFolder instanceof Ci.nsIMsgImapMailFolder); + let listener = new PromiseTestUtils.PromiseUrlListener(); + gDraftsFolder.updateFolderWithListener(null, listener); + await listener.promise; +}); + +add_task(async function saveDraft() { + let msgCompose = Cc["@mozilla.org/messengercompose/compose;1"].createInstance( + Ci.nsIMsgCompose + ); + let fields = Cc[ + "@mozilla.org/messengercompose/composefields;1" + ].createInstance(Ci.nsIMsgCompFields); + fields.from = "Nobody <nobody@tinderbox.test>"; + + let params = Cc[ + "@mozilla.org/messengercompose/composeparams;1" + ].createInstance(Ci.nsIMsgComposeParams); + params.composeFields = fields; + msgCompose.initialize(params); + + // Set up the identity + let identity = MailServices.accounts.createIdentity(); + identity.draftFolder = gDraftsFolder.URI; + + let progress = Cc["@mozilla.org/messenger/progress;1"].createInstance( + Ci.nsIMsgProgress + ); + let progressListener = new ProgressListener(); + progress.registerListener(progressListener); + msgCompose.sendMsg( + Ci.nsIMsgSend.nsMsgSaveAsDraft, + identity, + "", + null, + progress + ); + await progressListener.promise; +}); + +add_task(async function updateDrafts() { + let listener = new PromiseTestUtils.PromiseUrlListener(); + gDraftsFolder.updateFolderWithListener(null, listener); + await listener.promise; +}); + +add_task(function checkResult() { + Assert.equal(gDraftsFolder.getTotalMessages(false), 1); + Assert.equal(gDraftsFolder.getNumUnread(false), 1); +}); + +add_task(function endTest() { + teardownIMAPPump(); +}); + +function ProgressListener() { + this._promise = new Promise(resolve => { + this._resolve = resolve; + }); +} + +ProgressListener.prototype = { + onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) { + if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) { + this._resolve(); + } + }, + + onProgressChange( + aWebProgress, + aRequest, + aCurSelfProgress, + aMaxSelfProgress, + aCurTotalProgress, + aMaxTotalProgress + ) {}, + onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {}, + onStatusChange(aWebProgress, aRequest, aStatus, aMessage) {}, + onSecurityChange(aWebProgress, aRequest, state) {}, + onContentBlockingEvent(aWebProgress, aRequest, aEvent) {}, + + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsISupportsWeakReference", + ]), + get promise() { + return this._promise; + }, +}; diff --git a/comm/mailnews/imap/test/unit/test_saveTemplate.js b/comm/mailnews/imap/test/unit/test_saveTemplate.js new file mode 100644 index 0000000000..12560411ee --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_saveTemplate.js @@ -0,0 +1,96 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Tests imap save of message as a template, and test initial save right after + * creation of folder. + */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm"); +var { MessageGenerator } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageGenerator.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +add_setup(function () { + setupIMAPPump(); + Services.prefs.setBoolPref( + "mail.server.default.autosync_offline_stores", + false + ); +}); + +// load and update a message in the imap fake server +add_task(async function loadImapMessage() { + let gMessageGenerator = new MessageGenerator(); + // create a synthetic message with attachment + let smsg = gMessageGenerator.makeMessage(); + + let msgURI = Services.io.newURI( + "data:text/plain;base64," + btoa(smsg.toMessageString()) + ); + let imapInbox = IMAPPump.daemon.getMailbox("INBOX"); + let message = new ImapMessage(msgURI.spec, imapInbox.uidnext++, []); + IMAPPump.mailbox.addMessage(message); + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, listener); + await listener.promise; +}); + +// This is similar to the method in mailCommands.js, to test the way that +// it creates a new templates folder before saving the message as a template. +add_task(async function saveAsTemplate() { + // Prepare msgAddedListener for this test. + let msgAddedListener = new MsgAddedListener(); + MailServices.mfn.addListener(msgAddedListener, MailServices.mfn.msgAdded); + + let hdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox); + let uri = IMAPPump.inbox.getUriForMsg(hdr); + let identity = MailServices.accounts.getFirstIdentityForServer( + IMAPPump.incomingServer + ); + identity.stationeryFolder = + IMAPPump.incomingServer.rootFolder.URI + "/Templates"; + let templates = MailUtils.getOrCreateFolder(identity.stationeryFolder); + // Verify that Templates folder doesn't exist, and then create it. + Assert.equal(templates.parent, null); + templates.setFlag(Ci.nsMsgFolderFlags.Templates); + let listener = new PromiseTestUtils.PromiseUrlListener({ + OnStopRunningUrl() { + let messenger = Cc["@mozilla.org/messenger;1"].createInstance( + Ci.nsIMessenger + ); + messenger.saveAs(uri, false, identity, null); + }, + }); + templates.createStorageIfMissing(listener); + await listener.promise; + + await msgAddedListener.promise; +}); + +// Cleanup +add_task(function endTest() { + teardownIMAPPump(); +}); + +// listener for saveAsTemplate adding a message to the templates folder. +function MsgAddedListener() { + this._promise = new Promise(resolve => { + this._resolve = resolve; + }); +} + +MsgAddedListener.prototype = { + msgAdded(aMsg) { + // Check this is the templates folder. + Assert.equal(aMsg.folder.prettyName, "Templates"); + this._resolve(); + }, +}; diff --git a/comm/mailnews/imap/test/unit/test_starttlsFailure.js b/comm/mailnews/imap/test/unit/test_starttlsFailure.js new file mode 100644 index 0000000000..59c7f2e70f --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_starttlsFailure.js @@ -0,0 +1,79 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This test checks that we handle the server dropping the connection + * on starttls. Since fakeserver doesn't support STARTTLS, I've made + * it drop the connection when it's attempted. + */ + +/* import-globals-from ../../../test/resources/alertTestUtils.js */ +load("../../../resources/alertTestUtils.js"); + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +var gAlertResolve; +var gGotAlert = new Promise(resolve => { + gAlertResolve = resolve; +}); + +/* exported alert to alertTestUtils.js */ +function alertPS(parent, aDialogTitle, aText) { + gAlertResolve(aText); +} + +add_setup(async function () { + // Set up IMAP fakeserver and incoming server. + IMAPPump.daemon = new ImapDaemon(); + IMAPPump.server = makeServer(IMAPPump.daemon, "", { dropOnStartTLS: true }); + IMAPPump.incomingServer = createLocalIMAPServer(IMAPPump.server.port); + IMAPPump.incomingServer.socketType = Ci.nsMsgSocketType.alwaysSTARTTLS; + + // We need a local account for the IMAP server to have its sent messages in. + localAccountUtils.loadLocalMailAccount(); + + // We need an identity so that updateFolder doesn't fail. + let imapAccount = MailServices.accounts.createAccount(); + let identity = MailServices.accounts.createIdentity(); + imapAccount.addIdentity(identity); + imapAccount.defaultIdentity = identity; + imapAccount.incomingServer = IMAPPump.incomingServer; + MailServices.accounts.defaultAccount = imapAccount; + + // The server doesn't support more than one connection. + Services.prefs.setIntPref("mail.server.server1.max_cached_connections", 1); + // We aren't interested in downloading messages automatically. + Services.prefs.setBoolPref("mail.server.server1.download_on_biff", false); + + IMAPPump.inbox = IMAPPump.incomingServer.rootFolder + .getChildNamed("Inbox") + .QueryInterface(Ci.nsIMsgImapMailFolder); + + registerAlertTestUtils(); + + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(gDummyMsgWindow, listener); + await listener.promise + .then(res => { + throw new Error("updateFolderWithListener has to fail"); + }) + .catch(exitCode => { + Assert.ok(!Components.isSuccessCode(exitCode)); + }); +}); + +add_task(async function check_alert() { + let alertText = await gGotAlert; + Assert.ok(alertText.startsWith("Server localhost has disconnected")); +}); + +add_task(function teardown() { + IMAPPump.incomingServer.closeCachedConnections(); + IMAPPump.server.stop(); +}); diff --git a/comm/mailnews/imap/test/unit/test_stopMovingToLocalFolder.js b/comm/mailnews/imap/test/unit/test_stopMovingToLocalFolder.js new file mode 100644 index 0000000000..9a7718332f --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_stopMovingToLocalFolder.js @@ -0,0 +1,95 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* Test that the message failed to move to a local folder remains on IMAP + * server. */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +var { MessageGenerator } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageGenerator.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +function stop_server() { + IMAPPump.incomingServer.closeCachedConnections(); + IMAPPump.server.stop(); + let thread = gThreadManager.currentThread; + while (thread.hasPendingEvents()) { + thread.processNextEvent(true); + } +} + +add_setup(function () { + setupIMAPPump(); + Services.prefs.setBoolPref( + "mail.server.default.autosync_offline_stores", + false + ); +}); + +add_setup(async function () { + let messageGenerator = new MessageGenerator(); + let messageString = messageGenerator.makeMessage().toMessageString(); + let dataUri = Services.io.newURI( + "data:text/plain;base64," + btoa(messageString) + ); + let imapMsg = new ImapMessage(dataUri.spec, IMAPPump.mailbox.uidnext++, []); + IMAPPump.mailbox.addMessage(imapMsg); + + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, listener); + await listener.promise; +}); + +add_task(async function move_messages() { + let msg = IMAPPump.inbox.msgDatabase.getMsgHdrForKey( + IMAPPump.mailbox.uidnext - 1 + ); + let copyListener = new PromiseTestUtils.PromiseCopyListener({ + OnProgress(aProgress, aProgressMax) { + stop_server(); + }, + }); + MailServices.copy.copyMessages( + IMAPPump.inbox, + [msg], + localAccountUtils.inboxFolder, + true, + copyListener, + null, + false + ); + await copyListener.promise; +}); + +add_task(function check_messages() { + Assert.equal(IMAPPump.inbox.getTotalMessages(false), 1); + Assert.equal(localAccountUtils.inboxFolder.getTotalMessages(false), 0); +}); + +add_task(function endTest() { + // IMAPPump.server.performTest() brings this test to a halt, + // so we need teardownIMAPPump() without IMAPPump.server.performTest(). + IMAPPump.inbox = null; + IMAPPump.server.resetTest(); + try { + IMAPPump.incomingServer.closeCachedConnections(); + let serverSink = IMAPPump.incomingServer.QueryInterface( + Ci.nsIImapServerSink + ); + serverSink.abortQueuedUrls(); + } catch (ex) { + dump(ex); + } + IMAPPump.server.stop(); + let thread = gThreadManager.currentThread; + while (thread.hasPendingEvents()) { + thread.processNextEvent(true); + } +}); diff --git a/comm/mailnews/imap/test/unit/test_subfolderLocation.js b/comm/mailnews/imap/test/unit/test_subfolderLocation.js new file mode 100644 index 0000000000..7cbf1b7bb8 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_subfolderLocation.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test proper location of new imap offline subfolders for maildir. + +// async support +/* import-globals-from ../../../test/resources/logHelper.js */ +/* import-globals-from ../../../test/resources/alertTestUtils.js */ +load("../../../resources/logHelper.js"); +load("../../../resources/alertTestUtils.js"); + +// Globals + +// Messages to load must have CRLF line endings, that is Windows style +var gMessage = "bugmail10"; // message file used as the test message + +add_task(function () { + Services.prefs.setBoolPref( + "mail.server.server1.autosync_offline_stores", + false + ); + setupIMAPPump(); +}); + +// load and update a message in the imap fake server +add_task(async function loadImapMessage() { + IMAPPump.mailbox.addMessage( + new ImapMessage(specForFileName(gMessage), IMAPPump.mailbox.uidnext++, []) + ); + let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(gDummyMsgWindow, promiseUrlListener); + await promiseUrlListener.promise; + + Assert.equal(1, IMAPPump.inbox.getTotalMessages(false)); + let msgHdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox); + Assert.ok(msgHdr instanceof Ci.nsIMsgDBHdr); +}); + +add_task(async function downloadOffline() { + // ...and download for offline use. + let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.downloadAllForOffline(promiseUrlListener, null); + await promiseUrlListener.promise; +}); + +var folderName1 = "sub1"; +var folderName2 = "sub2"; + +// use a folder method to add a subfolder +add_task(async function addSubfolder() { + let promiseFolder1 = PromiseTestUtils.promiseFolderAdded(folderName1); + IMAPPump.inbox.createSubfolder(folderName1, null); + await promiseFolder1; +}); + +// use a store method to add a subfolder +add_task(function storeAddSubfolder() { + IMAPPump.incomingServer.msgStore.createFolder(IMAPPump.inbox, folderName2); +}); + +// test that folders created with store and folder have the same parent +add_task(function testSubfolder() { + let subfolder1 = IMAPPump.inbox.getChildNamed(folderName1); + let subfolder2 = IMAPPump.inbox.getChildNamed(folderName2); + Assert.equal( + subfolder1.filePath.parent.path, + subfolder2.filePath.parent.path + ); +}); + +// Cleanup at end +add_task(teardownIMAPPump); + +/* + * helper functions + */ + +// given a test file, return the file uri spec +function specForFileName(aFileName) { + let file = do_get_file("../../../data/" + aFileName); + let msgfileuri = Services.io.newFileURI(file).QueryInterface(Ci.nsIFileURL); + return msgfileuri.spec; +} diff --git a/comm/mailnews/imap/test/unit/test_syncChanges.js b/comm/mailnews/imap/test/unit/test_syncChanges.js new file mode 100644 index 0000000000..e4b46f35a8 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_syncChanges.js @@ -0,0 +1,80 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Test to ensure that changes made from a different profile/machine + * are synced correctly. In particular, we're checking that emptying out + * an imap folder on the server makes us delete all the headers from our db. + */ + +var { MessageGenerator } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageGenerator.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +var gMessage; +var gSecondFolder; +var gSynthMessage; + +add_setup(async function () { + /* + * Set up an IMAP server. + */ + setupIMAPPump(); + + IMAPPump.daemon.createMailbox("secondFolder", { subscribed: true }); + + let messages = []; + let gMessageGenerator = new MessageGenerator(); + messages = messages.concat(gMessageGenerator.makeMessage()); + gSynthMessage = messages[0]; + + let msgURI = Services.io.newURI( + "data:text/plain;base64," + btoa(gSynthMessage.toMessageString()) + ); + gMessage = new ImapMessage(msgURI.spec, IMAPPump.mailbox.uidnext++, []); + IMAPPump.mailbox.addMessage(gMessage); + + // update folder to download header. + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, listener); + await listener.promise; +}); + +add_task(async function switchAwayFromInbox() { + let rootFolder = IMAPPump.incomingServer.rootFolder; + gSecondFolder = rootFolder + .getChildNamed("secondFolder") + .QueryInterface(Ci.nsIMsgImapMailFolder); + + // Selecting the second folder will close the cached connection + // on the inbox because fake server only supports one connection at a time. + // Then, we can poke at the message on the imap server directly, which + // simulates the user changing the message from a different machine, + // and Thunderbird discovering the change when it does a flag sync + // upon reselecting the Inbox. + let listener = new PromiseTestUtils.PromiseUrlListener(); + gSecondFolder.updateFolderWithListener(null, listener); + await listener.promise; +}); + +add_task(async function simulateMailboxEmptied() { + gMessage.setFlag("\\Deleted"); + let expungeListener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.expunge(expungeListener, null); + await expungeListener.promise; + let updateListener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, updateListener); + await updateListener.promise; +}); + +add_task(function checkMailboxEmpty() { + Assert.equal(IMAPPump.inbox.getTotalMessages(false), 0); +}); + +add_task(function endTest() { + teardownIMAPPump(); +}); diff --git a/comm/mailnews/imap/test/unit/test_trustSpamAssassin.js b/comm/mailnews/imap/test/unit/test_trustSpamAssassin.js new file mode 100644 index 0000000000..89c223b206 --- /dev/null +++ b/comm/mailnews/imap/test/unit/test_trustSpamAssassin.js @@ -0,0 +1,146 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * This file tests recognizing a message as junk due to + * SpamAssassin headers, and marking that as good + * without having the message return to the junk folder, + * as discussed in bug 540385. + * + * adapted from test_filterNeedsBody.js + */ + +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +// Globals +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +var gMessage = "SpamAssassinYes"; // message file used as the test message +var gJunkFolder; + +add_setup(function () { + setupIMAPPump(); + let server = IMAPPump.incomingServer; + let spamSettings = server.spamSettings; + server.setBoolValue("useServerFilter", true); + server.setCharValue("serverFilterName", "SpamAssassin"); + server.setIntValue( + "serverFilterTrustFlags", + Ci.nsISpamSettings.TRUST_POSITIVES + ); + server.setBoolValue("moveOnSpam", true); + server.setIntValue( + "moveTargetMode", + Ci.nsISpamSettings.MOVE_TARGET_MODE_ACCOUNT + ); + server.setCharValue("spamActionTargetAccount", server.serverURI); + + spamSettings.initialize(server); +}); + +add_task(async function createJunkFolder() { + IMAPPump.incomingServer.rootFolder.createSubfolder("Junk", null); + await PromiseTestUtils.promiseFolderAdded("Junk"); + gJunkFolder = IMAPPump.incomingServer.rootFolder.getChildNamed("Junk"); + Assert.ok(gJunkFolder instanceof Ci.nsIMsgImapMailFolder); + let listener = new PromiseTestUtils.PromiseUrlListener(); + gJunkFolder.updateFolderWithListener(null, listener); + await listener.promise; +}); + +/* + * Load and update a message in the imap fake server, should move + * SpamAssassin-marked junk message to junk folder + */ +add_task(async function loadImapMessage() { + IMAPPump.mailbox.addMessage( + new ImapMessage(specForFileName(gMessage), IMAPPump.mailbox.uidnext++, []) + ); + /* + * The message matched the SpamAssassin header, so it moved + * to the junk folder + */ + IMAPPump.inbox.updateFolder(null); + await PromiseTestUtils.promiseFolderNotification( + gJunkFolder, + "msgsMoveCopyCompleted" + ); + let listener = new PromiseTestUtils.PromiseUrlListener(); + gJunkFolder.updateFolderWithListener(null, listener); + await listener.promise; +}); + +add_task(function testMessageInJunk() { + Assert.equal(0, IMAPPump.inbox.getTotalMessages(false)); + Assert.equal(1, gJunkFolder.getTotalMessages(false)); +}); + +add_task(async function markMessageAsGood() { + /* + * This is done in the application in nsMsgDBView, which is difficult + * to test in xpcshell tests. We aren't really trying to test that here + * though, since the point of this test is working with the server-based + * filters. So I will simply simulate the operations that would typically + * be done by a manual marking of the messages. + */ + let msgHdr = mailTestUtils.firstMsgHdr(gJunkFolder); + msgHdr.setStringProperty("junkscoreorigin", "user"); + msgHdr.setStringProperty("junkpercent", "0"); // good percent + msgHdr.setStringProperty("junkscore", "0"); // good score + + /* + * Now move this message to the inbox. In bug 540385, the message just + * gets moved back to the junk folder again. We'll test that we + * are now preventing that. + */ + MailServices.copy.copyMessages( + gJunkFolder, // srcFolder + [msgHdr], // messages + IMAPPump.inbox, // dstFolder + true, // isMove + null, // listener + null, // msgWindow + false // allowUndo + ); + await PromiseTestUtils.promiseFolderNotification( + IMAPPump.inbox, + "msgsMoveCopyCompleted" + ); +}); + +add_task(async function updateFoldersAndCheck() { + let inboxUrlListener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, inboxUrlListener); + await inboxUrlListener.promise; + let junkUrlListener = new PromiseTestUtils.PromiseUrlListener(); + gJunkFolder.updateFolderWithListener(null, junkUrlListener); + await junkUrlListener.promise; + // bug 540385 causes this test to fail + Assert.equal(1, IMAPPump.inbox.getTotalMessages(false)); + Assert.equal(0, gJunkFolder.getTotalMessages(false)); +}); + +add_task(function endTest() { + teardownIMAPPump(); +}); + +/* + * helper functions + */ + +// given a test file, return the file uri spec +function specForFileName(aFileName) { + let file = do_get_file("../../../data/" + aFileName); + let msgfileuri = Services.io.newFileURI(file).QueryInterface(Ci.nsIFileURL); + return msgfileuri.spec; +} + +// quick shorthand for output of a line of text. +function dl(text) { + dump(text + "\n"); +} diff --git a/comm/mailnews/imap/test/unit/xpcshell-cpp.ini b/comm/mailnews/imap/test/unit/xpcshell-cpp.ini new file mode 100644 index 0000000000..394ca5fb1d --- /dev/null +++ b/comm/mailnews/imap/test/unit/xpcshell-cpp.ini @@ -0,0 +1,14 @@ +[DEFAULT] +head = head_server.js +tags = mbox cpp +dupe-manifest = +run-sequentially = +prefs = + mailnews.imap.jsmodule=false + +[test_chunkLastLF.js] +[test_customCommandReturnsFetchResponse.js] +[test_imapChunks.js] +[test_imapHdrChunking.js] + +[include:xpcshell-shared.ini] diff --git a/comm/mailnews/imap/test/unit/xpcshell-shared.ini b/comm/mailnews/imap/test/unit/xpcshell-shared.ini new file mode 100644 index 0000000000..b169aee8d1 --- /dev/null +++ b/comm/mailnews/imap/test/unit/xpcshell-shared.ini @@ -0,0 +1,58 @@ +[test_autosync_date_constraints.js] +[test_bccProperty.js] +[test_bug460636.js] +[test_compactOfflineStore.js] +[test_converterImap.js] +[test_copyThenMove.js] +[test_dontStatNoSelect.js] +[test_downloadOffline.js] +[test_fetchCustomAttribute.js] +[test_filterCustomHeaders.js] +[test_filterNeedsBody.js] +[test_folderOfflineFlags.js] +[test_gmailAttributes.js] +[test_gmailOfflineMsgStore.js] +[test_imapAttachmentSaves.js] +[test_imapAutoSync.js] +[test_imapClientid.js] +[test_imapContentLength.js] +[test_imapCopyTimeout.js] +[test_imapFilterActions.js] +[test_imapFilterActionsPostplugin.js] +[test_imapFlagChange.js] +[test_imapFolderCopy.js] +[test_imapHdrStreaming.js] +[test_imapHighWater.js] +[test_imapID.js] +[test_imapMove.js] +[test_imapPasswordFailure.js] +[test_imapProtocols.js] +[test_imapProxy.js] +[test_imapRename.js] +[test_imapSearch.js] +[test_imapStatusCloseDBs.js] +[test_imapStoreMsgOffline.js] +[test_imapUndo.js] +[test_imapUrls.js] +[test_largeOfflineStore.js] +skip-if = os == 'mac' +[test_listClosesDB.js] +[test_listSubscribed.js] +[test_localToImapFilter.js] +[test_localToImapFilterQuarantine.js] +[test_lsub.js] +[test_mailboxes.js] +[test_nsIMsgFolderListenerIMAP.js] +[test_offlineCopy.js] +[test_offlineDraftDataloss.js] +[test_offlineMoveLocalToIMAP.js] +[test_offlinePlayback.js] +[test_offlineStoreLocking.js] +[test_preserveDataOnMove.js] +[test_saveImapDraft.js] +[test_saveTemplate.js] +[test_starttlsFailure.js] +[test_stopMovingToLocalFolder.js] +[test_subfolderLocation.js] +[test_syncChanges.js] +[test_trustSpamAssassin.js] diff --git a/comm/mailnews/imap/test/unit/xpcshell.ini b/comm/mailnews/imap/test/unit/xpcshell.ini new file mode 100644 index 0000000000..b0be23ad01 --- /dev/null +++ b/comm/mailnews/imap/test/unit/xpcshell.ini @@ -0,0 +1,12 @@ +[DEFAULT] +head = head_server.js +tags = mbox jsm +dupe-manifest = +run-sequentially = +prefs = + mailnews.imap.jsmodule=true + +[test_ImapResponse.js] +[test_imapAuthMethods.js] + +[include:xpcshell-shared.ini] diff --git a/comm/mailnews/imap/test/unit/xpcshell_maildir-cpp.ini b/comm/mailnews/imap/test/unit/xpcshell_maildir-cpp.ini new file mode 100644 index 0000000000..4c345d69e3 --- /dev/null +++ b/comm/mailnews/imap/test/unit/xpcshell_maildir-cpp.ini @@ -0,0 +1,14 @@ +[DEFAULT] +head = head_imap_maildir.js +tags = maildir cpp +dupe-manifest = +run-sequentially = +prefs = + mailnews.imap.jsmodule=false + +[test_chunkLastLF.js] +[test_customCommandReturnsFetchResponse.js] +[test_imapChunks.js] +[test_imapHdrChunking.js] + +[include:xpcshell-shared.ini] diff --git a/comm/mailnews/imap/test/unit/xpcshell_maildir.ini b/comm/mailnews/imap/test/unit/xpcshell_maildir.ini new file mode 100644 index 0000000000..6bb40162a8 --- /dev/null +++ b/comm/mailnews/imap/test/unit/xpcshell_maildir.ini @@ -0,0 +1,9 @@ +[DEFAULT] +head = head_imap_maildir.js +tags = maildir jsm +dupe-manifest = +run-sequentially = +prefs = + mailnews.imap.jsmodule=true + +[include:xpcshell-shared.ini] |