Bug 1914442 - Support copying messages from file for EWS. r=leftmostcat

Differential Revision: https://phabricator.services.mozilla.com/D219902

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Brendan Abolivier 2024-10-25 00:06:35 +00:00
Родитель 8af40f1b9e
Коммит 85def2b630
13 изменённых файлов: 691 добавлений и 149 удалений

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

@ -4,11 +4,15 @@
#include "EwsFolder.h"
#include "ErrorList.h"
#include "IEwsClient.h"
#include "IEwsIncomingServer.h"
#include "MailNewsTypes.h"
#include "nsIInputStream.h"
#include "nsIMsgWindow.h"
#include "nsNetUtil.h"
#include "nsPrintfCString.h"
#include "nscore.h"
#define kEWSRootURI "ews:/"
#define kEWSMessageRootURI "ews-message:/"
@ -16,25 +20,25 @@
#define ID_PROPERTY "ewsId"
#define SYNC_STATE_PROPERTY "ewsSyncStateToken"
class MessageSyncListener : public IEwsMessageCallbacks {
class MessageOperationCallbacks : public IEwsMessageCallbacks {
public:
NS_DECL_THREADSAFE_ISUPPORTS
NS_DECL_IEWSMESSAGECALLBACKS
MessageSyncListener(EwsFolder* folder, nsIMsgWindow* window)
MessageOperationCallbacks(EwsFolder* folder, nsIMsgWindow* window)
: mFolder(folder), mWindow(window) {}
protected:
virtual ~MessageSyncListener() = default;
virtual ~MessageOperationCallbacks() = default;
private:
RefPtr<EwsFolder> mFolder;
RefPtr<nsIMsgWindow> mWindow;
};
NS_IMPL_ISUPPORTS(MessageSyncListener, IEwsMessageCallbacks)
NS_IMPL_ISUPPORTS(MessageOperationCallbacks, IEwsMessageCallbacks)
NS_IMETHODIMP MessageSyncListener::CommitHeader(nsIMsgDBHdr* hdr) {
NS_IMETHODIMP MessageOperationCallbacks::CommitHeader(nsIMsgDBHdr* hdr) {
RefPtr<nsIMsgDatabase> db;
nsresult rv = mFolder->GetMsgDatabase(getter_AddRefs(db));
NS_ENSURE_SUCCESS(rv, rv);
@ -42,7 +46,7 @@ NS_IMETHODIMP MessageSyncListener::CommitHeader(nsIMsgDBHdr* hdr) {
return db->AddNewHdrToDB(hdr, true);
}
NS_IMETHODIMP MessageSyncListener::CreateNewHeaderForItem(
NS_IMETHODIMP MessageOperationCallbacks::CreateNewHeaderForItem(
const nsACString& ewsId, nsIMsgDBHdr** _retval) {
RefPtr<nsIMsgDatabase> db;
nsresult rv = mFolder->GetMsgDatabase(getter_AddRefs(db));
@ -69,13 +73,13 @@ NS_IMETHODIMP MessageSyncListener::CreateNewHeaderForItem(
return NS_OK;
}
NS_IMETHODIMP MessageSyncListener::UpdateSyncState(
NS_IMETHODIMP MessageOperationCallbacks::UpdateSyncState(
const nsACString& syncStateToken) {
return mFolder->SetStringProperty(SYNC_STATE_PROPERTY, syncStateToken);
}
NS_IMETHODIMP MessageSyncListener::OnError(IEwsClient::Error err,
const nsACString& desc) {
NS_IMETHODIMP MessageOperationCallbacks::OnError(IEwsClient::Error err,
const nsACString& desc) {
NS_ERROR("Error occurred while syncing EWS messages");
return NS_OK;
@ -218,18 +222,49 @@ NS_IMETHODIMP EwsFolder::RenameSubFolders(nsIMsgWindow* msgWindow,
}
NS_IMETHODIMP EwsFolder::UpdateFolder(nsIMsgWindow* aWindow) {
nsCOMPtr<nsIMsgIncomingServer> server;
nsresult rv = GetServer(getter_AddRefs(server));
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<IEwsIncomingServer> ewsServer(do_QueryInterface(server));
nsCOMPtr<IEwsClient> client;
rv = ewsServer->GetEwsClient(getter_AddRefs(client));
nsresult rv = GetEwsClient(getter_AddRefs(client));
NS_ENSURE_SUCCESS(rv, rv);
nsCString ewsId;
rv = GetStringProperty(ID_PROPERTY, ewsId);
rv = GetEwsId(ewsId);
NS_ENSURE_SUCCESS(rv, rv);
// EWS provides us an opaque value which specifies the last version of
// upstream messages we received. Provide that to simplify sync.
nsCString syncStateToken;
rv = GetStringProperty(SYNC_STATE_PROPERTY, syncStateToken);
if (NS_FAILED(rv)) {
syncStateToken = EmptyCString();
}
auto listener = RefPtr(new MessageOperationCallbacks(this, aWindow));
return client->SyncMessagesForFolder(listener, ewsId, syncStateToken);
}
NS_IMETHODIMP EwsFolder::CopyFileMessage(
nsIFile* aFile, nsIMsgDBHdr* msgToReplace, bool isDraftOrTemplate,
uint32_t newMsgFlags, const nsACString& aNewMsgKeywords,
nsIMsgWindow* msgWindow, nsIMsgCopyServiceListener* copyListener) {
nsCString ewsId;
nsresult rv = GetEwsId(ewsId);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIInputStream> inputStream;
rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream), aFile);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<IEwsClient> client;
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);
}
nsresult EwsFolder::GetEwsId(nsACString& ewsId) {
nsresult rv = GetStringProperty(ID_PROPERTY, ewsId);
NS_ENSURE_SUCCESS(rv, rv);
if (ewsId.IsEmpty()) {
@ -240,14 +275,15 @@ NS_IMETHODIMP EwsFolder::UpdateFolder(nsIMsgWindow* aWindow) {
return NS_ERROR_UNEXPECTED;
}
// EWS provides us an opaque value which specifies the last version of
// upstream messages we received. Provide that to simplify sync.
nsCString syncStateToken;
rv = GetStringProperty(SYNC_STATE_PROPERTY, syncStateToken);
if (NS_FAILED(rv)) {
syncStateToken = EmptyCString();
}
auto listener = RefPtr(new MessageSyncListener(this, aWindow));
return client->SyncMessagesForFolder(listener, ewsId, syncStateToken);
return NS_OK;
}
nsresult EwsFolder::GetEwsClient(IEwsClient** ewsClient) {
nsCOMPtr<nsIMsgIncomingServer> server;
nsresult rv = GetServer(getter_AddRefs(server));
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<IEwsIncomingServer> ewsServer(do_QueryInterface(server));
return ewsServer->GetEwsClient(ewsClient);
}

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

@ -5,7 +5,9 @@
#ifndef __COMM_MAILNEWS_PROTOCOLS_EWS_FOLDER_H
#define __COMM_MAILNEWS_PROTOCOLS_EWS_FOLDER_H
#include "IEwsClient.h"
#include "nsMsgDBFolder.h"
#include "nscore.h"
class EwsFolder : public nsMsgDBFolder {
public:
@ -22,6 +24,11 @@ class EwsFolder : public nsMsgDBFolder {
NS_IMETHOD CreateStorageIfMissing(nsIUrlListener* urlListener) override;
NS_IMETHOD CreateSubfolder(const nsAString& folderName,
nsIMsgWindow* msgWindow) override;
NS_IMETHOD CopyFileMessage(nsIFile* aFile, nsIMsgDBHdr* msgToReplace,
bool isDraftOrTemplate, uint32_t newMsgFlags,
const nsACString& aNewMsgKeywords,
nsIMsgWindow* msgWindow,
nsIMsgCopyServiceListener* listener) override;
NS_IMETHOD GetDBFolderInfoAndDB(nsIDBFolderInfo** folderInfo,
nsIMsgDatabase** _retval) override;
@ -37,6 +44,13 @@ class EwsFolder : public nsMsgDBFolder {
private:
bool mHasLoadedSubfolders;
// Generate or retrieve an EWS API client capable of interacting with the EWS
// server this folder depends from.
nsresult GetEwsClient(IEwsClient** ewsClient);
// Locally look up the EWS ID for the current folder.
nsresult GetEwsId(nsACString& ewsId);
};
#endif

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

@ -8,6 +8,8 @@ interface nsIMsgDBHdr;
interface nsIRequest;
interface nsIStreamListener;
interface nsIMsgIncomingServer;
interface nsIInputStream;
interface nsIMsgCopyServiceListener;
interface IEwsFolderCallbacks;
interface IEwsMessageCallbacks;
@ -25,6 +27,25 @@ interface IEwsClient : nsISupports
void syncFolderHierarchy(in IEwsFolderCallbacks callbacks, in AUTF8String syncStateToken);
void syncMessagesForFolder(in IEwsMessageCallbacks callbacks, in AUTF8String folderId, in AUTF8String syncStateToken);
void getMessage(in AUTF8String id, in nsIRequest request, in nsIStreamListener listener);
/**
* Create a new message on the server using the data read from the stream.
*
* @param folderId The EWS ID of the folder.
* @param isDraft Whether the message being created is an unsent
* draft.
* @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).
*/
void saveMessage(in AUTF8String folderId,
in boolean isDraft,
in nsIInputStream messageStream,
in nsIMsgCopyServiceListener copyListener,
in IEwsMessageCallbacks messageCallbacks);
};
[uuid(5dacc994-30e0-42f7-94c8-52756638add5)]

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

@ -96,9 +96,9 @@ git = "https://github.com/servo/unicode-bidi"
rev = "ca612daf1c08c53abe07327cb3e6ef6e0a760f0c"
replace-with = "vendored-sources"
[source."git+https://github.com/thunderbird/ews-rs.git?rev=c080aad0b37b6f4f0ba74165f4605e70f5740641"]
[source."git+https://github.com/thunderbird/ews-rs.git?rev=9a54b74bb655f91c9f629437b253c448595ce8d2"]
git = "https://github.com/thunderbird/ews-rs.git"
rev = "c080aad0b37b6f4f0ba74165f4605e70f5740641"
rev = "9a54b74bb655f91c9f629437b253c448595ce8d2"
replace-with = "vendored-sources"
[source."git+https://github.com/thunderbird/xml-struct-rs.git?rev=87723b90425d474fd29095d8b710baefd7c9b13a"]

12
rust/Cargo.lock сгенерированный
Просмотреть файл

@ -1605,7 +1605,7 @@ dependencies = [
[[package]]
name = "ews"
version = "0.1.0"
source = "git+https://github.com/thunderbird/ews-rs.git?rev=c080aad0b37b6f4f0ba74165f4605e70f5740641#c080aad0b37b6f4f0ba74165f4605e70f5740641"
source = "git+https://github.com/thunderbird/ews-rs.git?rev=9a54b74bb655f91c9f629437b253c448595ce8d2#9a54b74bb655f91c9f629437b253c448595ce8d2"
dependencies = [
"log",
"quick-xml",
@ -1626,6 +1626,7 @@ dependencies = [
"fxhash",
"log",
"mail-builder",
"mail-parser",
"moz_http",
"moz_task",
"nserror",
@ -3174,6 +3175,15 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25f5871d5270ed80f2ee750b95600c8d69b05f8653ad3be913b2ad2e924fefcb"
[[package]]
name = "mail-parser"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93c3b9e5d8b17faf573330bbc43b37d6e918c0a3bf8a88e7d0a220ebc84af9fc"
dependencies = [
"encoding_rs",
]
[[package]]
name = "malloc_buf"
version = "0.0.6"

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

@ -6,9 +6,10 @@ edition = "2021"
[dependencies]
base64 = "0.21.3"
cstr = "0.2"
ews = { git = "https://github.com/thunderbird/ews-rs.git", rev = "c080aad0b37b6f4f0ba74165f4605e70f5740641", version = "0.1.0" }
ews = { git = "https://github.com/thunderbird/ews-rs.git", rev = "9a54b74bb655f91c9f629437b253c448595ce8d2", version = "0.1.0" }
fxhash = "0.2.1"
log = "0.4.21"
mail-parser = "0.9.3"
moz_http = { version = "0.1.0", path = "../moz_http" }
moz_task = { version = "0.1.0", path = "../../../xpcom/rust/moz_task" }
nserror = { version = "0.1.0", path = "../../../xpcom/rust/nserror" }

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

@ -9,17 +9,20 @@ use std::{
use base64::prelude::{Engine, BASE64_STANDARD};
use ews::{
create_item::{self, CreateItem, MessageDisposition},
create_item::{
self, CreateItem, CreateItemResponseMessage, ExtendedProperty, MessageDisposition,
},
get_folder::GetFolder,
get_item::GetItem,
soap,
sync_folder_hierarchy::{self, SyncFolderHierarchy},
sync_folder_items::{self, SyncFolderItems},
ArrayOfRecipients, BaseFolderId, BaseItemId, BaseShape, Folder, FolderShape, Importance,
ItemShape, Message, MimeContent, Operation, PathToElement, RealItem, Recipient, ResponseClass,
ArrayOfRecipients, BaseFolderId, BaseItemId, BaseShape, ExtendedFieldURI, Folder, FolderShape,
ItemShape, MimeContent, Operation, PathToElement, RealItem, Recipient, ResponseClass,
ResponseCode,
};
use fxhash::FxHashMap;
use mail_parser::MessageParser;
use moz_http::StatusCode;
use nserror::nsresult;
use nsstring::{nsCString, nsString};
@ -29,15 +32,30 @@ use uuid::Uuid;
use xpcom::{
getter_addrefs,
interfaces::{
nsIMsgDBHdr, nsIRequest, nsIRequestObserver, nsIStreamListener, nsIStringInputStream,
nsMsgFolderFlagType, nsMsgFolderFlags, nsMsgMessageFlags, nsMsgPriority, IEwsClient,
IEwsFolderCallbacks, IEwsMessageCallbacks,
nsIMsgCopyServiceListener, nsIMsgDBHdr, nsIRequest, nsIRequestObserver, nsIStreamListener,
nsIStringInputStream, nsMsgFolderFlagType, nsMsgFolderFlags, nsMsgKey, nsMsgMessageFlags,
IEwsClient, IEwsFolderCallbacks, IEwsMessageCallbacks,
},
RefPtr,
};
use crate::headers::MessageHeaders;
use crate::{authentication::credentials::Credentials, cancellable_request::CancellableRequest};
// Flags to use for setting the `PR_MESSAGE_FLAGS` MAPI property.
//
// See
// <https://learn.microsoft.com/en-us/office/client-developer/outlook/mapi/pidtagmessageflags-canonical-property>,
// although the specific values are set in `Mapidefs.h` from the Windows SDK:
// <https://github.com/microsoft/MAPIStubLibrary/blob/1d30c31ebf05ef444371520cd4268d6e1fda8a3b/include/MAPIDefS.h#L2143-L2154>
//
// Message flags are of type `PT_LONG`, which corresponds to i32 (signed 32-bit
// integers) according to
// https://learn.microsoft.com/en-us/office/client-developer/outlook/mapi/property-types
const MSGFLAG_READ: i32 = 0x00000001;
const MSGFLAG_UNMODIFIED: i32 = 0x00000002;
const MSGFLAG_UNSENT: i32 = 0x00000008;
pub(crate) struct XpComEwsClient {
pub endpoint: Url,
pub credentials: Credentials,
@ -300,7 +318,7 @@ impl XpComEwsClient {
}
let header = result?;
populate_message_header_from_item(&header, &msg)?;
populate_message_header_from_item(&header, msg)?;
unsafe { callbacks.CommitHeader(&*header) }.to_result()?;
}
@ -757,9 +775,9 @@ impl XpComEwsClient {
for response_message in response.response_messages.get_item_response_message {
process_response_message_class(
"GetItem",
response_message.response_class,
response_message.response_code,
response_message.message_text,
&response_message.response_class,
&response_message.response_code,
&response_message.message_text,
)?;
// The expected shape of the list of response messages is
@ -817,7 +835,7 @@ impl XpComEwsClient {
Err(err) => {
log::error!("an error occurred while attempting to send the message: {err:?}");
nsresult::from(err)
err.into()
}
};
@ -830,7 +848,7 @@ impl XpComEwsClient {
}
async fn send_message_inner(
&self,
self,
mime_content: String,
message_id: String,
should_request_dsn: bool,
@ -847,7 +865,7 @@ impl XpComEwsClient {
let message = create_item::Message {
mime_content: Some(MimeContent {
character_set: None,
content: BASE64_STANDARD.encode(mime_content),
content: BASE64_STANDARD.encode(&mime_content),
}),
is_delivery_receipt_requested: Some(should_request_dsn),
internet_message_id: Some(message_id),
@ -860,12 +878,143 @@ impl XpComEwsClient {
// We don't need EWS to copy messages to the Sent folder after
// they've been sent, because the internal MessageSend module
// already takes care of it, and will include additional headers we
// already takes care of it and will include additional headers we
// don't send to EWS (such as Bcc).
message_disposition: MessageDisposition::SendOnly,
message_disposition: Some(MessageDisposition::SendOnly),
saved_item_folder_id: None,
};
self.make_create_item_request(create_item).await?;
Ok(())
}
/// Create a message on the server by performing a [`CreateItem`] operation
/// via EWS.
///
/// 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(
self,
folder_id: String,
is_draft: bool,
content: Vec<u8>,
copy_listener: RefPtr<nsIMsgCopyServiceListener>,
message_callbacks: RefPtr<IEwsMessageCallbacks>,
) {
if let Err(status) = unsafe { copy_listener.OnStartCopy().to_result() } {
log::error!("aborting copy: an error occurred while starting the listener: {status}");
return;
}
// Send the request, using an inner method to more easily handle errors.
// 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,
)
.await
{
Ok(_) => nserror::NS_OK,
Err(err) => {
log::error!("an error occurred while attempting to copy the message: {err:?}");
err.into()
}
};
if let Err(err) = unsafe { copy_listener.OnStopCopy(status) }.to_result() {
log::error!("aborting copy: an error occurred while stopping the listener: {err}")
}
}
async fn save_message_inner(
&self,
folder_id: String,
is_draft: bool,
content: Vec<u8>,
copy_listener: RefPtr<nsIMsgCopyServiceListener>,
message_callbacks: RefPtr<IEwsMessageCallbacks>,
) -> Result<(), XpComEwsError> {
// Create a new message from the binary content we got.
let mut message = create_item::Message {
mime_content: Some(MimeContent {
character_set: None,
content: BASE64_STANDARD.encode(&content),
}),
..Default::default()
};
// Set the `PR_MESSAGE_FLAGS` MAPI property. If not set, the EWS server
// uses `MSGFLAG_UNSENT` | `MSGFLAG_UNMODIFIED` as the default value,
// which is not what we want.
//
// See
// https://learn.microsoft.com/en-us/office/client-developer/outlook/mapi/pidtagmessageflags-canonical-property
let mut mapi_flags = MSGFLAG_READ;
if is_draft {
mapi_flags |= MSGFLAG_UNSENT;
} else {
mapi_flags |= MSGFLAG_UNMODIFIED;
}
message.extended_property = Some(vec![ExtendedProperty {
extended_field_URI: ExtendedFieldURI {
distinguished_property_set_id: None,
property_set_id: None,
property_name: None,
property_id: None,
// 3591 (0x0E07) is the `PR_MESSAGE_FLAGS` MAPI property.
property_tag: Some("3591".into()),
property_type: ews::PropertyType::Integer,
},
value: mapi_flags.to_string(),
}]);
let create_item = CreateItem {
items: vec![create_item::Item::Message(message)],
message_disposition: Some(MessageDisposition::SaveOnly),
saved_item_folder_id: Some(BaseFolderId::FolderId {
id: folder_id,
change_key: None,
}),
};
let response_message = self.make_create_item_request(create_item).await?;
let hdr = create_and_populate_header_from_save_response(
response_message,
&content,
message_callbacks,
)?;
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()?;
}
Ok(())
}
/// Performs a [`CreateItem`] operation and processes its response.
///
/// [`CreateItem`] https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/createitem-operation-email-message
async fn make_create_item_request(
&self,
create_item: CreateItem,
) -> Result<CreateItemResponseMessage, XpComEwsError> {
let response = self.make_operation_request(create_item).await?;
// We have only sent one message, therefore the response should only
@ -882,10 +1031,12 @@ impl XpComEwsClient {
let response_message = response_messages.into_iter().next().unwrap();
process_response_message_class(
"CreateItem",
response_message.response_class,
response_message.response_code,
response_message.message_text,
)
&response_message.response_class,
&response_message.response_code,
&response_message.message_text,
)?;
Ok(response_message)
}
/// Makes a request to the EWS endpoint to perform an operation.
@ -954,9 +1105,9 @@ impl XpComEwsClient {
/// Sets the fields of a database message header object from an EWS `Message`.
fn populate_message_header_from_item(
header: &nsIMsgDBHdr,
msg: &Message,
msg: impl MessageHeaders,
) -> Result<(), XpComEwsError> {
let internet_message_id = if let Some(internet_message_id) = msg.internet_message_id.as_ref() {
let internet_message_id = if let Some(internet_message_id) = msg.internet_message_id() {
nsCString::from(internet_message_id)
} else {
// Lots of code assumes Message-ID is set and unique, so we need to
@ -976,14 +1127,14 @@ fn populate_message_header_from_item(
let mut header_flags = 0;
unsafe { header.GetFlags(&mut header_flags) }.to_result()?;
if let Some(is_read) = msg.is_read {
if let Some(is_read) = msg.is_read() {
if is_read {
should_write_flags = true;
header_flags |= nsMsgMessageFlags::Read;
}
}
if let Some(has_attachments) = msg.has_attachments {
if let Some(has_attachments) = msg.has_attachments() {
if has_attachments {
should_write_flags = true;
header_flags |= nsMsgMessageFlags::Attachment;
@ -994,64 +1145,42 @@ fn populate_message_header_from_item(
unsafe { header.SetFlags(header_flags) }.to_result()?;
}
let sent_time_in_micros = msg.date_time_sent.as_ref().and_then(|date_time| {
// `time` gives Unix timestamps in seconds. `PRTime` is an `i64`
// representing Unix timestamps in microseconds. `PRTime` won't overflow
// for over 500,000 years, but we use `checked_mul()` to guard against
// receiving nonsensical values.
let time_in_micros = date_time.0.unix_timestamp().checked_mul(1_000 * 1_000);
if time_in_micros.is_none() {
log::warn!(
"message with ID {item_id} sent date {date_time:?} too big for `i64`, ignoring",
item_id = msg.item_id.id
);
}
time_in_micros
});
if let Some(sent) = sent_time_in_micros {
if let Some(sent) = msg.sent_timestamp_ms() {
unsafe { header.SetDate(sent) }.to_result()?;
}
if let Some(author) = msg.from.as_ref().or(msg.sender.as_ref()) {
let author = nsCString::from(make_header_string_for_mailbox(&author.mailbox));
if let Some(author) = msg.author() {
let author = nsCString::from(make_header_string_for_mailbox(&author));
unsafe { header.SetAuthor(&*author) }.to_result()?;
}
if let Some(reply_to) = msg.reply_to.as_ref() {
let reply_to = nsCString::from(make_header_string_for_mailbox(&reply_to.mailbox));
if let Some(reply_to) = msg.reply_to_recipient() {
let reply_to = nsCString::from(make_header_string_for_mailbox(&reply_to));
unsafe { header.SetStringProperty(cstr::cstr!("replyTo").as_ptr(), &*reply_to) }
.to_result()?;
}
if let Some(to) = msg.to_recipients.as_ref() {
if let Some(to) = msg.to_recipients() {
let to = nsCString::from(make_header_string_for_mailbox_list(to));
unsafe { header.SetRecipients(&*to) }.to_result()?;
}
if let Some(cc) = msg.cc_recipients.as_ref() {
if let Some(cc) = msg.cc_recipients() {
let cc = nsCString::from(make_header_string_for_mailbox_list(cc));
unsafe { header.SetCcList(&*cc) }.to_result()?;
}
if let Some(bcc) = msg.bcc_recipients.as_ref() {
if let Some(bcc) = msg.bcc_recipients() {
let bcc = nsCString::from(make_header_string_for_mailbox_list(bcc));
unsafe { header.SetBccList(&*bcc) }.to_result()?;
}
if let Some(subject) = msg.subject.as_ref() {
if let Some(subject) = msg.message_subject() {
let subject = nsCString::from(subject);
unsafe { header.SetSubject(&*subject) }.to_result()?;
}
if let Some(importance) = msg.importance {
let priority = match importance {
Importance::Low => nsMsgPriority::low,
Importance::Normal => nsMsgPriority::normal,
Importance::High => nsMsgPriority::high,
};
if let Some(priority) = msg.priority() {
unsafe { header.SetPriority(priority) }.to_result()?;
}
@ -1084,10 +1213,12 @@ fn maybe_get_backoff_delay_ms(err: &ews::Error) -> Option<u32> {
/// Creates a string representation of a list of mailboxes, suitable for use as
/// the value of an Internet Message Format header.
fn make_header_string_for_mailbox_list(mailboxes: &ArrayOfRecipients) -> String {
fn make_header_string_for_mailbox_list(
mailboxes: impl IntoIterator<Item = ews::Mailbox>,
) -> String {
let strings: Vec<_> = mailboxes
.iter()
.map(|item| make_header_string_for_mailbox(&item.mailbox))
.into_iter()
.map(|mailbox| make_header_string_for_mailbox(&mailbox))
.collect();
strings.join(", ")
@ -1203,9 +1334,9 @@ where
/// return an error accordingly.
fn process_response_message_class(
op_name: &str,
response_class: ResponseClass,
response_code: Option<ResponseCode>,
message_text: Option<String>,
response_class: &ResponseClass,
response_code: &Option<ResponseCode>,
message_text: &Option<String>,
) -> Result<(), XpComEwsError> {
match response_class {
ResponseClass::Success => Ok(()),
@ -1245,3 +1376,44 @@ fn process_response_message_class(
}
}
}
/// Uses the provided `CreateItemResponseMessage` to create, populate and commit
/// an `nsIMsgDBHdr` for a newly created message.
fn create_and_populate_header_from_save_response(
response_message: CreateItemResponseMessage,
content: &[u8],
message_callbacks: RefPtr<IEwsMessageCallbacks>,
) -> 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.
let items = &response_message.items.inner;
if items.len() != 1 {
return Err(XpComEwsError::Processing {
message: String::from("expected only one item in CreateItem response"),
});
}
let message = match &items[0] {
RealItem::Message(message) => message,
};
let ews_id = nsCString::from(message.item_id.id.clone());
let hdr =
getter_addrefs(|hdr| unsafe { message_callbacks.CreateNewHeaderForItem(&*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
// rather than use the `Message` from the `CreateItemResponse` because the
// latter only contains the item's ID, and so is missing the required
// fields.
let message = MessageParser::default()
.parse(content)
.ok_or(XpComEwsError::Processing {
message: String::from("failed to parse message"),
})?;
populate_message_header_from_item(&hdr, &message)?;
unsafe { message_callbacks.CommitHeader(&*hdr) }.to_result()?;
Ok(hdr)
}

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

@ -0,0 +1,241 @@
/* 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/. */
use std::iter::IntoIterator;
use xpcom::interfaces::{nsMsgPriority, nsMsgPriorityValue};
/// A message from which email headers can be retrieved.
pub(crate) trait MessageHeaders {
/// The value of the `Message-ID` header for this message.
fn internet_message_id(&self) -> Option<String>;
/// Whether the message has already been read.
fn is_read(&self) -> Option<bool>;
/// Whether the message has any attachment.
fn has_attachments(&self) -> Option<bool>;
/// The time the message was sent, as a Unix timestamp converted to
/// milliseconds.
fn sent_timestamp_ms(&self) -> Option<i64>;
/// The author for this message. This can be the value of either the `From`
/// or `Sender` header (in order of preference).
fn author(&self) -> Option<ews::Mailbox>;
/// The `Reply-To` header for this message.
fn reply_to_recipient(&self) -> Option<ews::Mailbox>;
/// The `To` header for this message.
fn to_recipients(&self) -> Option<impl IntoIterator<Item = ews::Mailbox>>;
/// The `Cc` header for this message.
fn cc_recipients(&self) -> Option<impl IntoIterator<Item = ews::Mailbox>>;
/// The `Bcc` header for this message.
fn bcc_recipients(&self) -> Option<impl IntoIterator<Item = ews::Mailbox>>;
/// The `Subject` header for this message.
fn message_subject(&self) -> Option<String>;
/// The message's priority/importance. Might be represented by its
/// `X-Priority` header.
fn priority(&self) -> Option<nsMsgPriorityValue>;
}
impl MessageHeaders for &ews::Message {
fn internet_message_id(&self) -> Option<String> {
self.internet_message_id.clone()
}
fn is_read(&self) -> Option<bool> {
self.is_read
}
fn has_attachments(&self) -> Option<bool> {
self.has_attachments
}
fn sent_timestamp_ms(&self) -> Option<i64> {
self.date_time_sent.as_ref().and_then(|date_time| {
// `time` gives Unix timestamps in seconds. `PRTime` is an `i64`
// representing Unix timestamps in microseconds. `PRTime` won't overflow
// for over 500,000 years, but we use `checked_mul()` to guard against
// receiving nonsensical values.
let time_in_micros = date_time.0.unix_timestamp().checked_mul(1_000 * 1_000);
if time_in_micros.is_none() {
log::warn!(
"message with ID {item_id} sent date {date_time:?} too big for `i64`, ignoring",
item_id = self.item_id.id
);
}
time_in_micros
})
}
fn author(&self) -> Option<ews::Mailbox> {
self.from
.as_ref()
.or(self.sender.as_ref())
.and_then(|recipient| Some(recipient.mailbox.clone()))
}
fn reply_to_recipient(&self) -> Option<ews::Mailbox> {
self.reply_to
.as_ref()
.and_then(|recipient| Some(recipient.mailbox.clone()))
}
fn to_recipients(&self) -> Option<impl IntoIterator<Item = ews::Mailbox>> {
self.to_recipients
.as_ref()
.and_then(|recipients| Some(array_of_recipients_to_mailboxes(recipients)))
}
fn cc_recipients(&self) -> Option<impl IntoIterator<Item = ews::Mailbox>> {
self.cc_recipients
.as_ref()
.and_then(|recipients| Some(array_of_recipients_to_mailboxes(recipients)))
}
fn bcc_recipients(&self) -> Option<impl IntoIterator<Item = ews::Mailbox>> {
self.bcc_recipients
.as_ref()
.and_then(|recipients| Some(array_of_recipients_to_mailboxes(recipients)))
}
fn message_subject(&self) -> Option<String> {
self.subject.clone()
}
fn priority(&self) -> Option<nsMsgPriorityValue> {
self.importance.and_then(|importance| {
Some(match importance {
ews::Importance::Low => nsMsgPriority::low,
ews::Importance::Normal => nsMsgPriority::normal,
ews::Importance::High => nsMsgPriority::high,
})
})
}
}
impl MessageHeaders for &mail_parser::Message<'_> {
fn internet_message_id(&self) -> Option<String> {
self.message_id()
.and_then(|message_id| Some(message_id.to_string()))
}
fn is_read(&self) -> Option<bool> {
// TODO: read this value from the X-Mozilla-Status header
Some(false)
}
fn has_attachments(&self) -> Option<bool> {
Some(self.attachment_count() > 0)
}
fn sent_timestamp_ms(&self) -> Option<i64> {
self.date()
.and_then(|date_time| match date_time.to_timestamp().checked_mul(1_000 * 1_000) {
Some(timestamp_ms) => Some(timestamp_ms),
None => {
log::warn!(
"message with ID {item_id} sent date {date_time:?} too big for `i64`, ignoring",
item_id=self.message_id().or(Some("<none>")).unwrap()
);
None
},
})
}
fn author(&self) -> Option<ews::Mailbox> {
self.to()
.or(self.sender())
.and_then(|author| author.first())
.and_then(addr_to_maybe_mailbox)
}
fn reply_to_recipient(&self) -> Option<ews::Mailbox> {
self.reply_to()
.and_then(|reply_to| reply_to.first())
.and_then(addr_to_maybe_mailbox)
}
fn to_recipients(&self) -> Option<impl IntoIterator<Item = ews::Mailbox>> {
self.to()
.and_then(|address| Some(address_to_mailboxes(address)))
}
fn cc_recipients(&self) -> Option<impl IntoIterator<Item = ews::Mailbox>> {
self.cc()
.and_then(|address| Some(address_to_mailboxes(address)))
}
fn bcc_recipients(&self) -> Option<impl IntoIterator<Item = ews::Mailbox>> {
self.bcc()
.and_then(|address| Some(address_to_mailboxes(address)))
}
fn message_subject(&self) -> Option<String> {
self.subject().and_then(|subject| Some(subject.to_string()))
}
fn priority(&self) -> Option<nsMsgPriorityValue> {
self.header("X-Priority")
.and_then(|value| value.as_text())
.and_then(|value| value.trim().chars().nth(0))
.and_then(|first_char| {
Some(match first_char {
// Annoyingly, the indices in nsMsgPriority don't match with the
// integer values in the header. These pairings come from
// https://people.dsv.su.se/~jpalme/ietf/ietf-mail-attributes.html#Heading14,
// and `NS_MsgGetPriorityFromString`.
'1' => nsMsgPriority::highest,
'2' => nsMsgPriority::high,
'3' => nsMsgPriority::normal,
'4' => nsMsgPriority::low,
'5' => nsMsgPriority::lowest,
_ => nsMsgPriority::Default,
})
})
}
}
/// Turns a `mail_parser::Address` into a vector of `ews::Mailbox`es, filtering
/// out any that does not have an e-mail address.
fn address_to_mailboxes(address: &mail_parser::Address) -> Vec<ews::Mailbox> {
address
.clone()
.into_list()
.iter()
.filter_map(addr_to_maybe_mailbox)
.collect()
}
/// Turns the given `mail_parser::Addr` into an `ews::Mailbox`, if its email
/// address is not `None`.
fn addr_to_maybe_mailbox(addr: &mail_parser::Addr) -> Option<ews::Mailbox> {
match addr.address() {
Some(address) => Some(ews::Mailbox {
name: addr
.name
.as_ref()
.and_then(|name| Some(name.clone().into_owned())),
email_address: address.into(),
..Default::default()
}),
None => None,
}
}
/// Turns an `ews::ArrayOfRecipients` into a vector of `ews::Mailbox`es.
fn array_of_recipients_to_mailboxes(recipients: &ews::ArrayOfRecipients) -> Vec<ews::Mailbox> {
recipients
.iter()
.map(|recipient| recipient.mailbox.clone())
.collect()
}

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

@ -6,25 +6,29 @@ extern crate xpcom;
use std::{cell::OnceCell, ffi::c_void};
use authentication::credentials::{AuthenticationProvider, Credentials};
use client::XpComEwsClient;
use url::Url;
use nserror::{
nsresult, NS_ERROR_ALREADY_INITIALIZED, NS_ERROR_INVALID_ARG, NS_ERROR_NOT_INITIALIZED, NS_OK,
};
use nsstring::nsACString;
use url::Url;
use xpcom::{
interfaces::{
nsIMsgIncomingServer, nsIRequest, nsIStreamListener, IEwsFolderCallbacks,
IEwsMessageCallbacks,
nsIInputStream, nsIMsgCopyServiceListener, nsIMsgIncomingServer, nsIRequest,
nsIStreamListener, IEwsFolderCallbacks, IEwsMessageCallbacks,
},
nsIID, xpcom_method, RefPtr,
};
use authentication::credentials::{AuthenticationProvider, Credentials};
use client::XpComEwsClient;
mod authentication;
mod cancellable_request;
mod client;
mod headers;
mod outgoing;
mod xpcom_io;
/// Creates a new instance of the XPCOM/EWS bridge interface [`XpcomEwsBridge`].
#[allow(non_snake_case)]
@ -154,6 +158,36 @@ 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(
&self,
folder_id: &nsACString,
is_draft: bool,
message_stream: &nsIInputStream,
copy_listener: &nsIMsgCopyServiceListener,
message_callbacks: &IEwsMessageCallbacks,
) -> Result<(), nsresult> {
let content = crate::xpcom_io::read_stream(message_stream)?;
let client = self.try_new_client()?;
// The client operation is async and we want it to survive the end of
// this scope, so spawn it as a detached `moz_task`.
moz_task::spawn_local(
"save_message",
client.save_message(
folder_id.to_utf8().into(),
is_draft,
content,
RefPtr::new(copy_listener),
RefPtr::new(message_callbacks),
),
)
.detach();
Ok(())
}
/// Gets a new EWS client if initialized.
fn try_new_client(&self) -> Result<XpComEwsClient, nsresult> {
// We only get a reference out of the cell, but we need ownership in

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

@ -4,7 +4,7 @@
use std::cell::{OnceCell, RefCell};
use std::ffi::CString;
use std::os::raw::{c_char, c_void};
use std::os::raw::c_void;
use std::ptr;
use ews::{Mailbox, Recipient};
@ -18,16 +18,16 @@ use url::Url;
use xpcom::{create_instance, get_service, getter_addrefs, nsIID};
use xpcom::{
interfaces::{
msgIAddressObject, msgIOAuth2Module, nsIFile, nsIFileInputStream, nsIIOService,
nsIMsgIdentity, nsIMsgStatusFeedback, nsIMsgWindow, nsIPrefBranch, nsIPrefService,
nsIRequestObserver, nsIURI, nsIUrlListener, nsMsgAuthMethodValue, nsMsgSocketType,
nsMsgSocketTypeValue,
msgIAddressObject, msgIOAuth2Module, nsIFile, nsIIOService, nsIMsgIdentity,
nsIMsgStatusFeedback, nsIMsgWindow, nsIPrefBranch, nsIPrefService, nsIRequestObserver,
nsIURI, nsIUrlListener, nsMsgAuthMethodValue, nsMsgSocketType, nsMsgSocketTypeValue,
},
xpcom_method, RefPtr,
};
use crate::authentication::credentials::AuthenticationProvider;
use crate::client::XpComEwsClient;
use crate::xpcom_io;
/// Whether a field is required to have a value (either in memory or in a pref)
/// upon access.
@ -544,7 +544,7 @@ impl EwsOutgoingServer {
message_id: &nsACString,
observer: &nsIRequestObserver,
) -> Result<(), nsresult> {
let message_content = read_file(file_path)?;
let message_content = xpcom_io::read_file(file_path)?;
let message_content =
String::from_utf8(message_content).or(Err(nserror::NS_ERROR_FAILURE))?;
@ -685,47 +685,3 @@ impl AuthenticationProvider for &EwsOutgoingServer {
Ok(oauth2_supported.then_some(oauth2_module))
}
}
/// Open the file provided and read its content into a vector of bytes.
fn read_file(file: &nsIFile) -> Result<Vec<u8>, nsresult> {
let file_stream =
create_instance::<nsIFileInputStream>(cstr!("@mozilla.org/network/file-input-stream;1"))
.ok_or(nserror::NS_ERROR_FAILURE)?;
// Open a stream from the file, and figure out how many bytes can be read
// from it.
let mut bytes_available = 0;
unsafe {
file_stream
.Init(file, -1, -1, nsIFileInputStream::CLOSE_ON_EOF)
.to_result()?;
file_stream.Available(&mut bytes_available)
}
.to_result()?;
// `nsIInputStream::Available` reads into a u64, but `nsIInputStream::Read`
// takes a u32.
let bytes_available = <u32>::try_from(bytes_available).or(Err(nserror::NS_ERROR_FAILURE))?;
let mut read_sink: Vec<u8> =
vec![0; <usize>::try_from(bytes_available).or(Err(nserror::NS_ERROR_FAILURE))?];
// The amount of bytes actually read from the stream.
let mut bytes_read: u32 = 0;
// SAFETY: The call contract from `nsIInputStream::Read` guarantees that the
// bytes written into the provided buffer is of type c_char (char* in
// C-land) and is contiguous for the length it writes in `bytes_read`; and
// that `bytes_read` is not greater than `bytes_available`.
unsafe {
let read_ptr = read_sink.as_mut_ptr();
file_stream
.Read(read_ptr as *mut c_char, bytes_available, &mut bytes_read)
.to_result()?;
};
let bytes_read = <usize>::try_from(bytes_read).or(Err(nserror::NS_ERROR_FAILURE))?;
Ok(Vec::from(&read_sink[..bytes_read]))
}

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

@ -0,0 +1,57 @@
/* 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/. */
use std::os::raw::c_char;
use cstr::cstr;
use nserror::nsresult;
use xpcom::create_instance;
use xpcom::interfaces::{nsIFile, nsIFileInputStream, nsIInputStream};
/// Open the file provided and read its content into a vector of bytes.
pub(crate) fn read_file(file: &nsIFile) -> Result<Vec<u8>, nsresult> {
// Open a stream from the file.
let file_stream =
create_instance::<nsIFileInputStream>(cstr!("@mozilla.org/network/file-input-stream;1"))
.ok_or(nserror::NS_ERROR_FAILURE)?;
unsafe { file_stream.Init(file, -1, -1, nsIFileInputStream::CLOSE_ON_EOF) }.to_result()?;
// Read as many bytes as available from the stream.
read_stream(file_stream.coerce())
}
pub(crate) fn read_stream(stream: &nsIInputStream) -> Result<Vec<u8>, nsresult> {
let mut bytes_available = 0;
unsafe { stream.Available(&mut bytes_available) }.to_result()?;
// `nsIInputStream::Available` reads into a u64, but `nsIInputStream::Read`
// takes a u32.
let bytes_available = <u32>::try_from(bytes_available).or(Err(nserror::NS_ERROR_FAILURE))?;
let mut read_sink: Vec<u8> =
vec![0; <usize>::try_from(bytes_available).or(Err(nserror::NS_ERROR_FAILURE))?];
// The amount of bytes actually read from the stream.
let mut bytes_read: u32 = 0;
// SAFETY: The call contract from `nsIInputStream::Read` guarantees that the
// bytes written into the provided buffer is of type c_char (char* in
// C-land) and is contiguous for the length it writes in `bytes_read`; and
// that `bytes_read` is not greater than `bytes_available`.
unsafe {
let read_ptr = read_sink.as_mut_ptr();
stream
.Read(read_ptr as *mut c_char, bytes_available, &mut bytes_read)
.to_result()?;
};
// TODO: We currently assume all of the data we care about is in the stream
// when we read, which might not be the case if we're copying multiple
// messages.
let bytes_read = <usize>::try_from(bytes_read).or(Err(nserror::NS_ERROR_FAILURE))?;
Ok(Vec::from(&read_sink[..bytes_read]))
}

2
third_party/rust/ews/.cargo-checksum.json поставляемый
Просмотреть файл

@ -1 +1 @@
{"files":{".github/workflows/ci.yaml":"86a41c10a1b90620bb4548c8f18d082e906c2274e8f1d951568e4c070b10d8cb","Cargo.toml":"ea5a29dc84fb226255c22e0aa2bafb83353d671970f2990c19226a61e52a1d05","LICENSE":"3f3d9e0024b1921b067d6f7f88deb4a60cbe7a78e76c64e3f1d7fc3b779b9d04","README.md":"eebefef86e483df98c6b1104101348293fbacbd16639f15044ca37d2949b68f1","src/lib.rs":"20e850b188c53ad1c5d533a4a7d797fd88a18b0aa1ca1486b8cd839ca94749b6","src/types.rs":"afa4bc3d11fdfdfd25fa23d50d2541d04087f0918609fad5db737b68cc7a73d1","src/types/common.rs":"b96b84ef16bc268762ae478e0d5634f270017dda4e107f82ca83f475eda9d3bb","src/types/create_item.rs":"6db9e7d7b0d9bdf1ecb4a5ceae187cf6eb76488214ddf64be58567a5d5536339","src/types/get_folder.rs":"4b26621d2efd40a406afabbd6e4c092c7aafd73e07c11e8cbdad19bed205d238","src/types/get_item.rs":"401f60da2ffb7ccda1dda56e25876c640851597bb137ce34a7f7eb0fc94508d4","src/types/operations.rs":"69a1f976f5fca24bff77765003dc8345df2aebd3f632f3480f41fd9d2011c3c1","src/types/soap.rs":"640b49ecd88d06054e71b8c464e13f34e1010e8bce942158e9d8ede1fd35e212","src/types/soap/de.rs":"6fb603f521a73984e5707988379e562018b179df54647cff89d8ab03c406cff2","src/types/sync_folder_hierarchy.rs":"1f219d9bda6f4685ba962ff0cb891a9925e6a5c7d159d0a3f4561ca8439a71d5","src/types/sync_folder_items.rs":"e8548d6e9b6b847bfafc399b7168092875b7bebf5fca74b47d082a0dc7ef53d1"},"package":null}
{"files":{".github/workflows/ci.yaml":"86a41c10a1b90620bb4548c8f18d082e906c2274e8f1d951568e4c070b10d8cb","Cargo.toml":"ea5a29dc84fb226255c22e0aa2bafb83353d671970f2990c19226a61e52a1d05","LICENSE":"3f3d9e0024b1921b067d6f7f88deb4a60cbe7a78e76c64e3f1d7fc3b779b9d04","README.md":"eebefef86e483df98c6b1104101348293fbacbd16639f15044ca37d2949b68f1","src/lib.rs":"20e850b188c53ad1c5d533a4a7d797fd88a18b0aa1ca1486b8cd839ca94749b6","src/types.rs":"afa4bc3d11fdfdfd25fa23d50d2541d04087f0918609fad5db737b68cc7a73d1","src/types/common.rs":"b96b84ef16bc268762ae478e0d5634f270017dda4e107f82ca83f475eda9d3bb","src/types/create_item.rs":"a50a1ae6ee62c6d2d4003e13b72e5a8e194c0df9f1454c5f7c4ae7f3929ccae6","src/types/get_folder.rs":"4b26621d2efd40a406afabbd6e4c092c7aafd73e07c11e8cbdad19bed205d238","src/types/get_item.rs":"401f60da2ffb7ccda1dda56e25876c640851597bb137ce34a7f7eb0fc94508d4","src/types/operations.rs":"69a1f976f5fca24bff77765003dc8345df2aebd3f632f3480f41fd9d2011c3c1","src/types/soap.rs":"640b49ecd88d06054e71b8c464e13f34e1010e8bce942158e9d8ede1fd35e212","src/types/soap/de.rs":"6fb603f521a73984e5707988379e562018b179df54647cff89d8ab03c406cff2","src/types/sync_folder_hierarchy.rs":"1f219d9bda6f4685ba962ff0cb891a9925e6a5c7d159d0a3f4561ca8439a71d5","src/types/sync_folder_items.rs":"e8548d6e9b6b847bfafc399b7168092875b7bebf5fca74b47d082a0dc7ef53d1"},"package":null}

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

@ -35,7 +35,7 @@ pub struct CreateItem {
///
/// See <https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/createitem#messagedisposition-attribute>
#[xml_struct(attribute)]
pub message_disposition: MessageDisposition,
pub message_disposition: Option<MessageDisposition>,
/// The folder in which to store an item once it has been created.
///