Skip to content

Commit

Permalink
Add support for parsing soap:Fault from error responses.
Browse files Browse the repository at this point in the history
Closes #5
  • Loading branch information
leftmostcat committed Apr 17, 2024
1 parent 1c627f1 commit cad3dd7
Show file tree
Hide file tree
Showing 5 changed files with 274 additions and 77 deletions.
43 changes: 37 additions & 6 deletions src/types/get_folder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/getfolder>
#[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<BaseFolderId>,
}

/// 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 <https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/getfolderresponse>
#[derive(Debug, Deserialize)]
Expand All @@ -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 <https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/responsemessages>
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct ResponseMessages {
pub get_folder_response_message: Vec<GetFolderResponseMessage>,
}

/// A message in a GetFolder response.
/// A response to a request for an individual folder within a [`GetFolder`] operation.
///
/// See <https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/getfolderresponsemessage>
#[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 <https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/folders-ex15websvcsotherref>
#[derive(Debug, Deserialize)]
Expand Down
63 changes: 32 additions & 31 deletions src/types/operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/getfolder-operation#getfolder-request-example>
GetFolder(GetFolder),

/// Retrieve the latest changes in the folder hierarchy for this mailbox.
///
/// See <https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/syncfolderhierarchy-operation#syncfolderhierarchy-request-example>
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 <https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/getfolder-operation#getfolder-response-example>
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 <https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/syncfolderhierarchy-operation#successful-syncfolderhierarchy-response>
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;
}
}
85 changes: 56 additions & 29 deletions src/types/soap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://www.w3.org/TR/2000/NOTE-SOAP-20000508/#_Toc478383494>
#[derive(Debug)]
Expand All @@ -21,10 +24,13 @@ pub struct Envelope<B> {

impl<B> Envelope<B>
where
B: XmlSerialize,
B: Operation,
{
/// Serializes the SOAP envelope as a complete XML document.
pub fn as_xml_document(&self) -> Result<Vec<u8>, Error> {
const SOAP_ENVELOPE: &str = "soap:Envelope";
const SOAP_BODY: &str = "soap:Body";

let mut writer = {
let inner: Vec<u8> = Default::default();
Writer::new(inner)
Expand All @@ -33,57 +39,48 @@ 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())
}
}

impl<B> Envelope<B>
where
B: for<'de> Deserialize<'de>,
B: OperationResponse,
{
/// Populates an [`Envelope`] from raw XML.
pub fn from_xml_document(document: &[u8]) -> Result<Self, Error> {
#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
struct DummyEnvelope<T> {
body: DummyBody<T>,
}

#[derive(Deserialize)]
struct DummyBody<T> {
#[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
// responses called `MessageXml` which is explicitly documented as
// 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<B> = quick_xml::de::from_reader(document)?;
let envelope: DeserializeEnvelope<B> = quick_xml::de::from_reader(document)?;

Ok(Envelope {
body: envelope.body.inner,
body: envelope.body,
})
}
}
Expand Down Expand Up @@ -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;

Expand All @@ -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#"<?xml version="1.0" encoding="utf-8"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"><s:Body><foo:Foo><text>testing content</text><other_field/></foo:Foo></s:Body></s:Envelope>"#;
Expand All @@ -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#"<?xml version="1.0" encoding="utf-8"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"><s:Body><s:Fault><faultcode xmlns:a="http://schemas.microsoft.com/exchange/services/2006/types">a:ErrorSchemaValidation</faultcode><faultstring xml:lang="en-US">The 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.</faultstring><detail><e:ResponseCode xmlns:e="http://schemas.microsoft.com/exchange/services/2006/errors">ErrorSchemaValidation</e:ResponseCode><e:Message xmlns:e="http://schemas.microsoft.com/exchange/services/2006/errors">The request failed schema validation.</e:Message><t:MessageXml xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types"><t:LineNumber>2</t:LineNumber><t:LinePosition>630</t:LinePosition><t:Violation>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.</t:Violation></t:MessageXml></detail></s:Fault></s:Body></s:Envelope>"#;

let err = <Envelope<()>>::from_xml_document(xml.as_bytes())
let err = <Envelope<Foo>>::from_xml_document(xml.as_bytes())
.expect_err("should return error when body contains fault");

if let Error::RequestFault(fault) = err {
Expand Down Expand Up @@ -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#"<?xml version="1.0" encoding="utf-8"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"><s:Body><s:Fault><faultcode xmlns:a="http://schemas.microsoft.com/exchange/services/2006/types">a:ErrorServerBusy</faultcode><faultstring xml:lang="en-US">I made this up because I don't have real testing data. 🙃</faultstring><detail><e:ResponseCode xmlns:e="http://schemas.microsoft.com/exchange/services/2006/errors">ErrorServerBusy</e:ResponseCode><e:Message xmlns:e="http://schemas.microsoft.com/exchange/services/2006/errors">Who really knows?</e:Message><t:MessageXml xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types"><t:Value Name="BackOffMilliseconds">25</t:Value></t:MessageXml></detail></s:Fault></s:Body></s:Envelope>"#;

let err = <Envelope<()>>::from_xml_document(xml.as_bytes())
let err = <Envelope<Foo>>::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
Expand Down
Loading

0 comments on commit cad3dd7

Please sign in to comment.