341 lines
12 KiB
Rust
341 lines
12 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
*/
|
|
|
|
use crate::exchange_ews::soap::write_escaped;
|
|
use crate::exchange_ews::types::{DistinguishedFolderId, FolderId, ItemId};
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub enum FolderRef<'a> {
|
|
Distinguished(DistinguishedFolderId),
|
|
Concrete(&'a FolderId),
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub enum FolderShape {
|
|
IdOnly,
|
|
}
|
|
|
|
impl FolderShape {
|
|
fn as_str(self) -> &'static str {
|
|
match self {
|
|
FolderShape::IdOnly => "IdOnly",
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub enum Traversal {
|
|
Shallow,
|
|
Deep,
|
|
}
|
|
|
|
impl Traversal {
|
|
fn as_str(self) -> &'static str {
|
|
match self {
|
|
Traversal::Shallow => "Shallow",
|
|
Traversal::Deep => "Deep",
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn find_folder_body(parent: FolderRef<'_>, traversal: Traversal) -> String {
|
|
let mut out = String::with_capacity(512);
|
|
out.push_str("<m:FindFolder Traversal=\"");
|
|
out.push_str(traversal.as_str());
|
|
out.push_str("\"><m:FolderShape>");
|
|
out.push_str("<t:BaseShape>Default</t:BaseShape>");
|
|
out.push_str("<t:AdditionalProperties>");
|
|
out.push_str("<t:FieldURI FieldURI=\"folder:FolderClass\"/>");
|
|
out.push_str("<t:FieldURI FieldURI=\"folder:DisplayName\"/>");
|
|
out.push_str("<t:FieldURI FieldURI=\"folder:ParentFolderId\"/>");
|
|
out.push_str("<t:FieldURI FieldURI=\"folder:TotalCount\"/>");
|
|
out.push_str("<t:FieldURI FieldURI=\"folder:ChildFolderCount\"/>");
|
|
out.push_str("</t:AdditionalProperties>");
|
|
out.push_str("</m:FolderShape><m:ParentFolderIds>");
|
|
write_folder_ref(&mut out, parent);
|
|
out.push_str("</m:ParentFolderIds></m:FindFolder>");
|
|
out
|
|
}
|
|
|
|
pub fn get_folder_body(folders: &[FolderRef<'_>], shape: FolderShape) -> String {
|
|
let mut out = String::with_capacity(256 + folders.len() * 64);
|
|
out.push_str("<m:GetFolder><m:FolderShape>");
|
|
out.push_str("<t:BaseShape>");
|
|
out.push_str(shape.as_str());
|
|
out.push_str("</t:BaseShape>");
|
|
out.push_str("</m:FolderShape><m:FolderIds>");
|
|
for f in folders {
|
|
write_folder_ref(&mut out, *f);
|
|
}
|
|
out.push_str("</m:FolderIds></m:GetFolder>");
|
|
out
|
|
}
|
|
|
|
pub fn find_item_body(
|
|
parent: FolderRef<'_>,
|
|
traversal: Traversal,
|
|
offset: u32,
|
|
page_size: u32,
|
|
) -> String {
|
|
let mut out = String::with_capacity(512);
|
|
out.push_str("<m:FindItem Traversal=\"");
|
|
out.push_str(traversal.as_str());
|
|
out.push_str("\"><m:ItemShape><t:BaseShape>IdOnly</t:BaseShape></m:ItemShape>");
|
|
out.push_str("<m:IndexedPageItemView MaxEntriesReturned=\"");
|
|
out.push_str(&page_size.to_string());
|
|
out.push_str("\" Offset=\"");
|
|
out.push_str(&offset.to_string());
|
|
out.push_str("\" BasePoint=\"Beginning\"/>");
|
|
out.push_str("<m:ParentFolderIds>");
|
|
write_folder_ref(&mut out, parent);
|
|
out.push_str("</m:ParentFolderIds></m:FindItem>");
|
|
out
|
|
}
|
|
|
|
pub fn sync_folder_items_body(folder: &FolderId, sync_state: &str, max_changes: u32) -> String {
|
|
let mut out = String::with_capacity(512);
|
|
out.push_str("<m:SyncFolderItems><m:ItemShape>");
|
|
out.push_str("<t:BaseShape>IdOnly</t:BaseShape>");
|
|
out.push_str("</m:ItemShape><m:SyncFolderId>");
|
|
write_folder_id_element(&mut out, folder);
|
|
out.push_str("</m:SyncFolderId>");
|
|
out.push_str("<m:SyncState>");
|
|
write_escaped(&mut out, sync_state);
|
|
out.push_str("</m:SyncState>");
|
|
out.push_str("<m:MaxChangesReturned>");
|
|
out.push_str(&max_changes.to_string());
|
|
out.push_str("</m:MaxChangesReturned>");
|
|
out.push_str("<m:SyncScope>NormalItems</m:SyncScope>");
|
|
out.push_str("</m:SyncFolderItems>");
|
|
out
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub enum ItemShape {
|
|
Message,
|
|
CalendarItem,
|
|
Contact,
|
|
}
|
|
|
|
pub fn get_item_body(shape: ItemShape, ids: &[ItemId]) -> String {
|
|
let mut out = String::with_capacity(256 + ids.len() * 128);
|
|
out.push_str("<m:GetItem><m:ItemShape>");
|
|
match shape {
|
|
ItemShape::Message => {
|
|
out.push_str("<t:BaseShape>IdOnly</t:BaseShape>");
|
|
out.push_str("<t:IncludeMimeContent>true</t:IncludeMimeContent>");
|
|
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=\"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\"/>");
|
|
out.push_str("<t:FieldURI FieldURI=\"message:IsReadReceiptRequested\"/>");
|
|
out.push_str("</t:AdditionalProperties>");
|
|
}
|
|
ItemShape::CalendarItem => {
|
|
out.push_str("<t:BaseShape>IdOnly</t:BaseShape>");
|
|
out.push_str("<t:BodyType>Best</t:BodyType>");
|
|
out.push_str("<t:AdditionalProperties>");
|
|
for f in CALENDAR_FIELDS {
|
|
out.push_str("<t:FieldURI FieldURI=\"");
|
|
out.push_str(f);
|
|
out.push_str("\"/>");
|
|
}
|
|
out.push_str("</t:AdditionalProperties>");
|
|
}
|
|
ItemShape::Contact => {
|
|
out.push_str("<t:BaseShape>AllProperties</t:BaseShape>");
|
|
}
|
|
}
|
|
out.push_str("</m:ItemShape><m:ItemIds>");
|
|
for id in ids {
|
|
write_item_id_element(&mut out, id);
|
|
}
|
|
out.push_str("</m:ItemIds></m:GetItem>");
|
|
out
|
|
}
|
|
|
|
const CALENDAR_FIELDS: &[&str] = &[
|
|
"calendar:UID",
|
|
"calendar:RecurrenceId",
|
|
"calendar:Start",
|
|
"calendar:End",
|
|
"calendar:OriginalStart",
|
|
"calendar:IsAllDayEvent",
|
|
"calendar:LegacyFreeBusyStatus",
|
|
"calendar:Location",
|
|
"calendar:IsRecurring",
|
|
"calendar:CalendarItemType",
|
|
"calendar:Organizer",
|
|
"calendar:RequiredAttendees",
|
|
"calendar:OptionalAttendees",
|
|
"calendar:Recurrence",
|
|
"calendar:ModifiedOccurrences",
|
|
"calendar:DeletedOccurrences",
|
|
"calendar:StartTimeZone",
|
|
"calendar:EndTimeZone",
|
|
"calendar:Duration",
|
|
"item:Subject",
|
|
"item:Body",
|
|
"item:Categories",
|
|
"item:DateTimeCreated",
|
|
"item:LastModifiedTime",
|
|
"item:ParentFolderId",
|
|
];
|
|
|
|
pub fn get_attachment_body(ids: &[&str]) -> String {
|
|
let mut out = String::with_capacity(256 + ids.len() * 64);
|
|
out.push_str("<m:GetAttachment><m:AttachmentShape>");
|
|
out.push_str("<t:IncludeMimeContent>true</t:IncludeMimeContent>");
|
|
out.push_str("<t:BodyType>Best</t:BodyType>");
|
|
out.push_str("</m:AttachmentShape><m:AttachmentIds>");
|
|
for id in ids {
|
|
out.push_str("<t:AttachmentId Id=\"");
|
|
write_escaped(&mut out, id);
|
|
out.push_str("\"/>");
|
|
}
|
|
out.push_str("</m:AttachmentIds></m:GetAttachment>");
|
|
out
|
|
}
|
|
|
|
pub fn write_folder_id_element(out: &mut String, folder: &FolderId) {
|
|
out.push_str("<t:FolderId Id=\"");
|
|
write_escaped(out, &folder.id);
|
|
out.push('"');
|
|
if !folder.change_key.is_empty() {
|
|
out.push_str(" ChangeKey=\"");
|
|
write_escaped(out, &folder.change_key);
|
|
out.push('"');
|
|
}
|
|
out.push_str("/>");
|
|
}
|
|
|
|
pub fn write_item_id_element(out: &mut String, id: &ItemId) {
|
|
out.push_str("<t:ItemId Id=\"");
|
|
write_escaped(out, &id.id);
|
|
out.push('"');
|
|
if !id.change_key.is_empty() {
|
|
out.push_str(" ChangeKey=\"");
|
|
write_escaped(out, &id.change_key);
|
|
out.push('"');
|
|
}
|
|
out.push_str("/>");
|
|
}
|
|
|
|
fn write_folder_ref(out: &mut String, parent: FolderRef<'_>) {
|
|
match parent {
|
|
FolderRef::Distinguished(d) => {
|
|
out.push_str("<t:DistinguishedFolderId Id=\"");
|
|
out.push_str(d.as_str());
|
|
out.push_str("\"/>");
|
|
}
|
|
FolderRef::Concrete(f) => write_folder_id_element(out, f),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn find_folder_uses_deep_traversal_for_msgfolderroot() {
|
|
let body = find_folder_body(
|
|
FolderRef::Distinguished(DistinguishedFolderId::MsgFolderRoot),
|
|
Traversal::Deep,
|
|
);
|
|
assert!(body.contains("Traversal=\"Deep\""));
|
|
assert!(body.contains("<t:DistinguishedFolderId Id=\"msgfolderroot\"/>"));
|
|
assert!(body.contains("folder:FolderClass"));
|
|
assert!(body.contains("folder:DisplayName"));
|
|
assert!(body.contains("folder:ParentFolderId"));
|
|
}
|
|
|
|
#[test]
|
|
fn find_folder_archive_uses_archiveroot() {
|
|
let body = find_folder_body(
|
|
FolderRef::Distinguished(DistinguishedFolderId::ArchiveRoot),
|
|
Traversal::Deep,
|
|
);
|
|
assert!(body.contains("<t:DistinguishedFolderId Id=\"archiveroot\"/>"));
|
|
}
|
|
|
|
#[test]
|
|
fn find_item_paginates_with_offset_and_page_size() {
|
|
let folder = FolderId::new("FID", "FCK");
|
|
let body = find_item_body(FolderRef::Concrete(&folder), Traversal::Shallow, 200, 50);
|
|
assert!(body.contains("Offset=\"200\""));
|
|
assert!(body.contains("MaxEntriesReturned=\"50\""));
|
|
assert!(body.contains("<t:FolderId Id=\"FID\" ChangeKey=\"FCK\"/>"));
|
|
}
|
|
|
|
#[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);
|
|
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 get_item_calendar_shape_lists_calendar_fields() {
|
|
let body = get_item_body(ItemShape::CalendarItem, &[ItemId::new("X", "")]);
|
|
assert!(body.contains("calendar:Recurrence"));
|
|
assert!(body.contains("calendar:ModifiedOccurrences"));
|
|
assert!(body.contains("calendar:DeletedOccurrences"));
|
|
assert!(body.contains("calendar:UID"));
|
|
assert!(!body.contains("<t:IncludeMimeContent>"));
|
|
}
|
|
|
|
#[test]
|
|
fn get_item_contact_shape_requests_all_properties() {
|
|
let body = get_item_body(ItemShape::Contact, &[ItemId::new("X", "")]);
|
|
assert!(body.contains("<t:BaseShape>AllProperties</t:BaseShape>"));
|
|
}
|
|
|
|
#[test]
|
|
fn sync_folder_items_body_carries_state_and_max() {
|
|
let folder = FolderId::new("FID", "FCK");
|
|
let body = sync_folder_items_body(&folder, "STATE", 512);
|
|
assert!(body.contains("<m:SyncFolderId><t:FolderId Id=\"FID\" ChangeKey=\"FCK\"/>"));
|
|
assert!(body.contains("<m:SyncState>STATE</m:SyncState>"));
|
|
assert!(body.contains("<m:MaxChangesReturned>512</m:MaxChangesReturned>"));
|
|
assert!(body.contains("<m:SyncScope>NormalItems</m:SyncScope>"));
|
|
}
|
|
|
|
#[test]
|
|
fn sync_folder_items_empty_state_round_trips() {
|
|
let folder = FolderId::new("FID", "");
|
|
let body = sync_folder_items_body(&folder, "", 100);
|
|
assert!(body.contains("<m:SyncState></m:SyncState>"));
|
|
}
|
|
|
|
#[test]
|
|
fn get_attachment_lists_ids() {
|
|
let body = get_attachment_body(&["AID1", "AID2"]);
|
|
assert!(body.contains("<t:AttachmentId Id=\"AID1\"/>"));
|
|
assert!(body.contains("<t:AttachmentId Id=\"AID2\"/>"));
|
|
assert!(body.contains("<t:IncludeMimeContent>true</t:IncludeMimeContent>"));
|
|
}
|
|
|
|
#[test]
|
|
fn get_folder_emits_distinguished_ids() {
|
|
let body = get_folder_body(
|
|
&[
|
|
FolderRef::Distinguished(DistinguishedFolderId::Inbox),
|
|
FolderRef::Distinguished(DistinguishedFolderId::SentItems),
|
|
],
|
|
FolderShape::IdOnly,
|
|
);
|
|
assert!(body.contains("<t:DistinguishedFolderId Id=\"inbox\"/>"));
|
|
assert!(body.contains("<t:DistinguishedFolderId Id=\"sentitems\"/>"));
|
|
assert!(body.contains("<t:BaseShape>IdOnly</t:BaseShape>"));
|
|
}
|
|
}
|