diff --git a/mailnews/base/public/nsIMsgCopyServiceListener.idl b/mailnews/base/public/nsIMsgCopyServiceListener.idl index dd94f0253c..3a56f076f5 100644 --- a/mailnews/base/public/nsIMsgCopyServiceListener.idl +++ b/mailnews/base/public/nsIMsgCopyServiceListener.idl @@ -26,9 +26,8 @@ interface nsIMsgCopyServiceListener : nsISupports { /** * Setting newly created message key. This method is tailored specifically - * for nsIMsgCopyService::copyFileMessage() when saving Drafts/Templates. - * We need to have a way to inform the client what's the key of the newly - * created message. + * for nsIMsgCopyService::copyFileMessage(). We need to have a way to inform + * the client what's the key of the newly created message. * @param aKey - Message key. */ void setMessageKey(in nsMsgKey aKey); diff --git a/mailnews/protocols/ews/src/EwsFolder.cpp b/mailnews/protocols/ews/src/EwsFolder.cpp index 7e951d98e7..cbb16b84a9 100644 --- a/mailnews/protocols/ews/src/EwsFolder.cpp +++ b/mailnews/protocols/ews/src/EwsFolder.cpp @@ -8,11 +8,12 @@ #include "IEwsClient.h" #include "IEwsIncomingServer.h" #include "MailNewsTypes.h" -#include "nsIMutableArray.h" -#include "nsISupportsPrimitives.h" +#include "nsIMsgDatabase.h" +#include "nsIMsgPluggableStore.h" #include "nsString.h" #include "nsIInputStream.h" #include "nsIMsgWindow.h" +#include "nsMsgUtils.h" #include "nsNetUtil.h" #include "nsPrintfCString.h" #include "nscore.h" @@ -97,6 +98,117 @@ NS_IMETHODIMP MessageDeletionCallbacks::OnError(IEwsClient::Error err, return NS_OK; } +class MessageCreateCallbacks : public IEWSMessageCreateCallbacks { + public: + NS_DECL_ISUPPORTS + NS_DECL_IEWSMESSAGECREATECALLBACKS + + MessageCreateCallbacks(EwsFolder* folder, nsIFile* file, + nsIMsgCopyServiceListener* copyListener) + : mFolder(folder), mFile(file), mCopyListener(copyListener) {} + + protected: + virtual ~MessageCreateCallbacks() = default; + + private: + RefPtr mFolder; + nsCOMPtr mFile; + nsCOMPtr mCopyListener; +}; + +NS_IMPL_ISUPPORTS(MessageCreateCallbacks, IEWSMessageCreateCallbacks) + +NS_IMETHODIMP MessageCreateCallbacks::OnRemoteCreateSuccessful( + const nsACString& ewsId, nsIMsgDBHdr** newHdr) { + // Open an input stream on the file. + nsCOMPtr inputStream; + nsresult rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream), mFile); + NS_ENSURE_SUCCESS(rv, rv); + + // Create a new header in the database for this message. We could do it in one + // go via `nsIMsgPluggableStore::GetNewMsgOutputStream`, but we'll want the + // message database and store to be more decoupled going forwards. + nsCOMPtr msgDB; + rv = mFolder->GetMsgDatabase(getter_AddRefs(msgDB)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr hdr; + rv = msgDB->CreateNewHdr(nsMsgKey_None, getter_AddRefs(hdr)); + NS_ENSURE_SUCCESS(rv, rv); + + // Create a new output stream to the folder's message store. + nsCOMPtr outStream; + rv = mFolder->GetOfflineStoreOutputStream(hdr, getter_AddRefs(outStream)); + NS_ENSURE_SUCCESS(rv, rv); + + // Stream the message content to the store. + uint64_t bytesCopied; + rv = SyncCopyStream(inputStream, outStream, bytesCopied); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr store; + rv = mFolder->GetMsgStore(getter_AddRefs(store)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = store->FinishNewMessage(outStream, hdr); + NS_ENSURE_SUCCESS(rv, rv); + + // Udpate some of the header's metadata, such as the size, the offline flag + // and the EWS ID. + rv = hdr->SetMessageSize(bytesCopied); + NS_ENSURE_SUCCESS(rv, rv); + + rv = hdr->SetOfflineMessageSize(bytesCopied); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t unused; + rv = hdr->OrFlags(nsMsgMessageFlags::Offline, &unused); + NS_ENSURE_SUCCESS(rv, rv); + + rv = hdr->SetStringProperty(ID_PROPERTY, ewsId); + NS_ENSURE_SUCCESS(rv, rv); + + // Return the newly-created header so that the consumer can update it with + // metadata from the message headers before adding it to the message database. + hdr.forget(newHdr); + + return NS_OK; +} + +NS_IMETHODIMP MessageCreateCallbacks::CommitHeader(nsIMsgDBHdr* hdr) { + nsCOMPtr msgDB; + nsresult rv = mFolder->GetMsgDatabase(getter_AddRefs(msgDB)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = msgDB->AddNewHdrToDB(hdr, true); + NS_ENSURE_SUCCESS(rv, rv); + + rv = msgDB->Commit(nsMsgDBCommitType::kLargeCommit); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP MessageCreateCallbacks::OnStartCreate() { + return mCopyListener->OnStartCopy(); +} + +NS_IMETHODIMP MessageCreateCallbacks::SetMessageKey(nsMsgKey aKey) { + return mCopyListener->SetMessageKey(aKey); +} + +NS_IMETHODIMP MessageCreateCallbacks::OnStopCreate(nsresult status) { + nsresult rv = mCopyListener->OnStopCopy(status); + NS_ENSURE_SUCCESS(rv, rv); + + // Note: at some point this will need to call + // `nsMsgCopyService::NotifyCompletion` to let the copy service it can dequeue + // the copy request. There seems to be a trick to it, so we'll take a look at + // it in a later step, see + // https://bugzilla.mozilla.org/show_bug.cgi?id=1931599 + return NS_OK; +} + class MessageOperationCallbacks : public IEwsMessageCallbacks { public: NS_DECL_THREADSAFE_ISUPPORTS @@ -367,9 +479,10 @@ NS_IMETHODIMP EwsFolder::CopyFileMessage( rv = GetEwsClient(getter_AddRefs(client)); NS_ENSURE_SUCCESS(rv, rv); - auto ewsMsgListener = RefPtr(new MessageOperationCallbacks(this, msgWindow)); - return client->SaveMessage(ewsId, isDraftOrTemplate, inputStream, - copyListener, ewsMsgListener); + RefPtr ewsListener = + new MessageCreateCallbacks(this, aFile, copyListener); + return client->CreateMessage(ewsId, isDraftOrTemplate, inputStream, + ewsListener); } NS_IMETHODIMP EwsFolder::DeleteMessages( diff --git a/mailnews/protocols/ews/src/IEwsClient.idl b/mailnews/protocols/ews/src/IEwsClient.idl index 6dc77adb81..db12241925 100644 --- a/mailnews/protocols/ews/src/IEwsClient.idl +++ b/mailnews/protocols/ews/src/IEwsClient.idl @@ -18,7 +18,22 @@ interface IEwsFolderCallbacks; interface IEwsMessageCallbacks; interface IEwsMessageDeleteCallbacks; interface IEWSMessageFetchCallbacks; +interface IEWSMessageCreateCallbacks; +/** + * An interface to communicate with an EWS server, and lives on + * `EwsIncomingServer`. + * + * Its main role is to perform remote operations on the relevant EWS server, + * which is passed to `initialize`. This same method also uses the provided + * `server` to retrieve connection settings (such as authentication). + * + * Most of the code to run besides forming and sending requests to (and reading + * responses from) the EWS server is defined by the consumer via the relevant + * callback interface. However, `IEwsClient` will in some cases take care of + * e.g. populating `nsIMsgDBHdr`s (instead of letting the consumer code do it) + * due to architectural constraints. + */ [uuid(4a117361-653b-48a5-9ddb-588482ef9dbb)] interface IEwsClient : nsISupports { @@ -39,19 +54,19 @@ interface IEwsClient : nsISupports * * @param folderId The EWS ID of the folder. * @param isDraft Whether the message being created is an unsent - * draft. + * draft, so the correct flags can be set on the + * server for the newly created message. * @param messageStream The input stream to read the message from. - * @param copyListener A listener to provide updates on copying of - * the message from the stream. * @param messageCallbacks Callbacks to use to communicate between the EWS - * client and the EWS folder (e.g. to access its - * database). + * client and the EWS folder (e.g. to signal the + * message can be added to its database), and to + * provide updates on the copying of the message to + * the server. */ - void saveMessage(in AUTF8String folderId, + void createMessage(in AUTF8String folderId, in boolean isDraft, in nsIInputStream messageStream, - in nsIMsgCopyServiceListener copyListener, - in IEwsMessageCallbacks messageCallbacks); + in IEWSMessageCreateCallbacks messageCallbacks); void deleteMessages(in Array messageEwsIds, in IEwsMessageDeleteCallbacks callbacks); }; @@ -101,3 +116,45 @@ interface IEWSMessageFetchCallbacks : nsISupports void onDataAvailable(in nsIInputStream aInputStream, in unsigned long aCount); void onFetchStop(in nsresult status); }; + +/** + * A listener used when creating a new message on the server. + * + * The listener is expected to hold a handle on the temporary file that contains + * the new message, so it can stream it to the store when it has been stored + * correctly. + */ +[uuid(ff45569f-d618-4bb0-9686-6cb24b92b02b)] +interface IEWSMessageCreateCallbacks : nsISupports +{ + /** + * These methods are mostly replicating the similarly-named ones in + * `nsIMsgCopyServiceListener`, and at the moment just forward calls to an + * instance of it. This is partly so to simplify `createMessage` so it only + * takes one listener, but also because solving + * https://bugzilla.mozilla.org/show_bug.cgi?id=1931599 will require doing + * more in `onStopCreate`. + */ + void onStartCreate(); + void onStopCreate(in nsresult status); + void setMessageKey(in nsMsgKey aKey); + + /** + * Signals that the message was correctly created on the server. + * + * Returns the header object to update with the message's metadata and to + * commit to the message database. + * + * `nsIMsgDBHdr` is a type quite strongly associated with the message database + * and storage, and, going forwards, we'll want to decouple these interfaces + * from local storage management. We use currently use it because we don't + * have a better way to represent structured headers over the XPCOM boundary, + * and parsing RFC822 messages is easier in Rust than using the C++ message + * parser. We should revisit our use of `nsIMsgDBHdr` in client code the + * situation improves. + */ + nsIMsgDBHdr onRemoteCreateSuccessful(in AUTF8String ewsId); + + // Commits the given header to the relevant message database. + void commitHeader(in nsIMsgDBHdr hdr); +}; diff --git a/rust/ews_xpcom/src/client.rs b/rust/ews_xpcom/src/client.rs index 71ff96ee97..17fce49727 100644 --- a/rust/ews_xpcom/src/client.rs +++ b/rust/ews_xpcom/src/client.rs @@ -35,8 +35,8 @@ use uuid::Uuid; use xpcom::{ getter_addrefs, interfaces::{ - nsIMsgCopyServiceListener, nsIMsgDBHdr, nsIMsgOutgoingListener, nsIStringInputStream, - nsIURI, nsMsgFolderFlagType, nsMsgFolderFlags, nsMsgKey, nsMsgMessageFlags, + nsIMsgDBHdr, nsIMsgOutgoingListener, nsIStringInputStream, nsIURI, nsMsgFolderFlagType, + nsMsgFolderFlags, nsMsgKey, nsMsgMessageFlags, IEWSMessageCreateCallbacks, IEWSMessageFetchCallbacks, IEwsClient, IEwsFolderCallbacks, IEwsMessageCallbacks, IEwsMessageDeleteCallbacks, }, @@ -972,15 +972,14 @@ impl XpComEwsClient { /// All headers are expected to be included in the provided MIME content. /// /// [`CreateItem`] https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/createitem-operation-email-message - pub async fn save_message( + pub async fn create_message( self, folder_id: String, is_draft: bool, content: Vec, - copy_listener: RefPtr, - message_callbacks: RefPtr, + message_callbacks: RefPtr, ) { - if let Err(status) = unsafe { copy_listener.OnStartCopy().to_result() } { + if let Err(status) = unsafe { message_callbacks.OnStartCreate().to_result() } { log::error!("aborting copy: an error occurred while starting the listener: {status}"); return; } @@ -989,13 +988,7 @@ impl XpComEwsClient { // Use the return value to determine which status we should use when // notifying the end of the request. let status = match self - .save_message_inner( - folder_id, - is_draft, - content, - copy_listener.clone(), - message_callbacks, - ) + .create_message_inner(folder_id, is_draft, content, message_callbacks.clone()) .await { Ok(_) => nserror::NS_OK, @@ -1006,18 +999,17 @@ impl XpComEwsClient { } }; - if let Err(err) = unsafe { copy_listener.OnStopCopy(status) }.to_result() { + if let Err(err) = unsafe { message_callbacks.OnStopCreate(status) }.to_result() { log::error!("aborting copy: an error occurred while stopping the listener: {err}") } } - async fn save_message_inner( + async fn create_message_inner( &self, folder_id: String, is_draft: bool, content: Vec, - copy_listener: RefPtr, - message_callbacks: RefPtr, + message_callbacks: RefPtr, ) -> Result<(), XpComEwsError> { // Create a new message from the binary content we got. let mut message = Message { @@ -1066,21 +1058,16 @@ impl XpComEwsClient { let response_message = self.make_create_item_request(create_item).await?; - let hdr = create_and_populate_header_from_save_response( + let hdr = create_and_populate_header_from_create_response( response_message, &content, - message_callbacks, + message_callbacks.clone(), )?; - if is_draft { - // If we're dealing with a draft message, copy the message key to - // the listener so that the draft can be replaced if a newer draft - // of the message is saved. - let mut key: nsMsgKey = 0; - - unsafe { hdr.GetMessageKey(&mut key) }.to_result()?; - unsafe { copy_listener.SetMessageKey(key) }.to_result()?; - } + // Let the listeners know of the local key for the newly created message. + let mut key: nsMsgKey = 0; + unsafe { hdr.GetMessageKey(&mut key) }.to_result()?; + unsafe { message_callbacks.SetMessageKey(key) }.to_result()?; Ok(()) } @@ -1678,10 +1665,10 @@ fn validate_get_folder_response_message( /// Uses the provided `CreateItemResponseMessage` to create, populate and commit /// an `nsIMsgDBHdr` for a newly created message. -fn create_and_populate_header_from_save_response( +fn create_and_populate_header_from_create_response( response_message: CreateItemResponseMessage, content: &[u8], - message_callbacks: RefPtr, + message_callbacks: RefPtr, ) -> Result, XpComEwsError> { // If we're saving the message (rather than sending it), we must create a // new database entry for it and associate it with the message's EWS ID. @@ -1702,8 +1689,10 @@ fn create_and_populate_header_from_save_response( .id; let ews_id = nsCString::from(ews_id); + // Signal that copying the message to the server has succeeded, which will + // trigger its content to be streamed to the relevant message store. let hdr = - getter_addrefs(|hdr| unsafe { message_callbacks.CreateNewHeaderForItem(&*ews_id, hdr) })?; + getter_addrefs(|hdr| unsafe { message_callbacks.OnRemoteCreateSuccessful(&*ews_id, hdr) })?; // Parse the message and use its headers to populate the `nsIMsgDBHdr` // before committing it to the database. We parse the original content diff --git a/rust/ews_xpcom/src/lib.rs b/rust/ews_xpcom/src/lib.rs index f96e175c16..fba530ede1 100644 --- a/rust/ews_xpcom/src/lib.rs +++ b/rust/ews_xpcom/src/lib.rs @@ -14,8 +14,9 @@ use thin_vec::ThinVec; use url::Url; use xpcom::{ interfaces::{ - nsIInputStream, nsIMsgCopyServiceListener, nsIMsgIncomingServer, IEWSMessageFetchCallbacks, - IEwsFolderCallbacks, IEwsMessageCallbacks, IEwsMessageDeleteCallbacks, + nsIInputStream, nsIMsgIncomingServer, IEWSMessageCreateCallbacks, + IEWSMessageFetchCallbacks, IEwsFolderCallbacks, IEwsMessageCallbacks, + IEwsMessageDeleteCallbacks, }, nsIID, xpcom_method, RefPtr, }; @@ -181,14 +182,13 @@ impl XpcomEwsBridge { Ok(()) } - xpcom_method!(save_message => SaveMessage(folder_id: *const nsACString, isDraft: bool, messageStream: *const nsIInputStream, copyListener: *const nsIMsgCopyServiceListener, messageCallbaks: *const IEwsMessageCallbacks)); - fn save_message( + xpcom_method!(create_message => CreateMessage(folder_id: *const nsACString, isDraft: bool, messageStream: *const nsIInputStream, messageCallbacks: *const IEWSMessageCreateCallbacks)); + fn create_message( &self, folder_id: &nsACString, is_draft: bool, message_stream: &nsIInputStream, - copy_listener: &nsIMsgCopyServiceListener, - message_callbacks: &IEwsMessageCallbacks, + message_callbacks: &IEWSMessageCreateCallbacks, ) -> Result<(), nsresult> { let content = crate::xpcom_io::read_stream(message_stream)?; @@ -198,11 +198,10 @@ impl XpcomEwsBridge { // this scope, so spawn it as a detached `moz_task`. moz_task::spawn_local( "save_message", - client.save_message( + client.create_message( folder_id.to_utf8().into(), is_draft, content, - RefPtr::new(copy_listener), RefPtr::new(message_callbacks), ), )