Fix MS Exchange EWS: add support for version negotiation and other enhancements (closes #6)

This commit is contained in:
Maurus Decimus
2026-06-10 14:19:23 +02:00
parent bf0ef2083b
commit 295038d8c0
4 changed files with 34 additions and 11 deletions
+1 -1
View File
@@ -94,7 +94,7 @@ impl DistinguishedFolderId {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ServerVersion {
Exchange2007,
Exchange2010,
+29 -7
View File
@@ -5,7 +5,7 @@
*/
use crate::exchange_ews::soap::write_escaped;
use crate::exchange_ews::types::{DistinguishedFolderId, FolderId, ItemId};
use crate::exchange_ews::types::{DistinguishedFolderId, FolderId, ItemId, ServerVersion};
#[derive(Debug, Clone, Copy)]
pub enum FolderRef<'a> {
@@ -124,7 +124,7 @@ pub enum ItemShape {
Contact,
}
pub fn get_item_body(shape: ItemShape, ids: &[ItemId]) -> String {
pub fn get_item_body(shape: ItemShape, ids: &[ItemId], version: ServerVersion) -> String {
let mut out = String::with_capacity(256 + ids.len() * 128);
out.push_str("<m:GetItem><m:ItemShape>");
match shape {
@@ -134,11 +134,13 @@ pub fn get_item_body(shape: ItemShape, ids: &[ItemId]) -> String {
out.push_str("<t:BodyType>Best</t:BodyType>");
out.push_str("<t:AdditionalProperties>");
out.push_str("<t:FieldURI FieldURI=\"item:DateTimeReceived\"/>");
out.push_str("<t:FieldURI FieldURI=\"item:IsRead\"/>");
out.push_str("<t:FieldURI FieldURI=\"message:IsRead\"/>");
out.push_str("<t:FieldURI FieldURI=\"item:IsDraft\"/>");
out.push_str("<t:FieldURI FieldURI=\"item:Categories\"/>");
out.push_str("<t:FieldURI FieldURI=\"item:ParentFolderId\"/>");
out.push_str("<t:FieldURI FieldURI=\"item:Flag\"/>");
if version >= ServerVersion::Exchange2013 {
out.push_str("<t:FieldURI FieldURI=\"item:Flag\"/>");
}
out.push_str("<t:FieldURI FieldURI=\"message:IsReadReceiptRequested\"/>");
out.push_str("</t:AdditionalProperties>");
}
@@ -302,15 +304,34 @@ mod tests {
#[test]
fn get_item_message_shape_requests_mime() {
let ids = vec![ItemId::new("I1", "CK1"), ItemId::new("I2", "")];
let body = get_item_body(ItemShape::Message, &ids);
let body = get_item_body(ItemShape::Message, &ids, ServerVersion::Exchange2013Sp1);
assert!(body.contains("<t:IncludeMimeContent>true</t:IncludeMimeContent>"));
assert!(body.contains("<t:ItemId Id=\"I1\" ChangeKey=\"CK1\"/>"));
assert!(body.contains("<t:ItemId Id=\"I2\"/>"));
}
#[test]
fn item_flag_requested_only_on_exchange_2013_and_later() {
let ids = [ItemId::new("I1", "")];
let modern = get_item_body(ItemShape::Message, &ids, ServerVersion::Exchange2013Sp1);
assert!(modern.contains("item:Flag"));
let legacy = get_item_body(ItemShape::Message, &ids, ServerVersion::Exchange2010Sp2);
assert!(
!legacy.contains("item:Flag"),
"item:Flag is an Exchange 2013 schema addition; must be omitted on 2010"
);
assert!(legacy.contains("item:DateTimeReceived"));
assert!(legacy.contains("message:IsReadReceiptRequested"));
assert!(
modern.contains("\"message:IsRead\"") && !modern.contains("\"item:IsRead\""),
"IsRead is a MessageType property; its FieldURI is message:IsRead"
);
}
#[test]
fn get_item_calendar_shape_lists_calendar_fields() {
let body = get_item_body(ItemShape::CalendarItem, &[ItemId::new("X", "")]);
let body =
get_item_body(ItemShape::CalendarItem, &[ItemId::new("X", "")], ServerVersion::Exchange2010Sp2);
assert!(body.contains("calendar:Recurrence"));
assert!(body.contains("calendar:ModifiedOccurrences"));
assert!(body.contains("calendar:DeletedOccurrences"));
@@ -320,7 +341,8 @@ mod tests {
#[test]
fn get_item_contact_shape_requests_all_properties() {
let body = get_item_body(ItemShape::Contact, &[ItemId::new("X", "")]);
let body =
get_item_body(ItemShape::Contact, &[ItemId::new("X", "")], ServerVersion::Exchange2013Sp1);
assert!(body.contains("<t:BaseShape>AllProperties</t:BaseShape>"));
}
+3 -2
View File
@@ -286,11 +286,12 @@ pub fn get_items(
) -> Result<GetItemBatchOutcome, EwsError> {
let batch = ctx.batch_size.max(1);
let workers = ctx.connections.clamp(1, 8);
let version = ctx.client.server_version();
let mut failed_items: u64 = 0;
if workers <= 1 || ids.len() <= batch {
let mut all = Vec::new();
for chunk in ids.chunks(batch) {
let body = get_item_body(shape, chunk);
let body = get_item_body(shape, chunk, version);
match ctx.client.call(ctx.url, "GetItem", &body) {
Ok(resp) => match parse_response_messages(&resp.body, b"GetItemResponseMessage") {
Ok(mut msgs) => all.append(&mut msgs),
@@ -326,7 +327,7 @@ pub fn get_items(
);
let pool: crate::sync::import_jmap::pool::Pool<Vec<ItemId>, BatchResult> =
crate::sync::import_jmap::pool::Pool::new(workers, move |chunk: Vec<ItemId>| {
let body = get_item_body(shape, &chunk);
let body = get_item_body(shape, &chunk, version);
let n = chunk.len();
let result = match client.call(&url, "GetItem", &body) {
Ok(resp) => parse_response_messages(&resp.body, b"GetItemResponseMessage"),