Bug 1930206 - Store EWS messages copied from file to the local store. r=#thunderbird-reviewers

Differential Revision: https://phabricator.services.mozilla.com/D229190
This commit is contained in:
Brendan Abolivier 2024-12-09 17:18:53 +00:00
Родитель d1883a4aa8
Коммит c5dabc2f98
5 изменённых файлов: 212 добавлений и 55 удалений

Просмотреть файл

@ -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);

Просмотреть файл

@ -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<EwsFolder> mFolder;
nsCOMPtr<nsIFile> mFile;
nsCOMPtr<nsIMsgCopyServiceListener> mCopyListener;
};
NS_IMPL_ISUPPORTS(MessageCreateCallbacks, IEWSMessageCreateCallbacks)
NS_IMETHODIMP MessageCreateCallbacks::OnRemoteCreateSuccessful(
const nsACString& ewsId, nsIMsgDBHdr** newHdr) {
// Open an input stream on the file.
nsCOMPtr<nsIInputStream> 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<nsIMsgDatabase> msgDB;
rv = mFolder->GetMsgDatabase(getter_AddRefs(msgDB));
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIMsgDBHdr> 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<nsIOutputStream> 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<nsIMsgPluggableStore> 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<nsIMsgDatabase> 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<MessageCreateCallbacks> ewsListener =
new MessageCreateCallbacks(this, aFile, copyListener);
return client->CreateMessage(ewsId, isDraftOrTemplate, inputStream,
ewsListener);
}
NS_IMETHODIMP EwsFolder::DeleteMessages(

Просмотреть файл

@ -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<AUTF8String> 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);
};

Просмотреть файл

@ -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<u8>,
copy_listener: RefPtr<nsIMsgCopyServiceListener>,
message_callbacks: RefPtr<IEwsMessageCallbacks>,
message_callbacks: RefPtr<IEWSMessageCreateCallbacks>,
) {
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<u8>,
copy_listener: RefPtr<nsIMsgCopyServiceListener>,
message_callbacks: RefPtr<IEwsMessageCallbacks>,
message_callbacks: RefPtr<IEWSMessageCreateCallbacks>,
) -> 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<IEwsMessageCallbacks>,
message_callbacks: RefPtr<IEWSMessageCreateCallbacks>,
) -> Result<RefPtr<nsIMsgDBHdr>, 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

Просмотреть файл

@ -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),
),
)