use de::EnvelopeContent;
use quick_xml::{
events::{BytesDecl, BytesEnd, BytesStart, Event},
Writer,
};
use serde::Deserialize;
use xml_struct::XmlSerialize;
use crate::{
response::ResponseCode, types::sealed, types::server_version, Error, MessageXml, Operation,
OperationResponse, SOAP_NS_URI, TYPES_NS_URI,
};
mod de;
use self::de::DeserializeEnvelope;
use super::server_version::ExchangeServerVersion;
#[derive(Clone, Debug, Deserialize, XmlSerialize)]
#[xml_struct(variant_ns_prefix = "t")]
#[non_exhaustive]
pub enum Header {
RequestServerVersion {
#[xml_struct(attribute)]
#[serde(rename = "@Version")]
version: ExchangeServerVersion,
},
ServerVersionInfo(server_version::ServerVersionInfo),
}
#[derive(Clone, Debug)]
pub struct Envelope<B> {
pub headers: Vec<Header>,
pub body: B,
}
impl<B> Envelope<B>
where
B: Operation,
{
pub fn as_xml_document(&self) -> Result<Vec<u8>, Error> {
const SOAP_ENVELOPE: &str = "soap:Envelope";
const SOAP_HEADER: &str = "soap:Header";
const SOAP_BODY: &str = "soap:Body";
let mut writer = {
let inner: Vec<u8> = Default::default();
Writer::new(inner)
};
writer.write_event(Event::Decl(BytesDecl::new("1.0", Some("utf-8"), None)))?;
writer.write_event(Event::Start(
BytesStart::new(SOAP_ENVELOPE)
.with_attributes([("xmlns:soap", SOAP_NS_URI), ("xmlns:t", TYPES_NS_URI)]),
))?;
self.headers
.serialize_as_element(&mut writer, SOAP_HEADER)?;
writer.write_event(Event::Start(BytesStart::new(SOAP_BODY)))?;
self.body
.serialize_as_element(&mut writer, <B as sealed::EnvelopeBodyContents>::NAME)?;
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: OperationResponse,
{
pub fn from_xml_document(document: &[u8]) -> Result<Self, Error> {
let de = &mut quick_xml::de::Deserializer::from_reader(document);
let envelope: DeserializeEnvelope<B> = serde_path_to_error::deserialize(de)?;
match envelope.body {
EnvelopeContent::Body(body) => Ok(Envelope {
headers: envelope
.header
.expect("all non-fault responses should have headers")
.inner,
body,
}),
EnvelopeContent::Fault(fault) => Err(Error::RequestFault(Box::new(fault))),
}
}
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct Fault {
pub faultcode: String,
pub faultstring: String,
pub faultactor: Option<String>,
pub detail: Option<FaultDetail>,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
#[serde(rename_all = "PascalCase")]
#[non_exhaustive]
pub struct FaultDetail {
pub response_code: Option<ResponseCode>,
pub message: Option<String>,
pub message_xml: Option<MessageXml>,
pub content: Option<String>,
}
#[cfg(test)]
mod tests {
use ews_proc_macros::operation_response;
use serde::Deserialize;
use xml_struct::XmlSerialize;
use crate::{
get_folder::{GetFolderResponse, GetFolderResponseMessage},
response::{ResponseClass, ResponseCode, ResponseError, ResponseMessages},
sync_folder_items::SyncFolderItemsResponse,
types::{
common::message_xml::{
MessageXmlElement, MessageXmlElements, MessageXmlTagged, MessageXmlValue,
ServerBusy,
},
sealed::EnvelopeBodyContents,
},
Error, Folder, FolderId, Folders, MessageXml, OperationResponse,
};
use super::Envelope;
fn assert_deserialized_envelope_body<T>(content: &str, expected: T)
where
T: OperationResponse + Eq + std::fmt::Debug,
{
let envelope: Envelope<T> = Envelope::from_xml_document(content.as_bytes())
.expect("deserialization should succeed");
assert_eq!(envelope.body, expected);
}
fn folder_response_message() -> GetFolderResponseMessage {
GetFolderResponseMessage {
folders: Folders {
inner: vec![Folder::Folder {
folder_id: Some(FolderId {
id: "AQMkADRiZGNhMWIxLWIwOGMtNDQAZjktODk3OS0zZWIxODJjNmI4NWYALgAAA8ZmIFRjoG9PpiagjztHaIcBAFSUeaisgPtKo3c6hV+VzpcAAAIBCAAAAA==".to_owned(),
change_key: Some(
"AQAAABYAAABUlHmorID7SqN3OoVflc6XAAAAAACW".to_owned(),
),
}),
parent_folder_id: None,
folder_class: None,
display_name: None,
total_count: None,
child_folder_count: None,
extended_property: None,
unread_count: None,
}],
},
}
}
#[test]
fn deserialize_envelope_with_content() {
#[derive(Clone, Debug, Deserialize)]
struct SomeStruct {
text: String,
other_field: ResponseMessages<()>,
}
impl OperationResponse for SomeStruct {
type Message = ();
fn response_messages(&self) -> &[ResponseClass<Self::Message>] {
&self.other_field.response_messages
}
fn into_response_messages(self) -> Vec<ResponseClass<Self::Message>> {
self.other_field.response_messages
}
}
impl EnvelopeBodyContents for SomeStruct {
const NAME: &'static str = "Foo";
}
let xml = r#"
<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Header></s:Header>
<s:Body>
<foo:Foo>
<text>testing content</text>
<other_field/>
</foo:Foo>
</s:Body>
</s:Envelope>"#;
let actual: Envelope<SomeStruct> =
Envelope::from_xml_document(xml.as_bytes()).expect("deserialization should succeed");
assert_eq!(
actual.body.text, "testing content",
"text field should match original document"
);
}
#[derive(Clone, Debug, XmlSerialize)]
#[operation_response(Bar)]
#[allow(dead_code)]
struct Foo {}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
pub struct Bar {}
#[test]
fn deserialize_envelope_with_schema_fault() {
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<FooResponse>>::from_xml_document(xml.as_bytes())
.expect_err("should return error when body contains fault");
if let Error::RequestFault(fault) = err {
assert_eq!(
fault.faultcode, "a:ErrorSchemaValidation",
"fault code should match original document"
);
assert_eq!(fault.faultstring, "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.", "fault string should match original document");
assert!(
fault.faultactor.is_none(),
"fault actor should not be present"
);
let detail = fault.detail.expect("fault detail should be present");
assert_eq!(
detail.response_code,
Some(ResponseCode::ErrorSchemaValidation),
"response code should match original document"
);
assert_eq!(
detail.message,
Some("The request failed schema validation.".to_string()),
"error message should match original document"
);
let message_xml = detail.message_xml.expect("message XML should be present");
let MessageXml::Other(elements) = message_xml else {
panic!("this message XML should only have bare tags")
};
let expected = MessageXmlElements {
elements: vec![
MessageXmlElement::MessageXmlTagged(MessageXmlTagged {
name: "LineNumber".to_string(),
value: "2".to_string()
}),
MessageXmlElement::MessageXmlTagged(MessageXmlTagged {
name: "LinePosition".to_string(),
value: "630".to_string()
}),
MessageXmlElement::MessageXmlTagged(MessageXmlTagged {
name: "Violation".to_string(),
value: "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.".to_string()
})
]
};
assert_eq!(
elements, expected,
"message XML should list all tags in order"
);
} else {
panic!("error should be request fault, got: {err:?}");
}
}
#[test]
fn deserialize_envelope_with_connection_count_fault() {
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:ErrorExceededConnectionCount</faultcode>
<faultstring xml:lang="en-US">You have exceeded the available concurrent connections for your account. Try again once your other requests have completed.</faultstring>
<detail>
<e:ResponseCode xmlns:e="http://schemas.microsoft.com/exchange/services/2006/errors">ErrorExceededConnectionCount</e:ResponseCode>
<e:Message xmlns:e="http://schemas.microsoft.com/exchange/services/2006/errors">You have exceeded the available concurrent connections for your account. Try again once your other requests have completed.</e:Message>
<t:MessageXml xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types">
<t:Value Name="Policy">MaxConcurrency</t:Value>
<t:Value Name="MaxConcurrencyLimit">27</t:Value>
<t:Value Name="ErrorMessage">This operation exceeds the throttling budget for policy part 'MaxConcurrency', policy value '27', Budget type: 'Ews'. Suggested backoff time 0 ms.</t:Value>
</t:MessageXml>
</detail>
</s:Fault>
</s:Body>
</s:Envelope>"#;
let err = <Envelope<FooResponse>>::from_xml_document(xml.as_bytes())
.expect_err("should return error when body contains fault");
if let Error::RequestFault(fault) = err {
assert_eq!(
fault.faultcode, "a:ErrorExceededConnectionCount",
"fault code should match original document"
);
assert!(
fault.faultactor.is_none(),
"fault actor should not be present"
);
let detail = fault.detail.expect("fault detail should be present");
assert_eq!(
detail.response_code,
Some(ResponseCode::ErrorExceededConnectionCount),
"response code should match original document"
);
let message_xml = detail.message_xml.expect("message XML should be present");
let MessageXml::Other(elements) = message_xml else {
panic!("this message XML should only have bare tags")
};
let expected = MessageXmlElements {
elements: vec![
MessageXmlElement::MessageXmlValue(MessageXmlValue {
name: "Policy".to_string(),
value: "MaxConcurrency".to_string()
}),
MessageXmlElement::MessageXmlValue(MessageXmlValue {
name: "MaxConcurrencyLimit".to_string(),
value: "27".to_string()
}),
MessageXmlElement::MessageXmlValue(MessageXmlValue {
name: "ErrorMessage".to_string(),
value: "This operation exceeds the throttling budget for policy part 'MaxConcurrency', policy value '27', Budget type: 'Ews'. Suggested backoff time 0 ms.".to_string()
})
]
};
assert_eq!(
elements, expected,
"message XML should list all tags in order"
);
} else {
panic!("error should be request fault, got: {err:?}");
}
}
#[test]
fn deserialize_envelope_with_server_busy_fault() {
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<FooResponse>>::from_xml_document(xml.as_bytes())
.expect_err("should return error when body contains fault");
if let Error::RequestFault(fault) = err {
assert_eq!(
fault.faultcode, "a:ErrorServerBusy",
"fault code should match original document"
);
assert!(
fault.faultactor.is_none(),
"fault actor should not be present"
);
let detail = fault.detail.expect("fault detail should be present");
assert_eq!(
detail.response_code,
Some(ResponseCode::ErrorServerBusy),
"response code should match original document"
);
let message_xml = detail.message_xml.expect("message XML should be present");
assert_eq!(
message_xml,
MessageXml::ServerBusy(ServerBusy {
back_off_milliseconds: 25
})
);
} else {
panic!("error should be request fault, got: {err:?}");
}
}
#[test]
fn deserialize_envelope_with_server_busy_error() {
let xml = r#"
<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Header>
<h:ServerVersionInfo
MajorVersion="15"
MinorVersion="20"
MajorBuildNumber="8769"
MinorBuildNumber="35"
Version="V2018_01_08"
xmlns:h="http://schemas.microsoft.com/exchange/services/2006/types"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>
</s:Header>
<s:Body>
<m:SyncFolderItemsResponse
xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types">
<m:ResponseMessages>
<m:SyncFolderItemsResponseMessage ResponseClass="Error">
<m:MessageText>The server cannot service this request right now. Try again later., This operation exceeds the throttling budget for policy part 'ConcurrentSyncCalls', policy value '5', Budget type: 'Ews'. Suggested backoff time 5000 ms.</m:MessageText>
<m:ResponseCode>ErrorServerBusy</m:ResponseCode>
<m:DescriptiveLinkKey>0</m:DescriptiveLinkKey>
<m:MessageXml>
<t:Value Name="BackOffMilliseconds">5000</t:Value>
</m:MessageXml>
<m:SyncState/>
<m:IncludesLastItemInRange>true</m:IncludesLastItemInRange>
</m:SyncFolderItemsResponseMessage>
</m:ResponseMessages>
</m:SyncFolderItemsResponse>
</s:Body>
</s:Envelope>"#;
let expected = SyncFolderItemsResponse {
response_messages: ResponseMessages {
response_messages: vec![ResponseClass::Error(ResponseError {
message_text: "The server cannot service this request right now. Try again later., This operation exceeds the throttling budget for policy part 'ConcurrentSyncCalls', policy value '5', Budget type: 'Ews'. Suggested backoff time 5000 ms.".to_string(),
response_code: ResponseCode::ErrorServerBusy,
message_xml: Some(MessageXml::ServerBusy(ServerBusy {
back_off_milliseconds: 5000,
})),
})],
},
};
assert_deserialized_envelope_body(xml, expected);
}
#[test]
fn deserialize_envelope_with_attributes_in_body() {
let xml = r#"
<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Header>
<h:ServerVersionInfo
MajorVersion="15"
MinorVersion="1"
MajorBuildNumber="2507"
MinorBuildNumber="57"
Version="V2017_07_11"
xmlns:h="http://schemas.microsoft.com/exchange/services/2006/types"
xmlns="http://schemas.microsoft.com/exchange/services/2006/types"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>
</s:Header>
<s:Body
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<m:GetFolderResponse
xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages"
xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types">
<m:ResponseMessages>
<m:GetFolderResponseMessage ResponseClass="Success">
<m:ResponseCode>NoError</m:ResponseCode>
<m:Folders>
<t:Folder>
<t:FolderId
Id="AQMkADRiZGNhMWIxLWIwOGMtNDQAZjktODk3OS0zZWIxODJjNmI4NWYALgAAA8ZmIFRjoG9PpiagjztHaIcBAFSUeaisgPtKo3c6hV+VzpcAAAIBCAAAAA=="
ChangeKey="AQAAABYAAABUlHmorID7SqN3OoVflc6XAAAAAACW"/>
</t:Folder>
</m:Folders>
</m:GetFolderResponseMessage>
</m:ResponseMessages>
</m:GetFolderResponse>
</s:Body>
</s:Envelope>"#;
let expected = GetFolderResponse {
response_messages: ResponseMessages {
response_messages: vec![ResponseClass::Success(folder_response_message())],
},
};
assert_deserialized_envelope_body(xml, expected);
}
#[test]
fn deserialize_envelope_with_warning() {
let xml = r#"
<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Header>
<h:ServerVersionInfo
MajorVersion="15"
MinorVersion="1"
MajorBuildNumber="2507"
MinorBuildNumber="57"
Version="V2017_07_11"
xmlns:h="http://schemas.microsoft.com/exchange/services/2006/types"
xmlns="http://schemas.microsoft.com/exchange/services/2006/types"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>
</s:Header>
<s:Body
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<m:GetFolderResponse
xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages"
xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types">
<m:ResponseMessages>
<m:GetFolderResponseMessage ResponseClass="Warning">
<m:MessageText>Multiple results were found.</m:MessageText>
<m:ResponseCode>ErrorNameResolutionMultipleResults</m:ResponseCode>
<m:DescriptiveLinkKey>0</m:DescriptiveLinkKey>
<m:Folders>
<t:Folder>
<t:FolderId
Id="AQMkADRiZGNhMWIxLWIwOGMtNDQAZjktODk3OS0zZWIxODJjNmI4NWYALgAAA8ZmIFRjoG9PpiagjztHaIcBAFSUeaisgPtKo3c6hV+VzpcAAAIBCAAAAA=="
ChangeKey="AQAAABYAAABUlHmorID7SqN3OoVflc6XAAAAAACW"/>
</t:Folder>
</m:Folders>
</m:GetFolderResponseMessage>
</m:ResponseMessages>
</m:GetFolderResponse>
</s:Body>
</s:Envelope>"#;
let expected = GetFolderResponse {
response_messages: ResponseMessages {
response_messages: vec![ResponseClass::Warning(folder_response_message())],
},
};
assert_deserialized_envelope_body(xml, expected);
}
}