Initial commit

This commit is contained in:
Maurus Decimus
2026-05-29 18:02:15 +02:00
commit 576073f8c9
263 changed files with 78638 additions and 0 deletions
+585
View File
@@ -0,0 +1,585 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*/
use mockito::Matcher;
use vandelay::exchange_ews::EwsClient;
use vandelay::exchange_ews::autodiscover::{DiscoverySource, discover};
use vandelay::exchange_ews::error::EwsError;
use vandelay::exchange_ews::parse::{
EnvelopeKind, parse_find_folder_response, parse_find_item_response,
parse_get_attachment_inline, parse_response_messages, parse_sync_folder_items_response,
read_envelope_summary,
};
use vandelay::exchange_ews::types::{FolderId, ItemId, ResponseCode};
use vandelay::exchange_ews::xml::{
FolderRef, ItemShape, Traversal, find_folder_body, find_item_body, get_attachment_body,
get_item_body, sync_folder_items_body,
};
use vandelay::jmap::http::{Auth, RetryPolicy};
const TXT_XML: &str = "text/xml; charset=utf-8";
const APP_JSON: &str = "application/json";
fn client(retries: u32) -> EwsClient {
EwsClient::new(
Auth::Bearer {
token: "t".to_owned(),
},
RetryPolicy::new(retries),
false,
)
}
const NS: &str = " xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" \
xmlns:t=\"http://schemas.microsoft.com/exchange/services/2006/types\" \
xmlns:m=\"http://schemas.microsoft.com/exchange/services/2006/messages\"";
fn envelope(inner: &str) -> String {
format!(
"<soap:Envelope{NS}><soap:Header><t:ServerVersionInfo MajorVersion=\"15\" MinorVersion=\"1\"/></soap:Header><soap:Body>{inner}</soap:Body></soap:Envelope>"
)
}
#[test]
fn autodiscover_v2_returns_global_endpoint() {
let mut server = mockito::Server::new();
let _m = server
.mock("GET", "/autodiscover/autodiscover.json")
.match_query(Matcher::AllOf(vec![
Matcher::UrlEncoded("Email".into(), "[email protected]".into()),
Matcher::UrlEncoded("Protocol".into(), "Ews".into()),
]))
.with_status(200)
.with_header("content-type", APP_JSON)
.with_body(r#"{"Protocol":"Ews","Url":"https://outlook.office365.com/EWS/Exchange.asmx"}"#)
.create();
let _ = server;
let url = "https://outlook.office365.com/EWS/Exchange.asmx";
assert!(vandelay::exchange_ews::autodiscover::is_fully_qualified_ews_url(url));
let r = discover(Some(url), None, None, false).unwrap();
assert_eq!(r.source, DiscoverySource::SuppliedUrl);
assert_eq!(r.ews_url, url);
}
#[test]
fn find_folder_pagination_and_classification() {
let body = format!(
"<m:FindFolderResponse{NS}><m:ResponseMessages><m:FindFolderResponseMessage ResponseClass=\"Success\">\
<m:ResponseCode>NoError</m:ResponseCode>\
<m:RootFolder TotalItemsInView=\"3\" IncludesLastItemInRange=\"true\">\
<t:Folders>\
<t:Folder><t:FolderId Id=\"FMAIL\" ChangeKey=\"C\"/><t:ParentFolderId Id=\"ROOT\"/>\
<t:FolderClass>IPF.Note</t:FolderClass><t:DisplayName>Inbox</t:DisplayName></t:Folder>\
<t:CalendarFolder><t:FolderId Id=\"FCAL\"/><t:ParentFolderId Id=\"ROOT\"/>\
<t:FolderClass>IPF.Appointment</t:FolderClass><t:DisplayName>Calendar</t:DisplayName></t:CalendarFolder>\
<t:ContactsFolder><t:FolderId Id=\"FCON\"/><t:ParentFolderId Id=\"ROOT\"/>\
<t:FolderClass>IPF.Contact</t:FolderClass><t:DisplayName>Contacts</t:DisplayName></t:ContactsFolder>\
</t:Folders></m:RootFolder></m:FindFolderResponseMessage></m:ResponseMessages></m:FindFolderResponse>"
);
let parsed = parse_find_folder_response(envelope(&body).as_bytes()).unwrap();
assert_eq!(parsed.folders.len(), 3);
let inbox = &parsed.folders[0];
assert_eq!(inbox.folder_id.id, "FMAIL");
assert_eq!(inbox.folder_class, "IPF.Note");
}
#[test]
fn find_item_offset_loop_terminates_on_includes_last_true() {
let mut server = mockito::Server::new();
let url = format!("{}/EWS/Exchange.asmx", server.url());
let page1 = envelope(&format!(
"<m:FindItemResponse{NS}><m:ResponseMessages><m:FindItemResponseMessage ResponseClass=\"Success\">\
<m:ResponseCode>NoError</m:ResponseCode>\
<m:RootFolder TotalItemsInView=\"4\" IncludesLastItemInRange=\"false\">\
<t:Items><t:Message><t:ItemId Id=\"A\" ChangeKey=\"1\"/></t:Message>\
<t:Message><t:ItemId Id=\"B\" ChangeKey=\"1\"/></t:Message></t:Items></m:RootFolder>\
</m:FindItemResponseMessage></m:ResponseMessages></m:FindItemResponse>"
));
let page2 = envelope(&format!(
"<m:FindItemResponse{NS}><m:ResponseMessages><m:FindItemResponseMessage ResponseClass=\"Success\">\
<m:ResponseCode>NoError</m:ResponseCode>\
<m:RootFolder TotalItemsInView=\"4\" IncludesLastItemInRange=\"true\">\
<t:Items><t:Message><t:ItemId Id=\"C\" ChangeKey=\"1\"/></t:Message>\
<t:Message><t:ItemId Id=\"D\" ChangeKey=\"1\"/></t:Message></t:Items></m:RootFolder>\
</m:FindItemResponseMessage></m:ResponseMessages></m:FindItemResponse>"
));
let _m1 = server
.mock("POST", "/EWS/Exchange.asmx")
.match_body(Matcher::Regex("Offset=\"0\"".into()))
.with_status(200)
.with_header("content-type", TXT_XML)
.with_body(page1)
.create();
let _m2 = server
.mock("POST", "/EWS/Exchange.asmx")
.match_body(Matcher::Regex("Offset=\"2\"".into()))
.with_status(200)
.with_header("content-type", TXT_XML)
.with_body(page2)
.create();
let c = client(0);
let folder = FolderId::new("FID", "FCK");
let body0 = find_item_body(FolderRef::Concrete(&folder), Traversal::Shallow, 0, 2);
let r0 = c.call(&url, "FindItem", &body0).unwrap();
let parsed0 = parse_find_item_response(&r0.body).unwrap();
assert_eq!(parsed0.items.len(), 2);
assert!(parsed0.more);
let body1 = find_item_body(FolderRef::Concrete(&folder), Traversal::Shallow, 2, 2);
let r1 = c.call(&url, "FindItem", &body1).unwrap();
let parsed1 = parse_find_item_response(&r1.body).unwrap();
assert_eq!(parsed1.items.len(), 2);
assert!(!parsed1.more);
}
#[test]
fn get_item_mixed_success_and_per_item_error() {
let body = envelope(&format!(
"<m:GetItemResponse{NS}><m:ResponseMessages>\
<m:GetItemResponseMessage ResponseClass=\"Success\">\
<m:ResponseCode>NoError</m:ResponseCode>\
<m:Items><t:Message><t:ItemId Id=\"OK1\" ChangeKey=\"K\"/>\
<t:Subject>S</t:Subject>\
<t:MimeContent CharacterSet=\"UTF-8\">SGVsbG8=</t:MimeContent>\
</t:Message></m:Items></m:GetItemResponseMessage>\
<m:GetItemResponseMessage ResponseClass=\"Error\">\
<m:ResponseCode>ErrorItemNotFound</m:ResponseCode>\
<m:MessageText>not found</m:MessageText>\
</m:GetItemResponseMessage>\
</m:ResponseMessages></m:GetItemResponse>"
));
let r = parse_response_messages(body.as_bytes(), b"GetItemResponseMessage").unwrap();
assert_eq!(r.len(), 2);
assert!(r[0].success);
assert!(r[0].inner_xml.contains("SGVsbG8="));
assert!(!r[1].success);
assert!(matches!(r[1].response_code, ResponseCode::ItemNotFound));
}
#[test]
fn server_busy_with_back_off_triggers_retry_and_eventually_succeeds() {
let mut server = mockito::Server::new();
let url = format!("{}/EWS/Exchange.asmx", server.url());
let busy_body = format!(
"<soap:Envelope{NS}><soap:Header><t:ServerVersionInfo MajorVersion=\"15\" MinorVersion=\"1\"/></soap:Header>\
<soap:Body><soap:Fault><faultcode>soap:Server</faultcode><faultstring>busy</faultstring><detail>\
<ResponseCode xmlns=\"http://schemas.microsoft.com/exchange/services/2006/types\">ErrorServerBusy</ResponseCode>\
<t:MessageXml><t:Value Name=\"BackOffMilliseconds\">200</t:Value></t:MessageXml>\
</detail></soap:Fault></soap:Body></soap:Envelope>"
);
let ok_body = envelope(&format!(
"<m:FindFolderResponse{NS}><m:ResponseMessages><m:FindFolderResponseMessage ResponseClass=\"Success\">\
<m:ResponseCode>NoError</m:ResponseCode>\
<m:RootFolder TotalItemsInView=\"0\" IncludesLastItemInRange=\"true\"><t:Folders/></m:RootFolder>\
</m:FindFolderResponseMessage></m:ResponseMessages></m:FindFolderResponse>"
));
let _m1 = server
.mock("POST", "/EWS/Exchange.asmx")
.with_status(200)
.with_header("content-type", TXT_XML)
.with_body(busy_body)
.expect(1)
.create();
let _m2 = server
.mock("POST", "/EWS/Exchange.asmx")
.with_status(200)
.with_header("content-type", TXT_XML)
.with_body(ok_body)
.expect_at_least(1)
.create();
let c = client(3);
let body = find_folder_body(
FolderRef::Distinguished(
vandelay::exchange_ews::types::DistinguishedFolderId::MsgFolderRoot,
),
Traversal::Deep,
);
let r = c
.call(&url, "FindFolder", &body)
.expect("call should retry past busy fault");
let parsed = parse_find_folder_response(&r.body).unwrap();
assert!(parsed.folders.is_empty());
assert!(c.retries_observed() >= 1);
}
#[test]
fn http_401_surfaces_as_auth_error() {
let mut server = mockito::Server::new();
let url = format!("{}/EWS/Exchange.asmx", server.url());
let _m = server
.mock("POST", "/EWS/Exchange.asmx")
.with_status(401)
.with_header("www-authenticate", "Bearer error=\"invalid_token\"")
.with_body("unauthorized")
.create();
let c = client(0);
let body = find_folder_body(
FolderRef::Distinguished(
vandelay::exchange_ews::types::DistinguishedFolderId::MsgFolderRoot,
),
Traversal::Deep,
);
let err = c.call(&url, "FindFolder", &body).unwrap_err();
assert!(matches!(err, EwsError::Auth(_)), "got {err:?}");
}
#[test]
fn mime_content_round_trips_through_base64_decode() {
let original = b"From: alice@x\r\nSubject: hi\r\n\r\nbody";
use base64::Engine;
let encoded = base64::engine::general_purpose::STANDARD.encode(original);
let body = envelope(&format!(
"<m:GetItemResponse{NS}><m:ResponseMessages>\
<m:GetItemResponseMessage ResponseClass=\"Success\">\
<m:ResponseCode>NoError</m:ResponseCode>\
<m:Items><t:Message><t:ItemId Id=\"X\" ChangeKey=\"K\"/>\
<t:Subject>hi</t:Subject>\
<t:MimeContent CharacterSet=\"UTF-8\">{encoded}</t:MimeContent>\
</t:Message></m:Items></m:GetItemResponseMessage>\
</m:ResponseMessages></m:GetItemResponse>"
));
let r = parse_response_messages(body.as_bytes(), b"GetItemResponseMessage").unwrap();
let item = vandelay::exchange_ews::parse::parse_message_item(&r[0].inner_xml).unwrap();
let s = item.mime_content.unwrap();
let cleaned: String = s.chars().filter(|c| !c.is_whitespace()).collect();
let bytes = base64::engine::general_purpose::STANDARD
.decode(cleaned.as_bytes())
.unwrap();
assert_eq!(bytes, original);
}
#[test]
fn get_attachment_inline_decodes_photo_blob() {
let body = envelope(&format!(
"<m:GetAttachmentResponse{NS}><m:ResponseMessages>\
<m:GetAttachmentResponseMessage ResponseClass=\"Success\">\
<m:ResponseCode>NoError</m:ResponseCode>\
<m:Attachments>\
<t:FileAttachment><t:AttachmentId Id=\"A1\"/><t:Name>p.png</t:Name>\
<t:ContentType>image/png</t:ContentType>\
<t:IsContactPhoto>true</t:IsContactPhoto>\
<t:Content>iVBORw0KGgo=</t:Content></t:FileAttachment>\
</m:Attachments></m:GetAttachmentResponseMessage></m:ResponseMessages></m:GetAttachmentResponse>"
));
let items = parse_get_attachment_inline(body.as_bytes()).unwrap();
assert_eq!(items.len(), 1);
assert!(items[0].is_contact_photo);
assert_eq!(items[0].content_base64, "iVBORw0KGgo=");
assert_eq!(items[0].content_type.as_deref(), Some("image/png"));
}
#[test]
fn calendar_item_master_inlines_modified_and_deleted_occurrences() {
let body = "<vandelay-inner xmlns:t=\"http://schemas.microsoft.com/exchange/services/2006/types\">\
<t:CalendarItem>\
<t:ItemId Id=\"M1\" ChangeKey=\"K1\"/>\
<t:Subject>Daily</t:Subject>\
<t:UID>uid-1</t:UID>\
<t:Start>2025-06-15T14:00:00Z</t:Start>\
<t:End>2025-06-15T15:00:00Z</t:End>\
<t:CalendarItemType>RecurringMaster</t:CalendarItemType>\
<t:Recurrence>\
<t:DailyRecurrence><t:Interval>1</t:Interval></t:DailyRecurrence>\
<t:NumberedRecurrence><t:StartDate>2025-06-15</t:StartDate><t:NumberOfOccurrences>3</t:NumberOfOccurrences></t:NumberedRecurrence>\
</t:Recurrence>\
<t:ModifiedOccurrences>\
<t:Occurrence><t:ItemId Id=\"OCC1\"/><t:Start>2025-06-16T15:00:00Z</t:Start><t:End>2025-06-16T16:30:00Z</t:End><t:OriginalStart>2025-06-16T14:00:00Z</t:OriginalStart></t:Occurrence>\
</t:ModifiedOccurrences>\
<t:DeletedOccurrences>\
<t:DeletedOccurrence><t:Start>2025-06-17T14:00:00Z</t:Start></t:DeletedOccurrence>\
</t:DeletedOccurrences>\
</t:CalendarItem></vandelay-inner>";
let item = vandelay::exchange_ews::parse::parse_calendar_item(body).unwrap();
assert_eq!(item.id.id, "M1");
assert_eq!(item.uid.as_deref(), Some("uid-1"));
assert_eq!(item.modified_occurrences.len(), 1);
assert_eq!(
item.modified_occurrences[0].item_id.id, "OCC1",
"occurrence ItemId must populate the occurrence, not overwrite the master"
);
assert_eq!(item.deleted_occurrences.len(), 1);
}
#[test]
fn sync_folder_items_creates_updates_deletes_round_trip() {
let body = envelope(&format!(
"<m:SyncFolderItemsResponse{NS}><m:ResponseMessages><m:SyncFolderItemsResponseMessage ResponseClass=\"Success\">\
<m:ResponseCode>NoError</m:ResponseCode>\
<m:SyncState>STATE-1</m:SyncState>\
<m:IncludesLastItemInRange>true</m:IncludesLastItemInRange>\
<m:Changes>\
<t:Create><t:Message><t:ItemId Id=\"N1\" ChangeKey=\"A\"/></t:Message></t:Create>\
<t:Create><t:Message><t:ItemId Id=\"N2\" ChangeKey=\"A\"/></t:Message></t:Create>\
<t:Update><t:Message><t:ItemId Id=\"U1\" ChangeKey=\"B\"/></t:Message></t:Update>\
<t:Delete><t:ItemId Id=\"D1\"/></t:Delete>\
<t:ReadFlagChange><t:ItemId Id=\"R1\"/><t:IsRead>true</t:IsRead></t:ReadFlagChange>\
</m:Changes></m:SyncFolderItemsResponseMessage></m:ResponseMessages></m:SyncFolderItemsResponse>"
));
let parsed = parse_sync_folder_items_response(body.as_bytes()).unwrap();
assert_eq!(parsed.sync_state, "STATE-1");
assert!(!parsed.more);
assert_eq!(parsed.changes.len(), 5);
}
#[test]
fn invalid_sync_state_data_fault_is_surfaced() {
let body = format!(
"<soap:Envelope{NS}><soap:Header><t:ServerVersionInfo MajorVersion=\"15\" MinorVersion=\"1\"/></soap:Header>\
<soap:Body><soap:Fault><faultcode>soap:Server</faultcode><faultstring>bad state</faultstring><detail>\
<ResponseCode xmlns=\"http://schemas.microsoft.com/exchange/services/2006/types\">ErrorInvalidSyncStateData</ResponseCode>\
</detail></soap:Fault></soap:Body></soap:Envelope>"
);
let env = read_envelope_summary(body.as_bytes()).unwrap();
match env {
EnvelopeKind::Fault { fault, .. } => {
assert!(matches!(
fault.response_code,
ResponseCode::InvalidSyncStateData
));
}
_ => panic!("expected fault"),
}
}
#[test]
fn coordinator_diff_classifies_new_vanished_changed_unchanged() {
use vandelay::db::exchange_ews_ids::ItemRow;
use vandelay::sync::import_exchange_ews::items::{EnumeratedItem, diff};
let server = vec![
EnumeratedItem {
element: "Message".into(),
id: ItemId::new("A", "ck-1"),
},
EnumeratedItem {
element: "Message".into(),
id: ItemId::new("B", "ck-2"),
},
EnumeratedItem {
element: "Message".into(),
id: ItemId::new("C", "ck-1"),
},
];
let local = vec![
ItemRow {
item_id: "A".into(),
change_key: "ck-1".into(),
local_id: 1,
},
ItemRow {
item_id: "B".into(),
change_key: "ck-1".into(),
local_id: 2,
},
ItemRow {
item_id: "Z".into(),
change_key: "ck-9".into(),
local_id: 99,
},
];
let plan = diff(&server, &local);
assert_eq!(plan.new.len(), 1);
assert_eq!(plan.new[0].id, "C");
assert_eq!(plan.present_unchanged.len(), 1);
assert_eq!(plan.present_changed.len(), 1);
assert_eq!(plan.present_changed[0].0.id, "B");
assert_eq!(plan.vanished.len(), 1);
assert_eq!(plan.vanished[0].0, "Z");
}
#[test]
fn source_change_protection_refuses_different_account() {
use rusqlite::Connection;
use vandelay::db;
use vandelay::db::sources::SourceKey;
let tmp = tempfile::NamedTempFile::new().unwrap();
let conn = Connection::open(tmp.path()).unwrap();
db::init::apply_schema(&conn).unwrap();
let key = SourceKey {
kind: "exchange_ews".into(),
session_url: "https://outlook.office365.com/EWS/Exchange.asmx".into(),
account_id: "[email protected]".into(),
};
db::sources::upsert_source(&conn, &key, None, "[email protected]").unwrap();
let conflict = db::sources::conflicting_source(
&conn,
"exchange_ews",
"https://outlook.office365.com/EWS/Exchange.asmx",
"[email protected]",
)
.unwrap();
assert!(conflict.is_some(), "different account_id must conflict");
}
#[test]
fn mailbox_kinds_are_three_separate_sources() {
use rusqlite::Connection;
use vandelay::db;
use vandelay::db::sources::SourceKey;
let tmp = tempfile::NamedTempFile::new().unwrap();
let conn = Connection::open(tmp.path()).unwrap();
db::init::apply_schema(&conn).unwrap();
let primary = SourceKey {
kind: "exchange_ews".into(),
session_url: "https://x/EWS/Exchange.asmx".into(),
account_id: "[email protected]".into(),
};
let archive = SourceKey {
kind: "exchange_ews".into(),
session_url: "https://x/EWS/Exchange.asmx".into(),
account_id: "[email protected]#archive".into(),
};
let public = SourceKey {
kind: "exchange_ews".into(),
session_url: "https://x/EWS/Exchange.asmx".into(),
account_id: "[email protected]".into(),
};
let p = db::sources::upsert_source(&conn, &primary, None, "[email protected]").unwrap();
let a = db::sources::upsert_source(&conn, &archive, None, "[email protected]").unwrap();
let q = db::sources::upsert_source(&conn, &public, None, "[email protected]").unwrap();
assert_ne!(p, a);
assert_ne!(p, q);
assert_ne!(a, q);
}
#[test]
fn get_item_batches_chunk_the_id_list() {
let ids: Vec<ItemId> = (0..7).map(|i| ItemId::new(format!("I{i}"), "K")).collect();
let chunks: Vec<&[ItemId]> = ids.chunks(3).collect();
assert_eq!(chunks.len(), 3);
for chunk in &chunks {
let body = get_item_body(ItemShape::Message, chunk);
assert!(body.contains("<m:GetItem>"));
for id in *chunk {
assert!(body.contains(&format!("Id=\"{}\"", id.id)));
}
}
}
#[test]
fn sync_folder_items_request_body_carries_state_and_max() {
let folder = FolderId::new("FID", "FCK");
let body = sync_folder_items_body(&folder, "OPAQUE", 512);
assert!(body.contains("<m:SyncState>OPAQUE</m:SyncState>"));
assert!(body.contains("<m:MaxChangesReturned>512</m:MaxChangesReturned>"));
assert!(body.contains("<m:SyncScope>NormalItems</m:SyncScope>"));
assert!(body.contains("<m:SyncFolderId><t:FolderId Id=\"FID\" ChangeKey=\"FCK\"/>"));
}
#[test]
fn get_attachment_body_carries_inline_request() {
let body = get_attachment_body(&["A1", "A2"]);
assert!(body.contains("<t:AttachmentId Id=\"A1\"/>"));
assert!(body.contains("<t:AttachmentId Id=\"A2\"/>"));
assert!(body.contains("<t:IncludeMimeContent>true</t:IncludeMimeContent>"));
}
#[test]
fn http_500_with_server_busy_body_is_treated_as_fault_and_retried() {
let mut server = mockito::Server::new();
let url = format!("{}/EWS/Exchange.asmx", server.url());
let busy_body = format!(
"<soap:Envelope{NS}><soap:Body><soap:Fault>\
<faultcode>soap:Server</faultcode><faultstring>busy</faultstring>\
<detail><ResponseCode xmlns=\"http://schemas.microsoft.com/exchange/services/2006/types\">ErrorServerBusy</ResponseCode>\
<t:MessageXml><t:Value Name=\"BackOffMilliseconds\">100</t:Value></t:MessageXml>\
</detail></soap:Fault></soap:Body></soap:Envelope>"
);
let ok_body = envelope(&format!(
"<m:FindFolderResponse{NS}><m:ResponseMessages><m:FindFolderResponseMessage ResponseClass=\"Success\">\
<m:ResponseCode>NoError</m:ResponseCode>\
<m:RootFolder TotalItemsInView=\"0\" IncludesLastItemInRange=\"true\"><t:Folders/></m:RootFolder>\
</m:FindFolderResponseMessage></m:ResponseMessages></m:FindFolderResponse>"
));
let _m1 = server
.mock("POST", "/EWS/Exchange.asmx")
.with_status(200)
.with_header("content-type", TXT_XML)
.with_body(busy_body)
.expect(1)
.create();
let _m2 = server
.mock("POST", "/EWS/Exchange.asmx")
.with_status(200)
.with_header("content-type", TXT_XML)
.with_body(ok_body)
.create();
let c = client(3);
let body = find_folder_body(
FolderRef::Distinguished(
vandelay::exchange_ews::types::DistinguishedFolderId::MsgFolderRoot,
),
Traversal::Deep,
);
let r = c.call(&url, "FindFolder", &body).expect("should retry");
let parsed = parse_find_folder_response(&r.body).unwrap();
assert!(parsed.folders.is_empty());
}
#[test]
fn warning_response_class_is_treated_as_success_in_mock() {
let body = envelope(&format!(
"<m:GetItemResponse{NS}><m:ResponseMessages>\
<m:GetItemResponseMessage ResponseClass=\"Warning\">\
<m:ResponseCode>NoError</m:ResponseCode>\
<m:Items><t:Message><t:ItemId Id=\"W1\" ChangeKey=\"K\"/></t:Message></m:Items>\
</m:GetItemResponseMessage></m:ResponseMessages></m:GetItemResponse>"
));
let r = parse_response_messages(body.as_bytes(), b"GetItemResponseMessage").unwrap();
assert!(r[0].success);
}
#[test]
fn default_namespace_envelope_parses_via_resolved_names() {
let body = "<Envelope xmlns=\"http://schemas.xmlsoap.org/soap/envelope/\" \
xmlns:m=\"http://schemas.microsoft.com/exchange/services/2006/messages\" \
xmlns:t=\"http://schemas.microsoft.com/exchange/services/2006/types\">\
<Body>\
<m:FindFolderResponse><m:ResponseMessages>\
<m:FindFolderResponseMessage ResponseClass=\"Success\">\
<m:ResponseCode>NoError</m:ResponseCode>\
<m:RootFolder TotalItemsInView=\"1\" IncludesLastItemInRange=\"true\">\
<t:Folders><t:Folder><t:FolderId Id=\"D1\"/><t:DisplayName>Inbox</t:DisplayName>\
</t:Folder></t:Folders></m:RootFolder>\
</m:FindFolderResponseMessage></m:ResponseMessages></m:FindFolderResponse>\
</Body></Envelope>";
let parsed = parse_find_folder_response(body.as_bytes()).unwrap();
assert_eq!(parsed.folders.len(), 1);
assert_eq!(parsed.folders[0].folder_id.id, "D1");
}
#[test]
fn get_folder_messages_preserve_position_when_one_errors() {
let body = envelope(&format!(
"<m:GetFolderResponse{NS}><m:ResponseMessages>\
<m:GetFolderResponseMessage ResponseClass=\"Success\">\
<m:ResponseCode>NoError</m:ResponseCode>\
<m:Folders><t:Folder><t:FolderId Id=\"FA\"/><t:DisplayName>Inbox</t:DisplayName></t:Folder></m:Folders>\
</m:GetFolderResponseMessage>\
<m:GetFolderResponseMessage ResponseClass=\"Error\">\
<m:ResponseCode>ErrorAccessDenied</m:ResponseCode>\
</m:GetFolderResponseMessage>\
<m:GetFolderResponseMessage ResponseClass=\"Success\">\
<m:ResponseCode>NoError</m:ResponseCode>\
<m:Folders><t:Folder><t:FolderId Id=\"FC\"/><t:DisplayName>Drafts</t:DisplayName></t:Folder></m:Folders>\
</m:GetFolderResponseMessage>\
</m:ResponseMessages></m:GetFolderResponse>"
));
let msgs = parse_response_messages(body.as_bytes(), b"GetFolderResponseMessage").unwrap();
assert_eq!(msgs.len(), 3, "all three messages must be present");
assert!(msgs[0].success);
assert!(!msgs[1].success);
assert!(msgs[2].success);
}