From cad3dd7d0763ddfeb336481fd2ba0c5632c932f5 Mon Sep 17 00:00:00 2001 From: Sean Burke Date: Thu, 11 Apr 2024 13:57:21 -0700 Subject: [PATCH] Add support for parsing soap:Fault from error responses. Closes https://github.com/thunderbird/ews-rs/pull/5 --- src/types/get_folder.rs | 43 ++++++++++++--- src/types/operations.rs | 63 +++++++++++----------- src/types/soap.rs | 85 +++++++++++++++++++---------- src/types/soap/de.rs | 86 ++++++++++++++++++++++++++++++ src/types/sync_folder_hierarchy.rs | 74 +++++++++++++++++++++---- 5 files changed, 274 insertions(+), 77 deletions(-) create mode 100644 src/types/soap/de.rs diff --git a/src/types/get_folder.rs b/src/types/get_folder.rs index e53a3de..82d3390 100644 --- a/src/types/get_folder.rs +++ b/src/types/get_folder.rs @@ -5,18 +5,36 @@ use serde::Deserialize; use xml_struct::XmlSerialize; -use crate::{BaseFolderId, Folder, FolderShape, ResponseClass}; +use crate::{ + types::sealed::EnvelopeBodyContents, BaseFolderId, Folder, FolderShape, Operation, + OperationResponse, ResponseClass, MESSAGES_NS_URI, +}; -/// The request to get one or more folder(s). +/// A request to get information on one or more folders. /// /// See #[derive(Debug, XmlSerialize)] +#[xml_struct(default_ns = MESSAGES_NS_URI)] pub struct GetFolder { + /// A description of the information to be included in the response for each + /// retrieved folder. pub folder_shape: FolderShape, + + /// A list of IDs for which to retrieve folder information. pub folder_ids: Vec, } -/// The response to a GetFolder request. +impl Operation for GetFolder { + type Response = GetFolderResponse; +} + +impl EnvelopeBodyContents for GetFolder { + fn name() -> &'static str { + "GetFolder" + } +} + +/// A response to a [`GetFolder`] request. /// /// See #[derive(Debug, Deserialize)] @@ -25,25 +43,38 @@ pub struct GetFolderResponse { pub response_messages: ResponseMessages, } -/// A collection of response messages from a GetFolder response. +impl OperationResponse for GetFolderResponse {} + +impl EnvelopeBodyContents for GetFolderResponse { + fn name() -> &'static str { + "GetFolderResponse" + } +} + +/// A collection of responses for individual entities within a request. +/// +/// See #[derive(Debug, Deserialize)] #[serde(rename_all = "PascalCase")] pub struct ResponseMessages { pub get_folder_response_message: Vec, } -/// A message in a GetFolder response. +/// A response to a request for an individual folder within a [`GetFolder`] operation. /// /// See #[derive(Debug, Deserialize)] #[serde(rename_all = "PascalCase")] pub struct GetFolderResponseMessage { + /// The success value of the corresponding request. #[serde(rename = "@ResponseClass")] pub response_class: ResponseClass, + + /// A collection of the retrieved folders. pub folders: Folders, } -/// A list of folders in a GetFolder response message. +/// A collection of information on Exchange folders. /// /// See #[derive(Debug, Deserialize)] diff --git a/src/types/operations.rs b/src/types/operations.rs index 97f8994..857a647 100644 --- a/src/types/operations.rs +++ b/src/types/operations.rs @@ -5,38 +5,39 @@ use serde::Deserialize; use xml_struct::XmlSerialize; -use crate::{ - get_folder::{GetFolder, GetFolderResponse}, - sync_folder_hierarchy::{SyncFolderHierarchy, SyncFolderHierarchyResponse}, - MESSAGES_NS_URI, -}; - -/// Available EWS operations (requests) that can be performed against an -/// Exchange server. -#[derive(Debug, XmlSerialize)] -#[xml_struct(default_ns = MESSAGES_NS_URI)] -pub enum Operation { - /// Retrieve information regarding one or more folder(s). - /// - /// See - GetFolder(GetFolder), - - /// Retrieve the latest changes in the folder hierarchy for this mailbox. - /// - /// See - SyncFolderHierarchy(SyncFolderHierarchy), +/// A marker trait for EWS operations. +/// +/// Types implementing this trait may appear in requests to EWS as the operation +/// to be performed. +/// +/// # Usage +/// +/// See [`Envelope`] for details. +/// +/// [`Envelope`]: crate::soap::Envelope +pub trait Operation: XmlSerialize + sealed::EnvelopeBodyContents { + /// The structure returned by EWS in response to requests containing this + /// operation. + type Response: OperationResponse; } -/// Responses to available operations. -#[derive(Debug, Deserialize)] -pub enum OperationResponse { - /// The response to a GetFolder operation. - /// - /// See - GetFolderResponse(GetFolderResponse), +/// A marker trait for EWS operation responses. +/// +/// Types implementing this trait may appear in responses from EWS after +/// requesting an operation be performed. +/// +/// # Usage +/// +/// See [`Envelope`] for details. +/// +/// [`Envelope`]: crate::soap::Envelope +pub trait OperationResponse: for<'de> Deserialize<'de> + sealed::EnvelopeBodyContents {} - /// The response to a SyncFolderHierarchy operation. - /// - /// See - SyncFolderHierarchyResponse(SyncFolderHierarchyResponse), +pub(super) mod sealed { + /// A trait for structures which may appear in the body of a SOAP envelope. + pub trait EnvelopeBodyContents { + /// Gets the name of the element enclosing the contents of this + /// structure when represented in XML. + fn name() -> &'static str; + } } diff --git a/src/types/soap.rs b/src/types/soap.rs index 1d89e19..b4dda98 100644 --- a/src/types/soap.rs +++ b/src/types/soap.rs @@ -6,12 +6,15 @@ use quick_xml::{ events::{BytesDecl, BytesEnd, BytesStart, Event}, Reader, Writer, }; -use serde::Deserialize; -use xml_struct::XmlSerialize; -use crate::{Error, MessageXml, ResponseCode, SOAP_NS_URI, TYPES_NS_URI}; +use crate::{ + Error, MessageXml, Operation, OperationResponse, ResponseCode, SOAP_NS_URI, TYPES_NS_URI, +}; + +mod de; +use self::de::DeserializeEnvelope; -/// A SOAP envelope wrapping an EWS operation. +/// A SOAP envelope containing the body of an EWS operation or response. /// /// See #[derive(Debug)] @@ -21,10 +24,13 @@ pub struct Envelope { impl Envelope where - B: XmlSerialize, + B: Operation, { /// Serializes the SOAP envelope as a complete XML document. pub fn as_xml_document(&self) -> Result, Error> { + const SOAP_ENVELOPE: &str = "soap:Envelope"; + const SOAP_BODY: &str = "soap:Body"; + let mut writer = { let inner: Vec = Default::default(); Writer::new(inner) @@ -33,16 +39,19 @@ where // All EWS examples use XML 1.0 with UTF-8, so stick to that for now. writer.write_event(Event::Decl(BytesDecl::new("1.0", Some("utf-8"), None)))?; - // To get around having to make `Envelope` itself implement - // `XmlSerialize` + // We manually write these elements in order to control the name we + // write the body with. writer.write_event(Event::Start( - BytesStart::new("soap:Envelope") + BytesStart::new(SOAP_ENVELOPE) .with_attributes([("xmlns:soap", SOAP_NS_URI), ("xmlns:t", TYPES_NS_URI)]), ))?; + writer.write_event(Event::Start(BytesStart::new(SOAP_BODY)))?; - self.body.serialize_as_element(&mut writer, "soap:Body")?; + // Write the operation itself. + self.body.serialize_as_element(&mut writer, B::name())?; - writer.write_event(Event::End(BytesEnd::new("soap:Envelope")))?; + writer.write_event(Event::End(BytesEnd::new(SOAP_BODY)))?; + writer.write_event(Event::End(BytesEnd::new(SOAP_ENVELOPE)))?; Ok(writer.into_inner()) } @@ -50,22 +59,10 @@ where impl Envelope where - B: for<'de> Deserialize<'de>, + B: OperationResponse, { /// Populates an [`Envelope`] from raw XML. pub fn from_xml_document(document: &[u8]) -> Result { - #[derive(Deserialize)] - #[serde(rename_all = "PascalCase")] - struct DummyEnvelope { - body: DummyBody, - } - - #[derive(Deserialize)] - struct DummyBody { - #[serde(rename = "$value")] - inner: T, - } - // The body of an envelope can contain a fault, indicating an error with // a request. We want to parse that and return it as the `Err` portion // of a result. However, Microsoft includes a field in their fault @@ -73,17 +70,17 @@ where // containing `xs:any`, meaning there is no documented schema for its // contents. However, it may contain details relevant for debugging, so // we want to capture it. Since we don't know what it contains, we - // settle for capturing it as XML test, but serde doesn't give us a nice + // settle for capturing it as XML text, but serde doesn't give us a nice // way of doing that, so we perform this step separately. let fault = extract_maybe_fault(document)?; if let Some(fault) = fault { return Err(Error::RequestFault(Box::new(fault))); } - let envelope: DummyEnvelope = quick_xml::de::from_reader(document)?; + let envelope: DeserializeEnvelope = quick_xml::de::from_reader(document)?; Ok(Envelope { - body: envelope.body.inner, + body: envelope.body, }) } } @@ -391,7 +388,7 @@ pub struct FaultDetail { mod tests { use serde::Deserialize; - use crate::Error; + use crate::{types::sealed::EnvelopeBodyContents, Error, OperationResponse}; use super::Envelope; @@ -405,6 +402,14 @@ mod tests { _other_field: (), } + impl OperationResponse for SomeStruct {} + + impl EnvelopeBodyContents for SomeStruct { + fn name() -> &'static str { + "Foo" + } + } + // This XML is contrived, with a custom structure defined in order to // test the generic behavior of the interface. let xml = r#"testing content"#; @@ -421,10 +426,21 @@ mod tests { #[test] fn deserialize_envelope_with_schema_fault() { + #[derive(Debug, Deserialize)] + struct Foo; + + impl OperationResponse for Foo {} + + impl EnvelopeBodyContents for Foo { + fn name() -> &'static str { + "Foo" + } + } + // This XML is drawn from testing data for `evolution-ews`. let xml = r#"a:ErrorSchemaValidationThe request failed schema validation: The 'Id' attribute is invalid - The value 'invalidparentid' is invalid according to its datatype 'http://schemas.microsoft.com/exchange/services/2006/types:DistinguishedFolderIdNameType' - The Enumeration constraint failed.ErrorSchemaValidationThe request failed schema validation.2630The 'Id' attribute is invalid - The value 'invalidparentid' is invalid according to its datatype 'http://schemas.microsoft.com/exchange/services/2006/types:DistinguishedFolderIdNameType' - The Enumeration constraint failed."#; - let err = >::from_xml_document(xml.as_bytes()) + let err = >::from_xml_document(xml.as_bytes()) .expect_err("should return error when body contains fault"); if let Error::RequestFault(fault) = err { @@ -463,12 +479,23 @@ mod tests { #[test] fn deserialize_envelope_with_server_busy_fault() { + #[derive(Debug, Deserialize)] + struct Foo; + + impl OperationResponse for Foo {} + + impl EnvelopeBodyContents for Foo { + fn name() -> &'static str { + "Foo" + } + } + // This XML is contrived based on what's known of the shape of // `ErrorServerBusy` responses. It should be replaced when we have // real-life examples. let xml = r#"a:ErrorServerBusyI made this up because I don't have real testing data. 🙃ErrorServerBusyWho really knows?25"#; - let err = >::from_xml_document(xml.as_bytes()) + let err = >::from_xml_document(xml.as_bytes()) .expect_err("should return error when body contains fault"); // The testing here isn't as thorough as the invalid schema test due to diff --git a/src/types/soap/de.rs b/src/types/soap/de.rs new file mode 100644 index 0000000..8ef577c --- /dev/null +++ b/src/types/soap/de.rs @@ -0,0 +1,86 @@ +/* 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::marker::PhantomData; + +use serde::{de::Visitor, Deserialize, Deserializer}; + +use crate::OperationResponse; + +/// A helper for deserialization of SOAP envelopes. +/// +/// This struct is declared separately from the more general [`Envelope`] type +/// so that the latter can be used with types that are write-only. +/// +/// [`Envelope`]: super::Envelope +#[derive(Deserialize)] +#[serde(rename_all = "PascalCase")] +pub(super) struct DeserializeEnvelope +where + T: OperationResponse, +{ + #[serde(deserialize_with = "deserialize_body")] + pub body: T, +} + +fn deserialize_body<'de, D, T>(body: D) -> Result +where + D: Deserializer<'de>, + T: OperationResponse, +{ + body.deserialize_map(BodyVisitor::(PhantomData)) +} + +/// A visitor for custom name-based deserialization of operation responses. +struct BodyVisitor(PhantomData); + +impl<'de, T> Visitor<'de> for BodyVisitor +where + T: OperationResponse, +{ + type Value = T; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("EWS operation response body") + } + + fn visit_map(self, mut map: A) -> Result + where + A: serde::de::MapAccess<'de>, + { + match map.next_key::()? { + Some(name) => { + // We expect the body of the response to contain a single + // element with the name of the expected operation response. + let expected = T::name(); + if name.as_str() != expected { + return Err(serde::de::Error::custom(format_args!( + "unknown element `{}`, expected {}", + name, expected + ))); + } + + let value = map.next_value()?; + + // To satisfy quick-xml's serde impl, we need to consume the + // final `None` key value in order to successfully complete. + match map.next_key::()? { + Some(name) => { + // The response body contained more than one element, + // which violates our expectations. + Err(serde::de::Error::custom(format_args!( + "unexpected element `{}`", + name + ))) + } + None => Ok(value), + } + } + None => Err(serde::de::Error::invalid_type( + serde::de::Unexpected::Map, + &self, + )), + } + } +} diff --git a/src/types/sync_folder_hierarchy.rs b/src/types/sync_folder_hierarchy.rs index b97c06e..f986f24 100644 --- a/src/types/sync_folder_hierarchy.rs +++ b/src/types/sync_folder_hierarchy.rs @@ -5,19 +5,45 @@ use serde::Deserialize; use xml_struct::XmlSerialize; -use crate::{BaseFolderId, Folder, FolderId, FolderShape, ResponseClass}; +use crate::{ + types::sealed::EnvelopeBodyContents, BaseFolderId, Folder, FolderId, FolderShape, Operation, + OperationResponse, ResponseClass, MESSAGES_NS_URI, +}; -/// The request for update regarding the folder hierarchy in a mailbox. +/// A request for a list of folders which have been created, updated, or deleted +/// server-side. /// /// See #[derive(Debug, XmlSerialize)] +#[xml_struct(default_ns = MESSAGES_NS_URI)] pub struct SyncFolderHierarchy { + /// A description of the information to be included in the response for each + /// changed folder. pub folder_shape: FolderShape, + + /// The ID of the folder to sync. pub sync_folder_id: Option, + + /// The synchronization state after which to list changes. + /// + /// If `None`, the response will include `Create` changes for each folder + /// which is a descendant of the requested folder. + /// + /// See pub sync_state: Option, } -/// The response to a SyncFolderHierarchy request. +impl Operation for SyncFolderHierarchy { + type Response = SyncFolderHierarchyResponse; +} + +impl EnvelopeBodyContents for SyncFolderHierarchy { + fn name() -> &'static str { + "SyncFolderHierarchy" + } +} + +/// A response to a [`SyncFolderHierarchy`] request. /// /// See #[derive(Debug, Deserialize)] @@ -26,27 +52,48 @@ pub struct SyncFolderHierarchyResponse { pub response_messages: ResponseMessages, } -/// A collection of response messages from a SyncFolderHierarchy response. +impl OperationResponse for SyncFolderHierarchyResponse {} + +impl EnvelopeBodyContents for SyncFolderHierarchyResponse { + fn name() -> &'static str { + "SyncFolderHierarchyResponse" + } +} + +/// A collection of responses for individual entities within a request. +/// +/// See #[derive(Debug, Deserialize)] #[serde(rename_all = "PascalCase")] pub struct ResponseMessages { pub sync_folder_hierarchy_response_message: Vec, } -/// A message in a SyncFolderHierarchy response. +/// A response to a request for an individual folder within a [`SyncFolderHierarchy`] operation. /// /// See #[derive(Debug, Deserialize)] #[serde(rename_all = "PascalCase")] pub struct SyncFolderHierarchyResponseMessage { + /// The success value of the corresponding request. #[serde(rename = "@ResponseClass")] pub response_class: ResponseClass, + + /// An identifier for the synchronization state following application of the + /// changes included in this response. pub sync_state: String, + + /// Whether all relevant folder changes have been synchronized following + /// this response. pub includes_last_folder_in_range: bool, + + /// The collection of changes between the prior synchronization state and + /// the one represented by this response. pub changes: Changes, } -/// The changes that happened since the last folder hierachy sync. +/// A sequentially-ordered collection of folder creations, updates, and +/// deletions. /// /// See #[derive(Debug, Deserialize)] @@ -55,29 +102,34 @@ pub struct Changes { pub inner: Vec, } -/// A single change described in a SyncFolderHierarchy response message. +/// A server-side change to a folder. /// /// See #[derive(Debug, Deserialize)] pub enum Change { - /// A folder to create. + /// A creation of a folder. /// /// See Create { + /// The state of the folder upon creation. #[serde(rename = "$value")] folder: Folder, }, - /// A folder to update. + /// An update to a folder. /// /// See Update { + /// The updated state of the folder. #[serde(rename = "$value")] folder: Folder, }, - /// A folder to delete. + /// A deletion of a folder. /// /// See - Delete(FolderId), + Delete( + /// The EWS ID for the deleted folder. + FolderId, + ), }