From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- comm/mailnews/base/src/nsMessenger.cpp | 2446 ++++++++++++++++++++++++++++++++ 1 file changed, 2446 insertions(+) create mode 100644 comm/mailnews/base/src/nsMessenger.cpp (limited to 'comm/mailnews/base/src/nsMessenger.cpp') diff --git a/comm/mailnews/base/src/nsMessenger.cpp b/comm/mailnews/base/src/nsMessenger.cpp new file mode 100644 index 0000000000..7fc5c05934 --- /dev/null +++ b/comm/mailnews/base/src/nsMessenger.cpp @@ -0,0 +1,2446 @@ +/* -*- 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 "prsystem.h" + +#include "nsMessenger.h" + +// xpcom +#include "nsIComponentManager.h" +#include "nsIServiceManager.h" +#include "nsIStringStream.h" +#include "nsLocalFile.h" +#include "nsDirectoryServiceDefs.h" +#include "nsQuickSort.h" +#include "nsNativeCharsetUtils.h" +#include "mozilla/Path.h" +#include "mozilla/Components.h" +#include "mozilla/dom/LoadURIOptionsBinding.h" + +// necko +#include "nsMimeTypes.h" +#include "nsIURL.h" +#include "nsIPrompt.h" +#include "nsIStreamListener.h" +#include "nsIStreamConverterService.h" +#include "nsNetUtil.h" +#include "nsIFileURL.h" +#include "nsIMIMEInfo.h" + +// gecko +#include "nsLayoutCID.h" +#include "nsIContentViewer.h" + +/* for access to docshell */ +#include "nsPIDOMWindow.h" +#include "nsIDocShell.h" +#include "nsIDocShellTreeItem.h" +#include "nsIWebNavigation.h" +#include "nsContentUtils.h" +#include "nsDocShellLoadState.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/XULFrameElement.h" +#include "nsFrameLoader.h" +#include "mozilla/dom/Document.h" + +// mail +#include "nsIMsgMailNewsUrl.h" +#include "nsIMsgAccountManager.h" +#include "nsIMsgMailSession.h" +#include "nsIMailboxUrl.h" +#include "nsIMsgFolder.h" +#include "nsMsgMessageFlags.h" +#include "nsIMsgIncomingServer.h" + +#include "nsIMsgMessageService.h" + +#include "nsIMsgHdr.h" +// compose +#include "nsNativeCharsetUtils.h" + +// draft/folders/sendlater/etc +#include "nsIMsgCopyService.h" +#include "nsIMsgCopyServiceListener.h" +#include "nsIUrlListener.h" +#include "UrlListener.h" + +// undo +#include "nsITransaction.h" +#include "nsMsgTxn.h" + +// charset conversions +#include "nsIMimeConverter.h" + +// Save As +#include "nsIStringBundle.h" +#include "nsIPrefService.h" +#include "nsIPrefBranch.h" +#include "nsCExternalHandlerService.h" +#include "nsIExternalProtocolService.h" +#include "nsIMIMEService.h" +#include "nsITransfer.h" + +#define MESSENGER_SAVE_DIR_PREF_NAME "messenger.save.dir" +#define MIMETYPE_DELETED "text/x-moz-deleted" +#define ATTACHMENT_PERMISSION 00664 + +// +// Convert an nsString buffer to plain text... +// +#include "nsMsgUtils.h" +#include "nsCharsetSource.h" +#include "nsIChannel.h" +#include "nsIOutputStream.h" +#include "nsIPrincipal.h" + +#include "mozilla/dom/BrowserParent.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" + +#include "mozilla/NullPrincipal.h" +#include "mozilla/dom/RemoteType.h" +#include "nsQueryObject.h" +#include "mozilla/JSONStringWriteFuncs.h" + +using namespace mozilla; +using namespace mozilla::dom; + +static void ConvertAndSanitizeFileName(const nsACString& displayName, + nsString& aResult) { + nsCString unescapedName; + + /* we need to convert the UTF-8 fileName to platform specific character set. + The display name is in UTF-8 because it has been escaped from JS + */ + MsgUnescapeString(displayName, 0, unescapedName); + CopyUTF8toUTF16(unescapedName, aResult); + + // replace platform specific path separator and illegale characters to avoid + // any confusion + aResult.ReplaceChar(u"" FILE_PATH_SEPARATOR FILE_ILLEGAL_CHARACTERS, u'-'); +} + +// *************************************************** +// jefft - this is a rather obscured class serves for Save Message As File, +// Save Message As Template, and Save Attachment to a file +// It's used to save out a single item. If multiple items are to be saved, +// a nsSaveAllAttachmentsState should be set, which holds a list of items. +// +class nsSaveAllAttachmentsState; + +class nsSaveMsgListener : public nsIUrlListener, + public nsIMsgCopyServiceListener, + public nsIStreamListener, + public nsICancelable { + using PathChar = mozilla::filesystem::Path::value_type; + + public: + nsSaveMsgListener(nsIFile* file, nsMessenger* aMessenger, + nsIUrlListener* aListener); + + NS_DECL_ISUPPORTS + + NS_DECL_NSIURLLISTENER + NS_DECL_NSIMSGCOPYSERVICELISTENER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSICANCELABLE + + nsCOMPtr m_file; + nsCOMPtr m_outputStream; + char m_dataBuffer[FILE_IO_BUFFER_SIZE]; + nsCOMPtr m_channel; + nsCString m_templateUri; + RefPtr m_messenger; + nsSaveAllAttachmentsState* m_saveAllAttachmentsState; + + // rhp: For character set handling + bool m_doCharsetConversion; + nsString m_charset; + enum { eUnknown, ePlainText, eHTML } m_outputFormat; + nsCString m_msgBuffer; + + nsCString m_contentType; // used only when saving attachment + + nsCOMPtr mTransfer; + nsCOMPtr mListener; + nsCOMPtr mListenerUri; + int64_t mProgress; + int64_t mMaxProgress; + bool mCanceled; + bool mInitialized; + bool mUrlHasStopped; + bool mRequestHasStopped; + nsresult InitializeDownload(nsIRequest* aRequest); + + private: + virtual ~nsSaveMsgListener(); +}; + +// This helper class holds a list of attachments to be saved and (optionally) +// detached. It's used by nsSaveMsgListener (which only sticks around for a +// single item, then passes the nsSaveAllAttachmentsState along to the next +// SaveAttachment() call). +class nsSaveAllAttachmentsState { + using PathChar = mozilla::filesystem::Path::value_type; + + public: + nsSaveAllAttachmentsState(const nsTArray& contentTypeArray, + const nsTArray& urlArray, + const nsTArray& displayNameArray, + const nsTArray& messageUriArray, + const PathChar* directoryName, + bool detachingAttachments, + nsIUrlListener* overallListener); + virtual ~nsSaveAllAttachmentsState(); + + uint32_t m_count; + uint32_t m_curIndex; + PathChar* m_directoryName; + nsTArray m_contentTypeArray; + nsTArray m_urlArray; + nsTArray m_displayNameArray; + nsTArray m_messageUriArray; + bool m_detachingAttachments; + // The listener to invoke when all the items have been saved. + nsCOMPtr m_overallListener; + // if detaching, do without warning? Will create unique files instead of + // prompting if duplicate files exist. + bool m_withoutWarning; + // if detaching first, remember where we saved to. + nsTArray m_savedFiles; +}; + +// +// nsMessenger +// +nsMessenger::nsMessenger() {} + +nsMessenger::~nsMessenger() {} + +NS_IMPL_ISUPPORTS(nsMessenger, nsIMessenger, nsISupportsWeakReference) + +NS_IMETHODIMP nsMessenger::SetWindow(mozIDOMWindowProxy* aWin, + nsIMsgWindow* aMsgWindow) { + nsresult rv; + + nsCOMPtr mailSession = + do_GetService("@mozilla.org/messenger/services/session;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + if (aWin) { + aMsgWindow->GetTransactionManager(getter_AddRefs(mTxnMgr)); + mMsgWindow = aMsgWindow; + mWindow = aWin; + + NS_ENSURE_TRUE(aWin, NS_ERROR_FAILURE); + nsCOMPtr win = nsPIDOMWindowOuter::From(aWin); + mDocShell = win->GetDocShell(); + } else { + mWindow = nullptr; + mDocShell = nullptr; + } + + return NS_OK; +} + +NS_IMPL_ISUPPORTS(nsMessenger::nsFilePickerShownCallback, + nsIFilePickerShownCallback) +nsMessenger::nsFilePickerShownCallback::nsFilePickerShownCallback() { + mResult = nsIFilePicker::returnOK; + mPickerDone = false; +} + +NS_IMETHODIMP +nsMessenger::nsFilePickerShownCallback::Done( + nsIFilePicker::ResultCode aResult) { + mResult = aResult; + mPickerDone = true; + return NS_OK; +} + +nsresult nsMessenger::ShowPicker(nsIFilePicker* aPicker, + nsIFilePicker::ResultCode* aResult) { + nsCOMPtr callback = + new nsMessenger::nsFilePickerShownCallback(); + nsFilePickerShownCallback* cb = + static_cast(callback.get()); + + nsresult rv; + rv = aPicker->Open(callback); + NS_ENSURE_SUCCESS(rv, rv); + + // Spin the event loop until the callback was called. + nsCOMPtr thread(do_GetCurrentThread()); + while (!cb->mPickerDone) { + NS_ProcessNextEvent(thread, true); + } + + *aResult = cb->mResult; + return NS_OK; +} + +nsresult nsMessenger::PromptIfFileExists(nsIFile* file) { + nsresult rv = NS_ERROR_FAILURE; + bool exists; + file->Exists(&exists); + if (!exists) return NS_OK; + + nsCOMPtr dialog(do_GetInterface(mDocShell)); + if (!dialog) return rv; + nsAutoString path; + bool dialogResult = false; + nsString errorMessage; + + file->GetPath(path); + AutoTArray pathFormatStrings = {path}; + + if (!mStringBundle) { + rv = InitStringBundle(); + NS_ENSURE_SUCCESS(rv, rv); + } + rv = mStringBundle->FormatStringFromName("fileExists", pathFormatStrings, + errorMessage); + NS_ENSURE_SUCCESS(rv, rv); + rv = dialog->Confirm(nullptr, errorMessage.get(), &dialogResult); + NS_ENSURE_SUCCESS(rv, rv); + + if (dialogResult) return NS_OK; // user says okay to replace + + // if we don't re-init the path for redisplay the picker will + // show the full path, not just the file name + nsCOMPtr currentFile = + do_CreateInstance("@mozilla.org/file/local;1"); + if (!currentFile) return NS_ERROR_FAILURE; + + rv = currentFile->InitWithPath(path); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString leafName; + currentFile->GetLeafName(leafName); + if (!leafName.IsEmpty()) + path.Assign(leafName); // path should be a copy of leafName + + nsCOMPtr filePicker = + do_CreateInstance("@mozilla.org/filepicker;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsString saveAttachmentStr; + GetString(u"SaveAttachment"_ns, saveAttachmentStr); + filePicker->Init(mWindow, saveAttachmentStr, nsIFilePicker::modeSave); + filePicker->SetDefaultString(path); + filePicker->AppendFilters(nsIFilePicker::filterAll); + + nsCOMPtr lastSaveDir; + rv = GetLastSaveDirectory(getter_AddRefs(lastSaveDir)); + if (NS_SUCCEEDED(rv) && lastSaveDir) { + filePicker->SetDisplayDirectory(lastSaveDir); + } + + nsIFilePicker::ResultCode dialogReturn; + rv = ShowPicker(filePicker, &dialogReturn); + if (NS_FAILED(rv) || dialogReturn == nsIFilePicker::returnCancel) { + // XXX todo + // don't overload the return value like this + // change this function to have an out boolean + // that we check to see if the user cancelled + return NS_ERROR_FAILURE; + } + + nsCOMPtr localFile; + + rv = filePicker->GetFile(getter_AddRefs(localFile)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = SetLastSaveDirectory(localFile); + NS_ENSURE_SUCCESS(rv, rv); + + // reset the file to point to the new path + return file->InitWithFile(localFile); +} + +NS_IMETHODIMP nsMessenger::SaveAttachmentToFile(nsIFile* aFile, + const nsACString& aURL, + const nsACString& aMessageUri, + const nsACString& aContentType, + nsIUrlListener* aListener) { + return SaveAttachment(aFile, aURL, aMessageUri, aContentType, nullptr, + aListener); +} + +NS_IMETHODIMP +nsMessenger::DetachAttachmentsWOPrompts( + nsIFile* aDestFolder, const nsTArray& aContentTypeArray, + const nsTArray& aUrlArray, + const nsTArray& aDisplayNameArray, + const nsTArray& aMessageUriArray, nsIUrlListener* aListener) { + NS_ENSURE_ARG_POINTER(aDestFolder); + MOZ_ASSERT(aContentTypeArray.Length() == aUrlArray.Length() && + aUrlArray.Length() == aDisplayNameArray.Length() && + aDisplayNameArray.Length() == aMessageUriArray.Length()); + + if (!aContentTypeArray.Length()) return NS_OK; + nsCOMPtr attachmentDestination; + nsresult rv = aDestFolder->Clone(getter_AddRefs(attachmentDestination)); + NS_ENSURE_SUCCESS(rv, rv); + + PathString path = attachmentDestination->NativePath(); + + nsAutoString unescapedFileName; + ConvertAndSanitizeFileName(aDisplayNameArray[0], unescapedFileName); + rv = attachmentDestination->Append(unescapedFileName); + NS_ENSURE_SUCCESS(rv, rv); + rv = attachmentDestination->CreateUnique(nsIFile::NORMAL_FILE_TYPE, + ATTACHMENT_PERMISSION); + NS_ENSURE_SUCCESS(rv, rv); + + // Set up to detach the attachments once they've been saved out. + // NOTE: nsSaveAllAttachmentsState has a detach option, but I'd like to + // phase it out, so we set up a listener to call DetachAttachments() + // instead. + UrlListener* listener = new UrlListener; + nsSaveAllAttachmentsState* saveState = new nsSaveAllAttachmentsState( + aContentTypeArray, aUrlArray, aDisplayNameArray, aMessageUriArray, + path.get(), + false, // detach = false + listener); + + // Note: saveState is kept in existence by SaveAttachment() until after + // the last item is saved. + listener->mStopFn = [saveState, self = RefPtr(this), + originalListener = nsCOMPtr(aListener)]( + nsIURI* url, nsresult status) -> nsresult { + if (NS_SUCCEEDED(status)) { + status = self->DetachAttachments( + saveState->m_contentTypeArray, saveState->m_urlArray, + saveState->m_displayNameArray, saveState->m_messageUriArray, + &saveState->m_savedFiles, originalListener, + saveState->m_withoutWarning); + } + if (NS_FAILED(status) && originalListener) { + return originalListener->OnStopRunningUrl(nullptr, status); + } + return NS_OK; + }; + + // This method is used in filters, where we don't want to warn + saveState->m_withoutWarning = true; + + rv = SaveAttachment(attachmentDestination, aUrlArray[0], aMessageUriArray[0], + aContentTypeArray[0], saveState, nullptr); + return rv; +} + +// Internal helper for Saving attachments. +// It handles a single attachment, but multiple attachments can be saved +// by passing in an nsSaveAllAttachmentsState. In this case, SaveAttachment() +// will be called for each attachment, and the saveState keeps track of which +// one we're up to. +// +// aListener is invoked to cover this single attachment save. +// If a saveState is used, it can also contain a nsIUrlListener which +// will be invoked when _all_ the saves are complete. +// +// SaveAttachment() takes ownership of the saveState passed in. +// If SaveAttachment() fails, then +// saveState->m_overallListener->OnStopRunningUrl() +// will be invoked and saveState itself will be deleted. +// +// Even though SaveAttachment() takes ownership of saveState, +// nsSaveMsgListener is responsible for finally deleting it when the +// last save operation successfully completes. +// +// Yes, this is convoluted. Bug 1788159 covers simplifying all this stuff. +nsresult nsMessenger::SaveAttachment(nsIFile* aFile, const nsACString& aURL, + const nsACString& aMessageUri, + const nsACString& aContentType, + nsSaveAllAttachmentsState* saveState, + nsIUrlListener* aListener) { + nsCOMPtr messageService; + nsCOMPtr fetchService; + nsAutoCString urlString; + nsAutoCString fullMessageUri(aMessageUri); + + nsresult rv = NS_OK; + + // This instance will be held onto by the listeners, and will be released once + // the transfer has been completed. + RefPtr saveListener( + new nsSaveMsgListener(aFile, this, aListener)); + + saveListener->m_contentType = aContentType; + if (saveState) { + if (saveState->m_overallListener && saveState->m_curIndex == 0) { + // This is the first item, so tell the caller we're starting. + saveState->m_overallListener->OnStartRunningUrl(nullptr); + } + saveListener->m_saveAllAttachmentsState = saveState; + // Record the resultant file:// URL for each saved attachment as we go + // along. It'll be used later if we want to also detach them from the email. + // Placeholder text will be inserted into the email to replace the + // removed attachment pointing at it's final resting place. + nsCOMPtr outputURI; + rv = NS_NewFileURI(getter_AddRefs(outputURI), aFile); + if (NS_SUCCEEDED(rv)) { + nsAutoCString fileUriSpec; + rv = outputURI->GetSpec(fileUriSpec); + if NS_SUCCEEDED (rv) { + saveState->m_savedFiles.AppendElement(fileUriSpec); + } + } + } + + nsCOMPtr URL; + if (NS_SUCCEEDED(rv)) { + urlString = aURL; + // strip out ?type=application/x-message-display because it confuses libmime + + int32_t typeIndex = urlString.Find("?type=application/x-message-display"); + if (typeIndex != kNotFound) { + urlString.Cut(typeIndex, + sizeof("?type=application/x-message-display") - 1); + // we also need to replace the next '&' with '?' + int32_t firstPartIndex = urlString.FindChar('&'); + if (firstPartIndex != kNotFound) urlString.SetCharAt('?', firstPartIndex); + } + + urlString.ReplaceSubstring("/;section", "?section"); + rv = NS_NewURI(getter_AddRefs(URL), urlString); + } + + if (NS_SUCCEEDED(rv)) { + rv = GetMessageServiceFromURI(aMessageUri, getter_AddRefs(messageService)); + if (NS_SUCCEEDED(rv)) { + fetchService = do_QueryInterface(messageService); + // if the message service has a fetch part service then we know we can + // fetch mime parts... + if (fetchService) { + int32_t partPos = urlString.FindChar('?'); + if (partPos == kNotFound) return NS_ERROR_FAILURE; + fullMessageUri.Append(Substring(urlString, partPos)); + } + + nsCOMPtr convertedListener; + saveListener->QueryInterface(NS_GET_IID(nsIStreamListener), + getter_AddRefs(convertedListener)); + + nsCOMPtr dummyNull; + if (fetchService) + rv = fetchService->FetchMimePart(URL, fullMessageUri, convertedListener, + mMsgWindow, saveListener, + getter_AddRefs(dummyNull)); + else + rv = messageService->LoadMessage(fullMessageUri, convertedListener, + mMsgWindow, nullptr, false); + } // if we got a message service + } // if we created a url + + if (NS_FAILED(rv)) { + if (saveState) { + // If we had a listener, make sure it sees the failure! + if (saveState->m_overallListener) { + saveState->m_overallListener->OnStopRunningUrl(nullptr, rv); + } + // Ugh. Ownership is all over the place here! + // Usually nsSaveMsgListener is responsible for cleaning up + // nsSaveAllAttachmentsState... but we're not getting + // that far, so have to clean it up here! + delete saveState; + saveListener->m_saveAllAttachmentsState = nullptr; + } + Alert("saveAttachmentFailed"); + } + return rv; +} + +NS_IMETHODIMP +nsMessenger::SaveAttachmentToFolder(const nsACString& contentType, + const nsACString& url, + const nsACString& displayName, + const nsACString& messageUri, + nsIFile* aDestFolder, nsIFile** aOutFile) { + NS_ENSURE_ARG_POINTER(aDestFolder); + nsresult rv; + + nsCOMPtr attachmentDestination; + rv = aDestFolder->Clone(getter_AddRefs(attachmentDestination)); + NS_ENSURE_SUCCESS(rv, rv); + + nsString unescapedFileName; + ConvertAndSanitizeFileName(displayName, unescapedFileName); + rv = attachmentDestination->Append(unescapedFileName); + NS_ENSURE_SUCCESS(rv, rv); +#ifdef XP_MACOSX + rv = attachmentDestination->CreateUnique(nsIFile::NORMAL_FILE_TYPE, + ATTACHMENT_PERMISSION); + NS_ENSURE_SUCCESS(rv, rv); +#endif + + rv = SaveAttachment(attachmentDestination, url, messageUri, contentType, + nullptr, nullptr); + attachmentDestination.forget(aOutFile); + return rv; +} + +NS_IMETHODIMP +nsMessenger::SaveAttachment(const nsACString& aContentType, + const nsACString& aURL, + const nsACString& aDisplayName, + const nsACString& aMessageUri, + bool aIsExternalAttachment) { + return SaveOneAttachment(aContentType, aURL, aDisplayName, aMessageUri, + false); +} + +nsresult nsMessenger::SaveOneAttachment(const nsACString& aContentType, + const nsACString& aURL, + const nsACString& aDisplayName, + const nsACString& aMessageUri, + bool detaching) { + nsresult rv = NS_ERROR_OUT_OF_MEMORY; + nsCOMPtr filePicker = + do_CreateInstance("@mozilla.org/filepicker;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsIFilePicker::ResultCode dialogResult; + nsCOMPtr localFile; + nsCOMPtr lastSaveDir; + nsCString filePath; + nsString saveAttachmentStr; + nsString defaultDisplayString; + ConvertAndSanitizeFileName(aDisplayName, defaultDisplayString); + + if (detaching) { + GetString(u"DetachAttachment"_ns, saveAttachmentStr); + } else { + GetString(u"SaveAttachment"_ns, saveAttachmentStr); + } + filePicker->Init(mWindow, saveAttachmentStr, nsIFilePicker::modeSave); + filePicker->SetDefaultString(defaultDisplayString); + + // Check if the attachment file name has an extension (which must not + // contain spaces) and set it as the default extension for the attachment. + int32_t extensionIndex = defaultDisplayString.RFindChar('.'); + if (extensionIndex > 0 && + defaultDisplayString.FindChar(' ', extensionIndex) == kNotFound) { + nsString extension; + extension = Substring(defaultDisplayString, extensionIndex + 1); + filePicker->SetDefaultExtension(extension); + if (!mStringBundle) { + rv = InitStringBundle(); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsString filterName; + AutoTArray extensionParam = {extension}; + rv = mStringBundle->FormatStringFromName("saveAsType", extensionParam, + filterName); + NS_ENSURE_SUCCESS(rv, rv); + + extension.InsertLiteral(u"*.", 0); + filePicker->AppendFilter(filterName, extension); + } + + filePicker->AppendFilters(nsIFilePicker::filterAll); + + rv = GetLastSaveDirectory(getter_AddRefs(lastSaveDir)); + if (NS_SUCCEEDED(rv) && lastSaveDir) + filePicker->SetDisplayDirectory(lastSaveDir); + + rv = ShowPicker(filePicker, &dialogResult); + if (NS_FAILED(rv) || dialogResult == nsIFilePicker::returnCancel) return rv; + + rv = filePicker->GetFile(getter_AddRefs(localFile)); + NS_ENSURE_SUCCESS(rv, rv); + + SetLastSaveDirectory(localFile); + + PathString dirName = localFile->NativePath(); + + AutoTArray contentTypeArray = { + PromiseFlatCString(aContentType)}; + AutoTArray urlArray = {PromiseFlatCString(aURL)}; + AutoTArray displayNameArray = { + PromiseFlatCString(aDisplayName)}; + AutoTArray messageUriArray = {PromiseFlatCString(aMessageUri)}; + nsSaveAllAttachmentsState* saveState = new nsSaveAllAttachmentsState( + contentTypeArray, urlArray, displayNameArray, messageUriArray, + dirName.get(), detaching, nullptr); + + // SaveAttachment takes ownership of saveState. + return SaveAttachment(localFile, aURL, aMessageUri, aContentType, saveState, + nullptr); +} + +NS_IMETHODIMP +nsMessenger::SaveAllAttachments(const nsTArray& contentTypeArray, + const nsTArray& urlArray, + const nsTArray& displayNameArray, + const nsTArray& messageUriArray) { + uint32_t len = contentTypeArray.Length(); + NS_ENSURE_TRUE(urlArray.Length() == len, NS_ERROR_INVALID_ARG); + NS_ENSURE_TRUE(displayNameArray.Length() == len, NS_ERROR_INVALID_ARG); + NS_ENSURE_TRUE(messageUriArray.Length() == len, NS_ERROR_INVALID_ARG); + if (len == 0) { + return NS_OK; + } + return SaveAllAttachments(contentTypeArray, urlArray, displayNameArray, + messageUriArray, false); +} + +nsresult nsMessenger::SaveAllAttachments( + const nsTArray& contentTypeArray, + const nsTArray& urlArray, + const nsTArray& displayNameArray, + const nsTArray& messageUriArray, bool detaching) { + nsresult rv = NS_ERROR_OUT_OF_MEMORY; + nsCOMPtr filePicker = + do_CreateInstance("@mozilla.org/filepicker;1", &rv); + nsCOMPtr localFile; + nsCOMPtr lastSaveDir; + nsIFilePicker::ResultCode dialogResult; + nsString saveAttachmentStr; + + NS_ENSURE_SUCCESS(rv, rv); + if (detaching) { + GetString(u"DetachAllAttachments"_ns, saveAttachmentStr); + } else { + GetString(u"SaveAllAttachments"_ns, saveAttachmentStr); + } + filePicker->Init(mWindow, saveAttachmentStr, nsIFilePicker::modeGetFolder); + + rv = GetLastSaveDirectory(getter_AddRefs(lastSaveDir)); + if (NS_SUCCEEDED(rv) && lastSaveDir) + filePicker->SetDisplayDirectory(lastSaveDir); + + rv = ShowPicker(filePicker, &dialogResult); + if (NS_FAILED(rv) || dialogResult == nsIFilePicker::returnCancel) return rv; + + rv = filePicker->GetFile(getter_AddRefs(localFile)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = SetLastSaveDirectory(localFile); + NS_ENSURE_SUCCESS(rv, rv); + + PathString dirName = localFile->NativePath(); + + nsString unescapedName; + ConvertAndSanitizeFileName(displayNameArray[0], unescapedName); + rv = localFile->Append(unescapedName); + NS_ENSURE_SUCCESS(rv, rv); + + rv = PromptIfFileExists(localFile); + NS_ENSURE_SUCCESS(rv, rv); + + nsSaveAllAttachmentsState* saveState = new nsSaveAllAttachmentsState( + contentTypeArray, urlArray, displayNameArray, messageUriArray, + dirName.get(), detaching, nullptr); + // SaveAttachment takes ownership of saveState. + rv = SaveAttachment(localFile, urlArray[0], messageUriArray[0], + contentTypeArray[0], saveState, nullptr); + return rv; +} + +enum MESSENGER_SAVEAS_FILE_TYPE { + EML_FILE_TYPE = 0, + HTML_FILE_TYPE = 1, + TEXT_FILE_TYPE = 2, + ANY_FILE_TYPE = 3 +}; +#define HTML_FILE_EXTENSION ".htm" +#define HTML_FILE_EXTENSION2 ".html" +#define TEXT_FILE_EXTENSION ".txt" + +/** + * Adjust the file name, removing characters from the middle of the name if + * the name would otherwise be too long - too long for what file systems + * usually support. + */ +nsresult nsMessenger::AdjustFileIfNameTooLong(nsIFile* aFile) { + NS_ENSURE_ARG_POINTER(aFile); + nsAutoString path; + nsresult rv = aFile->GetPath(path); + NS_ENSURE_SUCCESS(rv, rv); + // Most common file systems have a max filename length of 255. On windows, the + // total path length is (at least for all practical purposees) limited to 255. + // Let's just don't allow paths longer than that elsewhere either for + // simplicity. + uint32_t MAX = 255; + if (path.Length() > MAX) { + nsAutoString leafName; + rv = aFile->GetLeafName(leafName); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t pathLengthUpToLeaf = path.Length() - leafName.Length(); + if (pathLengthUpToLeaf >= MAX - 8) { // want at least 8 chars for name + return NS_ERROR_FILE_NAME_TOO_LONG; + } + uint32_t x = MAX - pathLengthUpToLeaf; // x = max leaf size + nsAutoString truncatedLeaf; + truncatedLeaf.Append(Substring(leafName, 0, x / 2)); + truncatedLeaf.AppendLiteral("..."); + truncatedLeaf.Append( + Substring(leafName, leafName.Length() - x / 2 + 3, leafName.Length())); + rv = aFile->SetLeafName(truncatedLeaf); + } + return rv; +} + +NS_IMETHODIMP +nsMessenger::SaveAs(const nsACString& aURI, bool aAsFile, + nsIMsgIdentity* aIdentity, const nsAString& aMsgFilename, + bool aBypassFilePicker) { + nsCOMPtr messageService; + nsCOMPtr urlListener; + RefPtr saveListener; + nsCOMPtr convertedListener; + int32_t saveAsFileType = EML_FILE_TYPE; + + nsresult rv = GetMessageServiceFromURI(aURI, getter_AddRefs(messageService)); + if (NS_FAILED(rv)) goto done; + + if (aAsFile) { + nsCOMPtr saveAsFile; + // show the file picker if BypassFilePicker is not specified (null) or false + if (!aBypassFilePicker) { + rv = GetSaveAsFile(aMsgFilename, &saveAsFileType, + getter_AddRefs(saveAsFile)); + // A null saveAsFile means that the user canceled the save as + if (NS_FAILED(rv) || !saveAsFile) goto done; + } else { + saveAsFile = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv); + rv = saveAsFile->InitWithPath(aMsgFilename); + if (NS_FAILED(rv)) goto done; + if (StringEndsWith(aMsgFilename, + NS_LITERAL_STRING_FROM_CSTRING(TEXT_FILE_EXTENSION), + nsCaseInsensitiveStringComparator)) + saveAsFileType = TEXT_FILE_TYPE; + else if ((StringEndsWith( + aMsgFilename, + NS_LITERAL_STRING_FROM_CSTRING(HTML_FILE_EXTENSION), + nsCaseInsensitiveStringComparator)) || + (StringEndsWith( + aMsgFilename, + NS_LITERAL_STRING_FROM_CSTRING(HTML_FILE_EXTENSION2), + nsCaseInsensitiveStringComparator))) + saveAsFileType = HTML_FILE_TYPE; + else + saveAsFileType = EML_FILE_TYPE; + } + + rv = AdjustFileIfNameTooLong(saveAsFile); + NS_ENSURE_SUCCESS(rv, rv); + + rv = PromptIfFileExists(saveAsFile); + if (NS_FAILED(rv)) { + goto done; + } + + // After saveListener goes out of scope, the listener will be owned by + // whoever the listener is registered with, usually a URL. + saveListener = new nsSaveMsgListener(saveAsFile, this, nullptr); + rv = saveListener->QueryInterface(NS_GET_IID(nsIUrlListener), + getter_AddRefs(urlListener)); + if (NS_FAILED(rv)) goto done; + + if (saveAsFileType == EML_FILE_TYPE) { + nsCOMPtr dummyNull; + rv = messageService->SaveMessageToDisk( + aURI, saveAsFile, false, urlListener, getter_AddRefs(dummyNull), true, + mMsgWindow); + } else { + nsAutoCString urlString(aURI); + + // we can't go RFC822 to TXT until bug #1775 is fixed + // so until then, do the HTML to TXT conversion in + // nsSaveMsgListener::OnStopRequest(), see ConvertBufToPlainText() + // + // Setup the URL for a "Save As..." Operation... + // For now, if this is a save as TEXT operation, then do + // a "printing" operation + if (saveAsFileType == TEXT_FILE_TYPE) { + saveListener->m_outputFormat = nsSaveMsgListener::ePlainText; + saveListener->m_doCharsetConversion = true; + urlString.AppendLiteral("?header=print"); + } else { + saveListener->m_outputFormat = nsSaveMsgListener::eHTML; + saveListener->m_doCharsetConversion = false; + urlString.AppendLiteral("?header=saveas"); + } + + nsCOMPtr url; + rv = NS_NewURI(getter_AddRefs(url), urlString); + NS_ASSERTION(NS_SUCCEEDED(rv), "NS_NewURI failed"); + if (NS_FAILED(rv)) goto done; + + nsCOMPtr nullPrincipal = + NullPrincipal::CreateWithoutOriginAttributes(); + + saveListener->m_channel = nullptr; + rv = NS_NewInputStreamChannel( + getter_AddRefs(saveListener->m_channel), url, nullptr, nullPrincipal, + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_OTHER); + NS_ASSERTION(NS_SUCCEEDED(rv), "NS_NewChannel failed"); + if (NS_FAILED(rv)) goto done; + + nsCOMPtr streamConverterService = + do_GetService("@mozilla.org/streamConverters;1"); + nsCOMPtr channelSupport = + do_QueryInterface(saveListener->m_channel); + + // we can't go RFC822 to TXT until bug #1775 is fixed + // so until then, do the HTML to TXT conversion in + // nsSaveMsgListener::OnStopRequest(), see ConvertBufToPlainText() + rv = streamConverterService->AsyncConvertData( + MESSAGE_RFC822, TEXT_HTML, saveListener, channelSupport, + getter_AddRefs(convertedListener)); + NS_ASSERTION(NS_SUCCEEDED(rv), "AsyncConvertData failed"); + if (NS_FAILED(rv)) goto done; + + rv = messageService->LoadMessage(urlString, convertedListener, mMsgWindow, + nullptr, false); + } + } else { + // ** save as Template + nsCOMPtr tmpFile; + nsresult rv = GetSpecialDirectoryWithFileName(NS_OS_TEMP_DIR, "nsmail.tmp", + getter_AddRefs(tmpFile)); + + NS_ENSURE_SUCCESS(rv, rv); + + // For temp file, we should use restrictive 00600 instead of + // ATTACHMENT_PERMISSION + rv = tmpFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 00600); + if (NS_FAILED(rv)) goto done; + + // The saveListener is owned by whoever we ultimately register the + // listener with, generally a URL. + saveListener = new nsSaveMsgListener(tmpFile, this, nullptr); + + if (aIdentity) + rv = aIdentity->GetStationeryFolder(saveListener->m_templateUri); + if (NS_FAILED(rv)) goto done; + + bool needDummyHeader = + StringBeginsWith(saveListener->m_templateUri, "mailbox://"_ns); + bool canonicalLineEnding = + StringBeginsWith(saveListener->m_templateUri, "imap://"_ns); + + rv = saveListener->QueryInterface(NS_GET_IID(nsIUrlListener), + getter_AddRefs(urlListener)); + if (NS_FAILED(rv)) goto done; + + nsCOMPtr dummyNull; + rv = messageService->SaveMessageToDisk( + aURI, tmpFile, needDummyHeader, urlListener, getter_AddRefs(dummyNull), + canonicalLineEnding, mMsgWindow); + } + +done: + if (NS_FAILED(rv)) { + Alert("saveMessageFailed"); + } + return rv; +} + +nsresult nsMessenger::GetSaveAsFile(const nsAString& aMsgFilename, + int32_t* aSaveAsFileType, + nsIFile** aSaveAsFile) { + nsresult rv; + nsCOMPtr filePicker = + do_CreateInstance("@mozilla.org/filepicker;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsString saveMailAsStr; + GetString(u"SaveMailAs"_ns, saveMailAsStr); + filePicker->Init(mWindow, saveMailAsStr, nsIFilePicker::modeSave); + + // if we have a non-null filename use it, otherwise use default save message + // one + if (aMsgFilename.IsEmpty()) { + nsString saveMsgStr; + GetString(u"defaultSaveMessageAsFileName"_ns, saveMsgStr); + filePicker->SetDefaultString(saveMsgStr); + } else { + filePicker->SetDefaultString(aMsgFilename); + } + + // because we will be using GetFilterIndex() + // we must call AppendFilters() one at a time, + // in MESSENGER_SAVEAS_FILE_TYPE order + nsString emlFilesStr; + GetString(u"EMLFiles"_ns, emlFilesStr); + filePicker->AppendFilter(emlFilesStr, u"*.eml"_ns); + filePicker->AppendFilters(nsIFilePicker::filterHTML); + filePicker->AppendFilters(nsIFilePicker::filterText); + filePicker->AppendFilters(nsIFilePicker::filterAll); + + // Save as the "All Files" file type by default. We want to save as .eml by + // default, but the filepickers on some platforms don't switch extensions + // based on the file type selected (bug 508597). + filePicker->SetFilterIndex(ANY_FILE_TYPE); + // Yes, this is fine even if we ultimately save as HTML or text. On Windows, + // this actually is a boolean telling the file picker to automatically add + // the correct extension depending on the filter. On Mac or Linux this is a + // no-op. + filePicker->SetDefaultExtension(u"eml"_ns); + + nsIFilePicker::ResultCode dialogResult; + + nsCOMPtr lastSaveDir; + rv = GetLastSaveDirectory(getter_AddRefs(lastSaveDir)); + if (NS_SUCCEEDED(rv) && lastSaveDir) + filePicker->SetDisplayDirectory(lastSaveDir); + + nsCOMPtr localFile; + rv = ShowPicker(filePicker, &dialogResult); + NS_ENSURE_SUCCESS(rv, rv); + if (dialogResult == nsIFilePicker::returnCancel) { + // We'll indicate this by setting the outparam to null. + *aSaveAsFile = nullptr; + return NS_OK; + } + + rv = filePicker->GetFile(getter_AddRefs(localFile)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = SetLastSaveDirectory(localFile); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t selectedSaveAsFileType; + rv = filePicker->GetFilterIndex(&selectedSaveAsFileType); + NS_ENSURE_SUCCESS(rv, rv); + + // If All Files was selected, look at the extension + if (selectedSaveAsFileType == ANY_FILE_TYPE) { + nsAutoString fileName; + rv = localFile->GetLeafName(fileName); + NS_ENSURE_SUCCESS(rv, rv); + + if (StringEndsWith(fileName, + NS_LITERAL_STRING_FROM_CSTRING(HTML_FILE_EXTENSION), + nsCaseInsensitiveStringComparator) || + StringEndsWith(fileName, + NS_LITERAL_STRING_FROM_CSTRING(HTML_FILE_EXTENSION2), + nsCaseInsensitiveStringComparator)) + *aSaveAsFileType = HTML_FILE_TYPE; + else if (StringEndsWith(fileName, + NS_LITERAL_STRING_FROM_CSTRING(TEXT_FILE_EXTENSION), + nsCaseInsensitiveStringComparator)) + *aSaveAsFileType = TEXT_FILE_TYPE; + else + // The default is .eml + *aSaveAsFileType = EML_FILE_TYPE; + } else { + *aSaveAsFileType = selectedSaveAsFileType; + } + + if (dialogResult == nsIFilePicker::returnReplace) { + // be extra safe and only delete when the file is really a file + bool isFile; + rv = localFile->IsFile(&isFile); + if (NS_SUCCEEDED(rv) && isFile) { + rv = localFile->Remove(false /* recursive delete */); + NS_ENSURE_SUCCESS(rv, rv); + } else { + // We failed, or this isn't a file. We can't do anything about it. + return NS_ERROR_FAILURE; + } + } + + *aSaveAsFile = nullptr; + localFile.forget(aSaveAsFile); + return NS_OK; +} + +/** + * Show a Save All dialog allowing the user to pick which folder to save + * messages to. + * @param [out] aSaveDir directory to save to. Will be null on cancel. + */ +nsresult nsMessenger::GetSaveToDir(nsIFile** aSaveDir) { + nsresult rv; + nsCOMPtr filePicker = + do_CreateInstance("@mozilla.org/filepicker;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsString chooseFolderStr; + GetString(u"ChooseFolder"_ns, chooseFolderStr); + filePicker->Init(mWindow, chooseFolderStr, nsIFilePicker::modeGetFolder); + + nsCOMPtr lastSaveDir; + rv = GetLastSaveDirectory(getter_AddRefs(lastSaveDir)); + if (NS_SUCCEEDED(rv) && lastSaveDir) + filePicker->SetDisplayDirectory(lastSaveDir); + + nsIFilePicker::ResultCode dialogResult; + rv = ShowPicker(filePicker, &dialogResult); + if (NS_FAILED(rv) || dialogResult == nsIFilePicker::returnCancel) { + // We'll indicate this by setting the outparam to null. + *aSaveDir = nullptr; + return NS_OK; + } + + nsCOMPtr dir; + rv = filePicker->GetFile(getter_AddRefs(dir)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = SetLastSaveDirectory(dir); + NS_ENSURE_SUCCESS(rv, rv); + + *aSaveDir = nullptr; + dir.forget(aSaveDir); + return NS_OK; +} + +NS_IMETHODIMP +nsMessenger::SaveMessages(const nsTArray& aFilenameArray, + const nsTArray& aMessageUriArray) { + MOZ_ASSERT(aFilenameArray.Length() == aMessageUriArray.Length()); + + nsresult rv; + + nsCOMPtr saveDir; + rv = GetSaveToDir(getter_AddRefs(saveDir)); + NS_ENSURE_SUCCESS(rv, rv); + if (!saveDir) // A null saveDir means that the user canceled the save. + return NS_OK; + + for (uint32_t i = 0; i < aFilenameArray.Length(); i++) { + nsCOMPtr saveToFile = + do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = saveToFile->InitWithFile(saveDir); + NS_ENSURE_SUCCESS(rv, rv); + + rv = saveToFile->Append(aFilenameArray[i]); + NS_ENSURE_SUCCESS(rv, rv); + + rv = AdjustFileIfNameTooLong(saveToFile); + NS_ENSURE_SUCCESS(rv, rv); + + rv = PromptIfFileExists(saveToFile); + if (NS_FAILED(rv)) continue; + + nsCOMPtr messageService; + nsCOMPtr urlListener; + + rv = GetMessageServiceFromURI(aMessageUriArray[i], + getter_AddRefs(messageService)); + if (NS_FAILED(rv)) { + Alert("saveMessageFailed"); + return rv; + } + + RefPtr saveListener = + new nsSaveMsgListener(saveToFile, this, nullptr); + + rv = saveListener->QueryInterface(NS_GET_IID(nsIUrlListener), + getter_AddRefs(urlListener)); + if (NS_FAILED(rv)) { + Alert("saveMessageFailed"); + return rv; + } + + // Ok, now save the message. + nsCOMPtr dummyNull; + rv = messageService->SaveMessageToDisk( + aMessageUriArray[i], saveToFile, false, urlListener, + getter_AddRefs(dummyNull), true, mMsgWindow); + if (NS_FAILED(rv)) { + Alert("saveMessageFailed"); + return rv; + } + } + return rv; +} + +nsresult nsMessenger::Alert(const char* stringName) { + nsresult rv = NS_OK; + + if (mDocShell) { + nsCOMPtr dialog(do_GetInterface(mDocShell)); + + if (dialog) { + nsString alertStr; + GetString(NS_ConvertASCIItoUTF16(stringName), alertStr); + rv = dialog->Alert(nullptr, alertStr.get()); + } + } + return rv; +} + +NS_IMETHODIMP +nsMessenger::MsgHdrFromURI(const nsACString& aUri, nsIMsgDBHdr** aMsgHdr) { + NS_ENSURE_ARG_POINTER(aMsgHdr); + nsCOMPtr msgService; + nsresult rv; + + rv = GetMessageServiceFromURI(aUri, getter_AddRefs(msgService)); + NS_ENSURE_SUCCESS(rv, rv); + return msgService->MessageURIToMsgHdr(aUri, aMsgHdr); +} + +NS_IMETHODIMP nsMessenger::GetUndoTransactionType(uint32_t* txnType) { + NS_ENSURE_TRUE(txnType && mTxnMgr, NS_ERROR_NULL_POINTER); + + nsresult rv; + *txnType = nsMessenger::eUnknown; + nsCOMPtr txn; + rv = mTxnMgr->PeekUndoStack(getter_AddRefs(txn)); + if (NS_SUCCEEDED(rv) && txn) { + nsCOMPtr propertyBag = do_QueryInterface(txn, &rv); + NS_ENSURE_SUCCESS(rv, rv); + return propertyBag->GetPropertyAsUint32(u"type"_ns, txnType); + } + return rv; +} + +NS_IMETHODIMP nsMessenger::CanUndo(bool* bValue) { + NS_ENSURE_TRUE(bValue && mTxnMgr, NS_ERROR_NULL_POINTER); + + nsresult rv; + *bValue = false; + int32_t count = 0; + rv = mTxnMgr->GetNumberOfUndoItems(&count); + if (NS_SUCCEEDED(rv) && count > 0) *bValue = true; + return rv; +} + +NS_IMETHODIMP nsMessenger::GetRedoTransactionType(uint32_t* txnType) { + NS_ENSURE_TRUE(txnType && mTxnMgr, NS_ERROR_NULL_POINTER); + + nsresult rv; + *txnType = nsMessenger::eUnknown; + nsCOMPtr txn; + rv = mTxnMgr->PeekRedoStack(getter_AddRefs(txn)); + if (NS_SUCCEEDED(rv) && txn) { + nsCOMPtr propertyBag = do_QueryInterface(txn, &rv); + NS_ENSURE_SUCCESS(rv, rv); + return propertyBag->GetPropertyAsUint32(u"type"_ns, txnType); + } + return rv; +} + +NS_IMETHODIMP nsMessenger::CanRedo(bool* bValue) { + NS_ENSURE_TRUE(bValue && mTxnMgr, NS_ERROR_NULL_POINTER); + + nsresult rv; + *bValue = false; + int32_t count = 0; + rv = mTxnMgr->GetNumberOfRedoItems(&count); + if (NS_SUCCEEDED(rv) && count > 0) *bValue = true; + return rv; +} + +MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP +nsMessenger::Undo(nsIMsgWindow* msgWindow) { + nsresult rv = NS_OK; + if (mTxnMgr) { + int32_t numTxn = 0; + rv = mTxnMgr->GetNumberOfUndoItems(&numTxn); + if (NS_SUCCEEDED(rv) && numTxn > 0) { + nsCOMPtr txn; + rv = mTxnMgr->PeekUndoStack(getter_AddRefs(txn)); + if (NS_SUCCEEDED(rv) && txn) { + static_cast(static_cast(txn.get())) + ->SetMsgWindow(msgWindow); + } + nsCOMPtr txnMgr = mTxnMgr; + txnMgr->UndoTransaction(); + } + } + return rv; +} + +MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP +nsMessenger::Redo(nsIMsgWindow* msgWindow) { + nsresult rv = NS_OK; + if (mTxnMgr) { + int32_t numTxn = 0; + rv = mTxnMgr->GetNumberOfRedoItems(&numTxn); + if (NS_SUCCEEDED(rv) && numTxn > 0) { + nsCOMPtr txn; + rv = mTxnMgr->PeekRedoStack(getter_AddRefs(txn)); + if (NS_SUCCEEDED(rv) && txn) { + static_cast(static_cast(txn.get())) + ->SetMsgWindow(msgWindow); + } + nsCOMPtr txnMgr = mTxnMgr; + txnMgr->RedoTransaction(); + } + } + return rv; +} + +NS_IMETHODIMP +nsMessenger::GetTransactionManager(nsITransactionManager** aTxnMgr) { + NS_ENSURE_TRUE(mTxnMgr && aTxnMgr, NS_ERROR_NULL_POINTER); + NS_ADDREF(*aTxnMgr = mTxnMgr); + return NS_OK; +} + +nsSaveMsgListener::nsSaveMsgListener(nsIFile* aFile, nsMessenger* aMessenger, + nsIUrlListener* aListener) { + m_file = aFile; + m_messenger = aMessenger; + mListener = aListener; + mUrlHasStopped = false; + mRequestHasStopped = false; + + // rhp: for charset handling + m_doCharsetConversion = false; + m_saveAllAttachmentsState = nullptr; + mProgress = 0; + mMaxProgress = -1; + mCanceled = false; + m_outputFormat = eUnknown; + mInitialized = false; +} + +nsSaveMsgListener::~nsSaveMsgListener() {} + +// +// nsISupports +// +NS_IMPL_ISUPPORTS(nsSaveMsgListener, nsIUrlListener, nsIMsgCopyServiceListener, + nsIStreamListener, nsIRequestObserver, nsICancelable) + +NS_IMETHODIMP +nsSaveMsgListener::Cancel(nsresult status) { + mCanceled = true; + return NS_OK; +} + +// +// nsIUrlListener +// +NS_IMETHODIMP +nsSaveMsgListener::OnStartRunningUrl(nsIURI* url) { + if (mListener) mListener->OnStartRunningUrl(url); + return NS_OK; +} + +NS_IMETHODIMP +nsSaveMsgListener::OnStopRunningUrl(nsIURI* url, nsresult exitCode) { + nsresult rv = exitCode; + mUrlHasStopped = true; + + // ** save as template goes here + if (!m_templateUri.IsEmpty()) { + nsCOMPtr templateFolder; + rv = GetOrCreateFolder(m_templateUri, getter_AddRefs(templateFolder)); + if (NS_FAILED(rv)) goto done; + nsCOMPtr copyService = + do_GetService("@mozilla.org/messenger/messagecopyservice;1"); + if (copyService) { + nsCOMPtr clone; + m_file->Clone(getter_AddRefs(clone)); + rv = copyService->CopyFileMessage(clone, templateFolder, nullptr, true, + nsMsgMessageFlags::Read, EmptyCString(), + this, nullptr); + // Clear this so we don't end up in a loop if OnStopRunningUrl gets + // called again. + m_templateUri.Truncate(); + } + } else if (m_outputStream && mRequestHasStopped) { + m_outputStream->Close(); + m_outputStream = nullptr; + } + +done: + if (NS_FAILED(rv)) { + if (m_file) m_file->Remove(false); + if (m_messenger) m_messenger->Alert("saveMessageFailed"); + } + + if (mRequestHasStopped && mListener) + mListener->OnStopRunningUrl(url, exitCode); + else + mListenerUri = url; + + return rv; +} + +NS_IMETHODIMP +nsSaveMsgListener::OnStartCopy(void) { return NS_OK; } + +NS_IMETHODIMP +nsSaveMsgListener::OnProgress(uint32_t aProgress, uint32_t aProgressMax) { + return NS_OK; +} + +NS_IMETHODIMP +nsSaveMsgListener::SetMessageKey(nsMsgKey aKey) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsSaveMsgListener::GetMessageId(nsACString& aMessageId) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsSaveMsgListener::OnStopCopy(nsresult aStatus) { + if (m_file) m_file->Remove(false); + return aStatus; +} + +// initializes the progress window if we are going to show one +// and for OSX, sets creator flags on the output file +nsresult nsSaveMsgListener::InitializeDownload(nsIRequest* aRequest) { + nsresult rv = NS_OK; + + mInitialized = true; + nsCOMPtr channel(do_QueryInterface(aRequest)); + + if (!channel) return rv; + + // Get the max progress from the URL if we haven't already got it. + if (mMaxProgress == -1) { + nsCOMPtr uri; + channel->GetURI(getter_AddRefs(uri)); + nsCOMPtr mailnewsUrl(do_QueryInterface(uri)); + if (mailnewsUrl) mailnewsUrl->GetMaxProgress(&mMaxProgress); + } + + if (!m_contentType.IsEmpty()) { + nsCOMPtr mimeService( + do_GetService(NS_MIMESERVICE_CONTRACTID)); + nsCOMPtr mimeinfo; + + mimeService->GetFromTypeAndExtension(m_contentType, EmptyCString(), + getter_AddRefs(mimeinfo)); + + // create a download progress window + + // Set saveToDisk explicitly to avoid launching the saved file. + // See + // https://hg.mozilla.org/mozilla-central/file/814a6f071472/toolkit/components/jsdownloads/src/DownloadLegacy.js#l164 + mimeinfo->SetPreferredAction(nsIHandlerInfo::saveToDisk); + + // When we don't allow warnings, also don't show progress, as this + // is an environment (typically filters) where we don't want + // interruption. + bool allowProgress = true; + if (m_saveAllAttachmentsState) + allowProgress = !m_saveAllAttachmentsState->m_withoutWarning; + if (allowProgress) { + nsCOMPtr tr = do_CreateInstance(NS_TRANSFER_CONTRACTID, &rv); + if (tr && m_file) { + PRTime timeDownloadStarted = PR_Now(); + + nsCOMPtr outputURI; + NS_NewFileURI(getter_AddRefs(outputURI), m_file); + + nsCOMPtr url; + channel->GetURI(getter_AddRefs(url)); + rv = tr->Init(url, nullptr, outputURI, EmptyString(), mimeinfo, + timeDownloadStarted, nullptr, this, false, + nsITransfer::DOWNLOAD_ACCEPTABLE, nullptr, false); + + // now store the web progresslistener + mTransfer = tr; + } + } + } + return rv; +} + +NS_IMETHODIMP +nsSaveMsgListener::OnStartRequest(nsIRequest* request) { + if (m_file) + MsgNewBufferedFileOutputStream(getter_AddRefs(m_outputStream), m_file, -1, + ATTACHMENT_PERMISSION); + if (!m_outputStream) { + mCanceled = true; + if (m_messenger) m_messenger->Alert("saveAttachmentFailed"); + } + return NS_OK; +} + +NS_IMETHODIMP +nsSaveMsgListener::OnStopRequest(nsIRequest* request, nsresult status) { + nsresult rv = NS_OK; + mRequestHasStopped = true; + + // rhp: If we are doing the charset conversion magic, this is different + // processing, otherwise, its just business as usual. + // If we need text/plain, then we need to convert the HTML and then convert + // to the systems charset. + if (m_doCharsetConversion && m_outputStream) { + // For HTML, code is emitted immediately in OnDataAvailable. + MOZ_ASSERT(m_outputFormat == ePlainText, + "For HTML, m_doCharsetConversion shouldn't be set"); + NS_ConvertUTF8toUTF16 utf16Buffer(m_msgBuffer); + ConvertBufToPlainText(utf16Buffer, false, false, false); + + nsCString outCString; + // NS_CopyUnicodeToNative() doesn't return an error, so we have no choice + // but to always use UTF-8. + CopyUTF16toUTF8(utf16Buffer, outCString); + uint32_t writeCount; + rv = m_outputStream->Write(outCString.get(), outCString.Length(), + &writeCount); + if (outCString.Length() != writeCount) rv = NS_ERROR_FAILURE; + } + + if (m_outputStream) { + m_outputStream->Close(); + m_outputStream = nullptr; + } + + // Are there more attachments to deal with? + nsSaveAllAttachmentsState* state = m_saveAllAttachmentsState; + if (state) { + state->m_curIndex++; + if (!mCanceled && state->m_curIndex < state->m_count) { + // Yes, start on the next attachment. + uint32_t i = state->m_curIndex; + nsString unescapedName; + RefPtr localFile = + new nsLocalFile(nsTDependentString(state->m_directoryName)); + if (localFile->NativePath().IsEmpty()) { + rv = NS_ERROR_FAILURE; + goto done; + } + + ConvertAndSanitizeFileName(state->m_displayNameArray[i], unescapedName); + rv = localFile->Append(unescapedName); + if (NS_FAILED(rv)) goto done; + + // When we are running with no warnings (typically filters and other + // automatic uses), then don't prompt for duplicates, but create a unique + // file instead. + if (!state->m_withoutWarning) { + rv = m_messenger->PromptIfFileExists(localFile); + if (NS_FAILED(rv)) goto done; + } else { + rv = localFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, + ATTACHMENT_PERMISSION); + if (NS_FAILED(rv)) goto done; + } + // Start the next attachment saving. + // NOTE: null listener passed in on subsequent saves! The original + // listener will already have been invoked. + // See Bug 1789565 + rv = m_messenger->SaveAttachment( + localFile, state->m_urlArray[i], state->m_messageUriArray[i], + state->m_contentTypeArray[i], state, nullptr); + if (NS_FAILED(rv)) { + // If SaveAttachment() fails, state will have been deleted, and + // m_overallListener->OnStopRunningUrl() will have been called. + state = nullptr; + m_saveAllAttachmentsState = nullptr; + } + done: + if (NS_FAILED(rv) && state) { + if (state->m_overallListener) { + state->m_overallListener->OnStopRunningUrl(nullptr, rv); + } + delete state; + m_saveAllAttachmentsState = nullptr; + } + } else { + // All attachments have been saved. + if (state->m_overallListener) { + state->m_overallListener->OnStopRunningUrl( + nullptr, mCanceled ? NS_ERROR_FAILURE : NS_OK); + } + // Check if we're supposed to be detaching attachments after saving them. + if (state->m_detachingAttachments && !mCanceled) { + m_messenger->DetachAttachments( + state->m_contentTypeArray, state->m_urlArray, + state->m_displayNameArray, state->m_messageUriArray, + &state->m_savedFiles, nullptr, state->m_withoutWarning); + } + delete m_saveAllAttachmentsState; + m_saveAllAttachmentsState = nullptr; + } + } + + if (mTransfer) { + mTransfer->OnProgressChange64(nullptr, nullptr, mMaxProgress, mMaxProgress, + mMaxProgress, mMaxProgress); + mTransfer->OnStateChange(nullptr, nullptr, + nsIWebProgressListener::STATE_STOP | + nsIWebProgressListener::STATE_IS_NETWORK, + NS_OK); + mTransfer = nullptr; // break any circular dependencies between the + // progress dialog and use + } + + if (mUrlHasStopped && mListener) + mListener->OnStopRunningUrl(mListenerUri, rv); + + return NS_OK; +} + +NS_IMETHODIMP +nsSaveMsgListener::OnDataAvailable(nsIRequest* request, + nsIInputStream* inStream, uint64_t srcOffset, + uint32_t count) { + nsresult rv = NS_ERROR_FAILURE; + // first, check to see if we've been canceled.... + if (mCanceled) // then go cancel our underlying channel too + return request->Cancel(NS_BINDING_ABORTED); + + if (!mInitialized) InitializeDownload(request); + + if (m_outputStream) { + mProgress += count; + uint64_t available; + uint32_t readCount, maxReadCount = sizeof(m_dataBuffer); + uint32_t writeCount; + rv = inStream->Available(&available); + while (NS_SUCCEEDED(rv) && available) { + if (maxReadCount > available) maxReadCount = (uint32_t)available; + rv = inStream->Read(m_dataBuffer, maxReadCount, &readCount); + + // rhp: + // Ok, now we do one of two things. If we are sending out HTML, then + // just write it to the HTML stream as it comes along...but if this is + // a save as TEXT operation, we need to buffer this up for conversion + // when we are done. When the stream converter for HTML-TEXT gets in + // place, this magic can go away. + // + if (NS_SUCCEEDED(rv)) { + if ((m_doCharsetConversion) && (m_outputFormat == ePlainText)) + m_msgBuffer.Append(Substring(m_dataBuffer, m_dataBuffer + readCount)); + else + rv = m_outputStream->Write(m_dataBuffer, readCount, &writeCount); + + available -= readCount; + } + } + + if (NS_SUCCEEDED(rv) && mTransfer) // Send progress notification. + mTransfer->OnProgressChange64(nullptr, request, mProgress, mMaxProgress, + mProgress, mMaxProgress); + } + return rv; +} + +#define MESSENGER_STRING_URL "chrome://messenger/locale/messenger.properties" + +nsresult nsMessenger::InitStringBundle() { + if (mStringBundle) return NS_OK; + + const char propertyURL[] = MESSENGER_STRING_URL; + nsCOMPtr sBundleService = + mozilla::components::StringBundle::Service(); + NS_ENSURE_TRUE(sBundleService, NS_ERROR_UNEXPECTED); + return sBundleService->CreateBundle(propertyURL, + getter_AddRefs(mStringBundle)); +} + +void nsMessenger::GetString(const nsString& aStringName, nsString& aValue) { + nsresult rv; + aValue.Truncate(); + + if (!mStringBundle) InitStringBundle(); + + if (mStringBundle) + rv = mStringBundle->GetStringFromName( + NS_ConvertUTF16toUTF8(aStringName).get(), aValue); + else + rv = NS_ERROR_FAILURE; + + if (NS_FAILED(rv) || aValue.IsEmpty()) aValue = aStringName; + return; +} + +nsSaveAllAttachmentsState::nsSaveAllAttachmentsState( + const nsTArray& contentTypeArray, + const nsTArray& urlArray, + const nsTArray& displayNameArray, + const nsTArray& messageUriArray, const PathChar* dirName, + bool detachingAttachments, nsIUrlListener* overallListener) + : m_contentTypeArray(contentTypeArray.Clone()), + m_urlArray(urlArray.Clone()), + m_displayNameArray(displayNameArray.Clone()), + m_messageUriArray(messageUriArray.Clone()), + m_detachingAttachments(detachingAttachments), + m_overallListener(overallListener), + m_withoutWarning(false) { + m_count = contentTypeArray.Length(); + m_curIndex = 0; + m_directoryName = NS_xstrdup(dirName); +} + +nsSaveAllAttachmentsState::~nsSaveAllAttachmentsState() { + free(m_directoryName); +} + +nsresult nsMessenger::GetLastSaveDirectory(nsIFile** aLastSaveDir) { + nsresult rv; + nsCOMPtr prefBranch = + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // this can fail, and it will, on the first time we call it, as there is no + // default for this pref. + nsCOMPtr localFile; + rv = prefBranch->GetComplexValue(MESSENGER_SAVE_DIR_PREF_NAME, + NS_GET_IID(nsIFile), + getter_AddRefs(localFile)); + if (NS_SUCCEEDED(rv)) localFile.forget(aLastSaveDir); + return rv; +} + +nsresult nsMessenger::SetLastSaveDirectory(nsIFile* aLocalFile) { + NS_ENSURE_ARG_POINTER(aLocalFile); + nsresult rv; + nsCOMPtr prefBranch = + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // if the file is a directory, just use it for the last dir chosen + // otherwise, use the parent of the file as the last dir chosen. + // IsDirectory() will return error on saving a file, as the + // file doesn't exist yet. + bool isDirectory; + rv = aLocalFile->IsDirectory(&isDirectory); + if (NS_SUCCEEDED(rv) && isDirectory) { + rv = prefBranch->SetComplexValue(MESSENGER_SAVE_DIR_PREF_NAME, + NS_GET_IID(nsIFile), aLocalFile); + NS_ENSURE_SUCCESS(rv, rv); + } else { + nsCOMPtr parent; + rv = aLocalFile->GetParent(getter_AddRefs(parent)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = prefBranch->SetComplexValue(MESSENGER_SAVE_DIR_PREF_NAME, + NS_GET_IID(nsIFile), parent); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + +NS_IMETHODIMP +nsMessenger::FormatFileSize(uint64_t aSize, bool aUseKB, + nsAString& aFormattedSize) { + return ::FormatFileSize(aSize, aUseKB, aFormattedSize); +} + +/////////////////////////////////////////////////////////////////////////////// +// Detach/Delete Attachments +/////////////////////////////////////////////////////////////////////////////// + +static const char* GetAttachmentPartId(const char* aAttachmentUrl) { + static const char partIdPrefix[] = "part="; + const char* partId = PL_strstr(aAttachmentUrl, partIdPrefix); + return partId ? (partId + sizeof(partIdPrefix) - 1) : nullptr; +} + +static int CompareAttachmentPartId(const char* aAttachUrlLeft, + const char* aAttachUrlRight) { + // part ids are numbers separated by periods, like "1.2.3.4". + // we sort by doing a numerical comparison on each item in turn. e.g. "1.4" < + // "1.25" shorter entries come before longer entries. e.g. "1.4" < "1.4.1.2" + // return values: + // -2 left is a parent of right + // -1 left is less than right + // 0 left == right + // 1 right is greater than left + // 2 right is a parent of left + + const char* partIdLeft = GetAttachmentPartId(aAttachUrlLeft); + const char* partIdRight = GetAttachmentPartId(aAttachUrlRight); + + // for detached attachments the URL does not contain any "part=xx" + if (!partIdLeft) partIdLeft = "0"; + + if (!partIdRight) partIdRight = "0"; + + long idLeft, idRight; + do { + MOZ_ASSERT(partIdLeft && IS_DIGIT(*partIdLeft), + "Invalid character in part id string"); + MOZ_ASSERT(partIdRight && IS_DIGIT(*partIdRight), + "Invalid character in part id string"); + + // if the part numbers are different then the numerically smaller one is + // first + char* fixConstLoss; + idLeft = strtol(partIdLeft, &fixConstLoss, 10); + partIdLeft = fixConstLoss; + idRight = strtol(partIdRight, &fixConstLoss, 10); + partIdRight = fixConstLoss; + if (idLeft != idRight) return idLeft < idRight ? -1 : 1; + + // if one part id is complete but the other isn't, then the shortest one + // is first (parents before children) + if (*partIdLeft != *partIdRight) return *partIdRight ? -2 : 2; + + // if both part ids are complete (*partIdLeft == *partIdRight now) then + // they are equal + if (!*partIdLeft) return 0; + + MOZ_ASSERT(*partIdLeft == '.', "Invalid character in part id string"); + MOZ_ASSERT(*partIdRight == '.', "Invalid character in part id string"); + + ++partIdLeft; + ++partIdRight; + } while (true); +} + +// ------------------------------------ + +// struct on purpose -> show that we don't ever want a vtable +struct msgAttachment { + msgAttachment(const nsACString& aContentType, const nsACString& aUrl, + const nsACString& aDisplayName, const nsACString& aMessageUri) + : mContentType(aContentType), + mUrl(aUrl), + mDisplayName(aDisplayName), + mMessageUri(aMessageUri) {} + + nsCString mContentType; + nsCString mUrl; + nsCString mDisplayName; + nsCString mMessageUri; +}; + +// ------------------------------------ + +class nsAttachmentState { + public: + nsAttachmentState(); + nsresult Init(const nsTArray& aContentTypeArray, + const nsTArray& aUrlArray, + const nsTArray& aDisplayNameArray, + const nsTArray& aMessageUriArray); + nsresult PrepareForAttachmentDelete(); + + private: + static int CompareAttachmentsByPartId(const void* aLeft, const void* aRight); + + public: + uint32_t mCurIndex; + nsTArray mAttachmentArray; +}; + +nsAttachmentState::nsAttachmentState() : mCurIndex(0) {} + +nsresult nsAttachmentState::Init(const nsTArray& aContentTypeArray, + const nsTArray& aUrlArray, + const nsTArray& aDisplayNameArray, + const nsTArray& aMessageUriArray) { + MOZ_ASSERT(aContentTypeArray.Length() > 0); + MOZ_ASSERT(aContentTypeArray.Length() == aUrlArray.Length() && + aUrlArray.Length() == aDisplayNameArray.Length() && + aDisplayNameArray.Length() == aMessageUriArray.Length()); + + uint32_t count = aContentTypeArray.Length(); + mCurIndex = 0; + mAttachmentArray.Clear(); + mAttachmentArray.SetCapacity(count); + + for (uint32_t u = 0; u < count; ++u) { + mAttachmentArray.AppendElement( + msgAttachment(aContentTypeArray[u], aUrlArray[u], aDisplayNameArray[u], + aMessageUriArray[u])); + } + + return NS_OK; +} + +nsresult nsAttachmentState::PrepareForAttachmentDelete() { + // this must be called before any processing + if (mCurIndex != 0) return NS_ERROR_FAILURE; + + // this prepares the attachment list for use in deletion. In order to prepare, + // we sort the attachments in numerical ascending order on their part id, + // remove all duplicates and remove any subparts which will be removed + // automatically by the removal of the parent. + // + // e.g. the attachment list processing (showing only part ids) + // before: 1.11, 1.3, 1.2, 1.2.1.3, 1.4.1.2 + // sorted: 1.2, 1.2.1.3, 1.3, 1.4.1.2, 1.11 + // after: 1.2, 1.3, 1.4.1.2, 1.11 + + // sort + qsort(mAttachmentArray.Elements(), mAttachmentArray.Length(), + sizeof(msgAttachment), CompareAttachmentsByPartId); + + // remove duplicates and sub-items + int nCompare; + for (uint32_t u = 1; u < mAttachmentArray.Length();) { + nCompare = ::CompareAttachmentPartId(mAttachmentArray[u - 1].mUrl.get(), + mAttachmentArray[u].mUrl.get()); + if (nCompare == 0 || + nCompare == -2) // [u-1] is the same as or a parent of [u] + { + // shuffle the array down (and thus keeping the sorted order) + mAttachmentArray.RemoveElementAt(u); + } else { + ++u; + } + } + + return NS_OK; +} + +// Static compare callback for sorting. +int nsAttachmentState::CompareAttachmentsByPartId(const void* aLeft, + const void* aRight) { + msgAttachment& attachLeft = *((msgAttachment*)aLeft); + msgAttachment& attachRight = *((msgAttachment*)aRight); + return ::CompareAttachmentPartId(attachLeft.mUrl.get(), + attachRight.mUrl.get()); +} + +// ------------------------------------ +// Helper class to coordinate deleting attachments from a message. +// +// Implementation notes: +// The basic technique is to use nsIMsgMessageService.streamMessage() to +// stream the message through a streamconverter which is set up to strip +// out the attachments. The result is written out to a temporary file, +// which is then copied over the old message using +// nsIMsgCopyService.copyFileMessage() and the old message deleted with +// nsIMsgFolder.deleteMessages(). Phew. +// +// The nsIStreamListener, nsIUrlListener and nsIMsgCopyServiceListener +// inheritances here are just unfortunately-exposed implementation details. +// And they are a bit of a mess. Some are used multiple times, for different +// phases of the operation. So we use m_state to keep track. +class AttachmentDeleter : public nsIStreamListener, + public nsIUrlListener, + public nsIMsgCopyServiceListener { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSIURLLISTENER + NS_DECL_NSIMSGCOPYSERVICELISTENER + + public: + AttachmentDeleter(); + nsresult StartProcessing(nsMessenger* aMessenger, nsIMsgWindow* aMsgWindow, + nsAttachmentState* aAttach, bool aSaveFirst); + + public: + nsAttachmentState* mAttach; // list of attachments to process + bool mSaveFirst; // detach (true) or delete (false) + nsCOMPtr mMsgFile; // temporary file (processed mail) + nsCOMPtr mMsgFileStream; // temporary file (processed mail) + nsCOMPtr mMessageService; // original message service + nsCOMPtr mOriginalMessage; // original message header + nsCOMPtr mMessageFolder; // original message folder + nsCOMPtr mMessenger; // our messenger instance + nsCOMPtr mMsgWindow; // our UI window + nsMsgKey mOriginalMessageKey; // old message key + nsMsgKey mNewMessageKey; // new message key + uint32_t mOrigMsgFlags; + + enum { + eStarting, + eCopyingNewMsg, + eUpdatingFolder, // for IMAP + eDeletingOldMessage, + eSelectingNewMessage + } m_state; + // temp + nsTArray mDetachedFileUris; + + // The listener to invoke when the full operation is complete. + nsCOMPtr mListener; + + private: + nsresult InternalStartProcessing(nsMessenger* aMessenger, + nsIMsgWindow* aMsgWindow, + nsAttachmentState* aAttach, bool aSaveFirst); + nsresult DeleteOriginalMessage(); + virtual ~AttachmentDeleter(); +}; + +// +// nsISupports +// +NS_IMPL_ISUPPORTS(AttachmentDeleter, nsIStreamListener, nsIRequestObserver, + nsIUrlListener, nsIMsgCopyServiceListener) + +// +// nsIRequestObserver +// +NS_IMETHODIMP +AttachmentDeleter::OnStartRequest(nsIRequest* aRequest) { + // called when we start processing the StreamMessage request. + // This is called after OnStartRunningUrl(). + return NS_OK; +} + +NS_IMETHODIMP +AttachmentDeleter::OnStopRequest(nsIRequest* aRequest, nsresult aStatusCode) { + // called when we have completed processing the StreamMessage request. + // This is called before OnStopRunningUrl(). This means that we have now + // received all data of the message and we have completed processing. + // We now start to copy the processed message from the temporary file + // back into the message store, replacing the original message. + + mMessageFolder->CopyDataDone(); + if (NS_FAILED(aStatusCode)) return aStatusCode; + + // copy the file back into the folder. Note: setting msgToReplace only copies + // metadata, so we do the delete ourselves + nsCOMPtr listenerCopyService; + nsresult rv = this->QueryInterface(NS_GET_IID(nsIMsgCopyServiceListener), + getter_AddRefs(listenerCopyService)); + NS_ENSURE_SUCCESS(rv, rv); + + mMsgFileStream->Close(); + mMsgFileStream = nullptr; + mNewMessageKey = nsMsgKey_None; + nsCOMPtr copyService = + do_GetService("@mozilla.org/messenger/messagecopyservice;1"); + m_state = eCopyingNewMsg; + // clone file because nsIFile on Windows caches the wrong file size. + nsCOMPtr clone; + mMsgFile->Clone(getter_AddRefs(clone)); + if (copyService) { + nsCString originalKeys; + mOriginalMessage->GetStringProperty("keywords", originalKeys); + rv = copyService->CopyFileMessage(clone, mMessageFolder, mOriginalMessage, + false, mOrigMsgFlags, originalKeys, + listenerCopyService, mMsgWindow); + } + return rv; +} + +// +// nsIStreamListener +// + +NS_IMETHODIMP +AttachmentDeleter::OnDataAvailable(nsIRequest* aRequest, + nsIInputStream* aInStream, + uint64_t aSrcOffset, uint32_t aCount) { + if (!mMsgFileStream) return NS_ERROR_NULL_POINTER; + return mMessageFolder->CopyDataToOutputStreamForAppend(aInStream, aCount, + mMsgFileStream); +} + +// +// nsIUrlListener +// + +NS_IMETHODIMP +AttachmentDeleter::OnStartRunningUrl(nsIURI* aUrl) { + // called when we start processing the StreamMessage request. This is + // called before OnStartRequest(). + return NS_OK; +} + +nsresult AttachmentDeleter::DeleteOriginalMessage() { + nsCOMPtr listenerCopyService; + QueryInterface(NS_GET_IID(nsIMsgCopyServiceListener), + getter_AddRefs(listenerCopyService)); + + mOriginalMessage->SetUint32Property("attachmentDetached", 1); + RefPtr doomed(mOriginalMessage); + mOriginalMessage = nullptr; + m_state = eDeletingOldMessage; + return mMessageFolder->DeleteMessages({doomed}, // messages + mMsgWindow, // msgWindow + true, // deleteStorage + false, // isMove + listenerCopyService, // listener + false); // allowUndo +} + +// This is called (potentially) multiple times. +// Firstly, as a result of StreamMessage() (when the message is being passed +// through a streamconverter to strip the attachments). +// Secondly, after the DeleteMessages() call. But maybe not for IMAP? +// Maybe also after CopyFileMessage()? Gah. +NS_IMETHODIMP +AttachmentDeleter::OnStopRunningUrl(nsIURI* aUrl, nsresult aExitCode) { + nsresult rv = NS_OK; + if (mOriginalMessage && m_state == eUpdatingFolder) + rv = DeleteOriginalMessage(); + + return rv; +} + +// +// nsIMsgCopyServiceListener +// + +NS_IMETHODIMP +AttachmentDeleter::OnStartCopy(void) { + // never called? + return NS_OK; +} + +NS_IMETHODIMP +AttachmentDeleter::OnProgress(uint32_t aProgress, uint32_t aProgressMax) { + // never called? + return NS_OK; +} + +NS_IMETHODIMP +AttachmentDeleter::SetMessageKey(nsMsgKey aKey) { + // called during the copy of the modified message back into the message + // store to notify us of the message key of the newly created message. + mNewMessageKey = aKey; + + nsCString folderURI; + nsresult rv = mMessageFolder->GetURI(folderURI); + NS_ENSURE_SUCCESS(rv, rv); + + mozilla::JSONStringWriteFunc jsonString; + mozilla::JSONWriter data(jsonString); + data.Start(); + data.IntProperty("oldMessageKey", mOriginalMessageKey); + data.IntProperty("newMessageKey", aKey); + data.StringProperty("folderURI", folderURI); + data.End(); + + nsCOMPtr obs = services::GetObserverService(); + if (obs) { + obs->NotifyObservers(nullptr, "attachment-delete-msgkey-changed", + NS_ConvertUTF8toUTF16(jsonString.StringCRef()).get()); + } + return NS_OK; +} + +NS_IMETHODIMP +AttachmentDeleter::GetMessageId(nsACString& aMessageId) { + // never called? + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +AttachmentDeleter::OnStopCopy(nsresult aStatus) { + // This is called via `CopyFileMessage()` and `DeleteMessages()`. + // `m_state` tells us which callback it is. + if (m_state == eDeletingOldMessage) { + m_state = eSelectingNewMessage; + + // OK... that's it. The entire operation is now done. + // (there may still be another call to OnStopRunningUrl(), but that'll be + // a no-op in this state). + if (mListener) { + mListener->OnStopRunningUrl(nullptr, aStatus); + } + return NS_OK; + } + + // For non-IMAP messages, the original is deleted here, for IMAP messages + // that happens in `OnStopRunningUrl()` which isn't called for non-IMAP + // messages. + const nsACString& messageUri = mAttach->mAttachmentArray[0].mMessageUri; + if (mOriginalMessage && + !Substring(messageUri, 0, 13).EqualsLiteral("imap-message:")) { + return DeleteOriginalMessage(); + } else { + // Arrange for the message to be deleted in the next `OnStopRunningUrl()` + // call. + m_state = eUpdatingFolder; + } + + return NS_OK; +} + +// +// local methods +// + +AttachmentDeleter::AttachmentDeleter() + : mAttach(nullptr), + mSaveFirst(false), + mOriginalMessageKey(nsMsgKey_None), + mNewMessageKey(nsMsgKey_None), + mOrigMsgFlags(0), + m_state(eStarting) {} + +AttachmentDeleter::~AttachmentDeleter() { + if (mAttach) { + delete mAttach; + } + if (mMsgFileStream) { + mMsgFileStream->Close(); + mMsgFileStream = nullptr; + } + if (mMsgFile) { + mMsgFile->Remove(false); + } +} + +nsresult AttachmentDeleter::StartProcessing(nsMessenger* aMessenger, + nsIMsgWindow* aMsgWindow, + nsAttachmentState* aAttach, + bool detaching) { + if (mListener) { + mListener->OnStartRunningUrl(nullptr); + } + + nsresult rv = + InternalStartProcessing(aMessenger, aMsgWindow, aAttach, detaching); + if (NS_FAILED(rv)) { + if (mListener) { + mListener->OnStopRunningUrl(nullptr, rv); + } + } + return rv; +} + +nsresult AttachmentDeleter::InternalStartProcessing(nsMessenger* aMessenger, + nsIMsgWindow* aMsgWindow, + nsAttachmentState* aAttach, + bool detaching) { + aMessenger->QueryInterface(NS_GET_IID(nsIMessenger), + getter_AddRefs(mMessenger)); + mMsgWindow = aMsgWindow; + mAttach = aAttach; + + nsresult rv; + + // all attachments refer to the same message + const nsCString& messageUri = mAttach->mAttachmentArray[0].mMessageUri; + + // get the message service, original message and folder for this message + rv = GetMessageServiceFromURI(messageUri, getter_AddRefs(mMessageService)); + NS_ENSURE_SUCCESS(rv, rv); + rv = mMessageService->MessageURIToMsgHdr(messageUri, + getter_AddRefs(mOriginalMessage)); + NS_ENSURE_SUCCESS(rv, rv); + rv = mOriginalMessage->GetMessageKey(&mOriginalMessageKey); + NS_ENSURE_SUCCESS(rv, rv); + rv = mOriginalMessage->GetFolder(getter_AddRefs(mMessageFolder)); + NS_ENSURE_SUCCESS(rv, rv); + mOriginalMessage->GetFlags(&mOrigMsgFlags); + + // ensure that we can store and delete messages in this folder, if we + // can't then we can't do attachment deleting + bool canDelete = false; + mMessageFolder->GetCanDeleteMessages(&canDelete); + bool canFile = false; + mMessageFolder->GetCanFileMessages(&canFile); + if (!canDelete || !canFile) return NS_ERROR_FAILURE; + + // create an output stream on a temporary file. This stream will save the + // modified message data to a file which we will later use to replace the + // existing message. The file is removed in the destructor. + rv = GetSpecialDirectoryWithFileName(NS_OS_TEMP_DIR, "nsmail.tmp", + getter_AddRefs(mMsgFile)); + NS_ENSURE_SUCCESS(rv, rv); + + // For temp file, we should use restrictive 00600 instead of + // ATTACHMENT_PERMISSION + rv = mMsgFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 00600); + NS_ENSURE_SUCCESS(rv, rv); + + rv = MsgNewBufferedFileOutputStream(getter_AddRefs(mMsgFileStream), mMsgFile, + -1, ATTACHMENT_PERMISSION); + + // Create the additional header for data conversion. This will tell the stream + // converter which MIME emitter we want to use, and it will tell the MIME + // emitter which attachments should be deleted. + // It also supplies the path of the already-saved attachments, so that + // path can be noted in the message, where those attachements are removed. + // The X-Mozilla-External-Attachment-URL header will be added, with the + // location of the saved attachment. + const char* partId; + const char* nextField; + nsAutoCString sHeader("attach&del="); + nsAutoCString detachToHeader("&detachTo="); + for (uint32_t u = 0; u < mAttach->mAttachmentArray.Length(); ++u) { + if (u > 0) { + sHeader.Append(','); + if (detaching) detachToHeader.Append(','); + } + partId = GetAttachmentPartId(mAttach->mAttachmentArray[u].mUrl.get()); + if (partId) { + nextField = PL_strchr(partId, '&'); + sHeader.Append(partId, nextField ? nextField - partId : -1); + } + if (detaching) { + // The URI can contain commas, so percent-encode those first. + nsAutoCString uri(mDetachedFileUris[u]); + int ind = uri.FindChar(','); + while (ind != kNotFound) { + uri.Replace(ind, 1, "%2C"); + ind = uri.FindChar(','); + } + detachToHeader.Append(uri); + } + } + + if (detaching) sHeader.Append(detachToHeader); + // stream this message to our listener converting it via the attachment mime + // converter. The listener will just write the converted message straight to + // disk. + nsCOMPtr listenerSupports; + rv = this->QueryInterface(NS_GET_IID(nsISupports), + getter_AddRefs(listenerSupports)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr listenerUrlListener = + do_QueryInterface(listenerSupports, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr dummyNull; + rv = mMessageService->StreamMessage(messageUri, listenerSupports, mMsgWindow, + listenerUrlListener, true, sHeader, false, + getter_AddRefs(dummyNull)); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +// ------------------------------------ + +NS_IMETHODIMP +nsMessenger::DetachAttachment(const nsACString& aContentType, + const nsACString& aURL, + const nsACString& aDisplayName, + const nsACString& aMessageUri, bool aSaveFirst, + bool withoutWarning = false) { + if (aSaveFirst) + return SaveOneAttachment(aContentType, aURL, aDisplayName, aMessageUri, + true); + AutoTArray contentTypeArray = { + PromiseFlatCString(aContentType)}; + AutoTArray urlArray = {PromiseFlatCString(aURL)}; + AutoTArray displayNameArray = { + PromiseFlatCString(aDisplayName)}; + AutoTArray messageUriArray = {PromiseFlatCString(aMessageUri)}; + return DetachAttachments(contentTypeArray, urlArray, displayNameArray, + messageUriArray, nullptr, nullptr, withoutWarning); +} + +NS_IMETHODIMP +nsMessenger::DetachAllAttachments(const nsTArray& aContentTypeArray, + const nsTArray& aUrlArray, + const nsTArray& aDisplayNameArray, + const nsTArray& aMessageUriArray, + bool aSaveFirst, + bool withoutWarning = false) { + NS_ENSURE_ARG_MIN(aContentTypeArray.Length(), 1); + MOZ_ASSERT(aContentTypeArray.Length() == aUrlArray.Length() && + aUrlArray.Length() == aDisplayNameArray.Length() && + aDisplayNameArray.Length() == aMessageUriArray.Length()); + + if (aSaveFirst) + return SaveAllAttachments(aContentTypeArray, aUrlArray, aDisplayNameArray, + aMessageUriArray, true); + else + return DetachAttachments(aContentTypeArray, aUrlArray, aDisplayNameArray, + aMessageUriArray, nullptr, nullptr, + withoutWarning); +} + +nsresult nsMessenger::DetachAttachments( + const nsTArray& aContentTypeArray, + const nsTArray& aUrlArray, + const nsTArray& aDisplayNameArray, + const nsTArray& aMessageUriArray, + nsTArray* saveFileUris, nsIUrlListener* aListener, + bool withoutWarning) { + // if withoutWarning no dialog for user + if (!withoutWarning && NS_FAILED(PromptIfDeleteAttachments( + saveFileUris != nullptr, aDisplayNameArray))) + return NS_OK; + + nsresult rv = NS_OK; + + // ensure that our arguments are valid + // char * partId; + for (uint32_t u = 0; u < aContentTypeArray.Length(); ++u) { + // ensure all of the message URI are the same, we cannot process + // attachments from different messages + if (u > 0 && aMessageUriArray[0] != aMessageUriArray[u]) { + rv = NS_ERROR_INVALID_ARG; + break; + } + + // ensure that we don't have deleted messages in this list + if (aContentTypeArray[u].EqualsLiteral(MIMETYPE_DELETED)) { + rv = NS_ERROR_INVALID_ARG; + break; + } + + // for the moment we prevent any attachments other than root level + // attachments being deleted (i.e. you can't delete attachments from a + // email forwarded as an attachment). We do this by ensuring that the + // part id only has a single period in it (e.g. "1.2"). + // TODO: support non-root level attachment delete + // partId = ::GetAttachmentPartId(aUrlArray[u]); + // if (!partId || PL_strchr(partId, '.') != PL_strrchr(partId, '.')) + // { + // rv = NS_ERROR_INVALID_ARG; + // break; + // } + } + if (NS_FAILED(rv)) { + Alert("deleteAttachmentFailure"); + return rv; + } + + // TODO: ensure that nothing else is processing this message uri at the same + // time + + // TODO: if any of the selected attachments are messages that contain other + // attachments we need to warn the user that all sub-attachments of those + // messages will also be deleted. Best to display a list of them. + + RefPtr deleter = new AttachmentDeleter; + deleter->mListener = aListener; + if (saveFileUris) { + deleter->mDetachedFileUris = saveFileUris->Clone(); + } + // create the attachments for use by the deleter + nsAttachmentState* attach = new nsAttachmentState; + rv = attach->Init(aContentTypeArray, aUrlArray, aDisplayNameArray, + aMessageUriArray); + if (NS_SUCCEEDED(rv)) rv = attach->PrepareForAttachmentDelete(); + if (NS_FAILED(rv)) { + delete attach; + return rv; + } + + // initialize our deleter with the attachments and details. The deleter + // takes ownership of 'attach' immediately irrespective of the return value + // (error or not). + return deleter->StartProcessing(this, mMsgWindow, attach, + saveFileUris != nullptr); +} + +nsresult nsMessenger::PromptIfDeleteAttachments( + bool aSaveFirst, const nsTArray& aDisplayNameArray) { + nsresult rv = NS_ERROR_FAILURE; + + nsCOMPtr dialog(do_GetInterface(mDocShell)); + if (!dialog) return rv; + + if (!mStringBundle) { + rv = InitStringBundle(); + NS_ENSURE_SUCCESS(rv, rv); + } + + // create the list of attachments we are removing + nsString displayString; + nsString attachmentList; + for (uint32_t u = 0; u < aDisplayNameArray.Length(); ++u) { + ConvertAndSanitizeFileName(aDisplayNameArray[u], displayString); + attachmentList.Append(displayString); + attachmentList.Append(char16_t('\n')); + } + AutoTArray formatStrings = {attachmentList}; + + // format the message and display + nsString promptMessage; + const char* propertyName = + aSaveFirst ? "detachAttachments" : "deleteAttachments"; + rv = mStringBundle->FormatStringFromName(propertyName, formatStrings, + promptMessage); + NS_ENSURE_SUCCESS(rv, rv); + + bool dialogResult = false; + rv = dialog->Confirm(nullptr, promptMessage.get(), &dialogResult); + NS_ENSURE_SUCCESS(rv, rv); + + return dialogResult ? NS_OK : NS_ERROR_FAILURE; +} -- cgit v1.2.3