From 6d85d50d81ee6a7ec892160ff30dc7f1c5428e2d Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 20 Jun 2024 18:47:31 +0100 Subject: [PATCH] Add types for CreateItem (#11) This change undoes part of https://github.com/thunderbird/ews-rs/pull/10 following complications from making the item ID optional. See https://phabricator.services.mozilla.com/D211258 for more context. Instead of using `common::Message` for both serialization and deserialization, this change introduces a new `Message` type that is specific to the `CreateItem` operation. For now we agreed on only adding the fields that Thunderbird uses, but ultimately `create_item::Message` should include every field that `common::Message` does. --- src/types.rs | 1 + src/types/common.rs | 202 ++++++++++++++++++++++++++++++++++----- src/types/create_item.rs | 140 +++++++++++++++++++++++++++ src/types/get_item.rs | 10 +- 4 files changed, 321 insertions(+), 32 deletions(-) create mode 100644 src/types/create_item.rs diff --git a/src/types.rs b/src/types.rs index ca1a73c..8c85f6f 100644 --- a/src/types.rs +++ b/src/types.rs @@ -9,6 +9,7 @@ pub use common::*; pub use operations::*; pub mod soap; +pub mod create_item; pub mod get_folder; pub mod get_item; pub mod sync_folder_hierarchy; diff --git a/src/types/common.rs b/src/types/common.rs index 7071a9b..4f21a64 100644 --- a/src/types/common.rs +++ b/src/types/common.rs @@ -2,7 +2,9 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -use serde::Deserialize; +use std::ops::{Deref, DerefMut}; + +use serde::{Deserialize, Deserializer}; use time::format_description::well_known::Iso8601; use xml_struct::XmlSerialize; @@ -278,7 +280,7 @@ pub enum BaseItemId { /// The unique identifier of an item. /// /// See -#[derive(Debug, Deserialize, XmlSerialize)] +#[derive(Clone, Debug, Deserialize, XmlSerialize, PartialEq)] pub struct ItemId { #[xml_struct(attribute)] #[serde(rename = "@Id")] @@ -359,11 +361,18 @@ pub enum Folder { }, } +/// An array of items. +#[derive(Debug, Deserialize)] +pub struct Items { + #[serde(rename = "$value", default)] + pub inner: Vec, +} + /// An item which may appear as the result of a request to read or modify an /// Exchange item. /// /// See -#[derive(Debug, Deserialize, XmlSerialize)] +#[derive(Debug, Deserialize)] pub enum RealItem { Message(Message), } @@ -372,7 +381,8 @@ pub enum RealItem { /// /// See [`Attachment::ItemAttachment`] for details. // N.B.: Commented-out variants are not yet implemented. -#[derive(Debug, Deserialize, XmlSerialize)] +#[non_exhaustive] +#[derive(Debug, Deserialize)] pub enum AttachmentItem { // Item(Item), Message(Message), @@ -413,14 +423,13 @@ impl XmlSerialize for DateTime { /// An email message. /// /// See -#[derive(Debug, Deserialize, XmlSerialize)] +#[derive(Debug, Deserialize)] #[serde(rename_all = "PascalCase")] pub struct Message { /// The MIME content of the item. pub mime_content: Option, - /// The item's Exchange identifier. - pub item_id: Option, + pub item_id: ItemId, /// The identifier for the containing folder. /// @@ -436,7 +445,6 @@ pub struct Message { /// /// See pub subject: Option, - pub sensitivity: Option, pub body: Option, pub attachments: Option, @@ -447,7 +455,6 @@ pub struct Message { /// /// See pub categories: Option>, - pub importance: Option, pub in_reply_to: Option, pub is_submitted: Option, @@ -465,7 +472,7 @@ pub struct Message { pub display_to: Option, pub has_attachments: Option, pub culture: Option, - pub sender: Option, + pub sender: Option, pub to_recipients: Option, pub cc_recipients: Option, pub bcc_recipients: Option, @@ -473,13 +480,13 @@ pub struct Message { pub is_delivery_receipt_requested: Option, pub conversation_index: Option, pub conversation_topic: Option, - pub from: Option, + pub from: Option, pub internet_message_id: Option, pub is_read: Option, pub is_response_requested: Option, - pub reply_to: Option, - pub received_by: Option, - pub received_representing: Option, + pub reply_to: Option, + pub received_by: Option, + pub received_representing: Option, pub last_modified_name: Option, pub last_modified_time: Option, pub is_associated: Option, @@ -496,18 +503,61 @@ pub struct Attachments { pub inner: Vec, } +/// A newtype around a vector of `Recipient`s, that is deserialized using +/// `deserialize_recipients`. +#[derive(Debug, Default, Deserialize, XmlSerialize)] +pub struct ArrayOfRecipients( + #[serde(deserialize_with = "deserialize_recipients")] pub Vec, +); + +impl Deref for ArrayOfRecipients { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for ArrayOfRecipients { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + /// A single mailbox. -#[derive(Debug, Deserialize, XmlSerialize)] +#[derive(Debug, Deserialize, XmlSerialize, PartialEq)] #[serde(rename_all = "PascalCase")] -pub struct SingleRecipient { +pub struct Recipient { + #[xml_struct(ns_prefix = "t")] pub mailbox: Mailbox, } -/// A list of mailboxes. -#[derive(Debug, Deserialize, XmlSerialize)] -#[serde(rename_all = "PascalCase")] -pub struct ArrayOfRecipients { - pub mailbox: Vec, +/// Deserializes a list of recipients. +/// +/// `quick-xml`'s `serde` implementation requires the presence of an +/// intermediate type when dealing with lists, and this is not compatible with +/// our model for serialization. +/// +/// We could directly deserialize into a `Vec`, which would also +/// simplify this function a bit, but this would mean using different models +/// to represent single vs. multiple recipient(s). +fn deserialize_recipients<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + #[derive(Debug, Deserialize)] + #[serde(rename_all = "PascalCase")] + struct MailboxSequence { + mailbox: Vec, + } + + let seq = MailboxSequence::deserialize(deserializer)?; + + Ok(seq + .mailbox + .into_iter() + .map(|mailbox| Recipient { mailbox }) + .collect()) } /// A list of Internet Message Format headers. @@ -520,13 +570,15 @@ pub struct InternetMessageHeaders { /// A reference to a user or address which can send or receive mail. /// /// See -#[derive(Debug, Deserialize, XmlSerialize)] +#[derive(Clone, Debug, Deserialize, XmlSerialize, PartialEq)] #[serde(rename_all = "PascalCase")] pub struct Mailbox { /// The name of this mailbox's user. + #[xml_struct(ns_prefix = "t")] pub name: Option, /// The email address for this mailbox. + #[xml_struct(ns_prefix = "t")] pub email_address: String, /// The protocol used in routing to this mailbox. @@ -547,7 +599,7 @@ pub struct Mailbox { /// A protocol used in routing mail. /// /// See -#[derive(Clone, Copy, Debug, Default, Deserialize, XmlSerialize)] +#[derive(Clone, Copy, Debug, Default, Deserialize, XmlSerialize, PartialEq)] #[xml_struct(text)] pub enum RoutingType { #[default] @@ -558,7 +610,7 @@ pub enum RoutingType { /// The type of sender or recipient a mailbox represents. /// /// See -#[derive(Clone, Copy, Debug, Deserialize, XmlSerialize)] +#[derive(Clone, Copy, Debug, Deserialize, XmlSerialize, PartialEq)] #[xml_struct(text)] pub enum MailboxType { Mailbox, @@ -829,3 +881,105 @@ pub struct MessageXml { /// if the server is throttling operations. pub back_off_milliseconds: Option, } + +#[cfg(test)] +mod tests { + use quick_xml::Writer; + + use super::*; + use crate::Error; + + /// Tests that an [`ArrayOfRecipients`] correctly serializes into XML. It + /// should serialize as multiple `` elements, one per [`Recipient`]. + #[test] + fn serialize_array_of_recipients() -> Result<(), Error> { + // Define the recipients to serialize. + let alice = Recipient { + mailbox: Mailbox { + name: Some("Alice Test".into()), + email_address: "alice@test.com".into(), + routing_type: None, + mailbox_type: None, + item_id: None, + }, + }; + + let bob = Recipient { + mailbox: Mailbox { + name: Some("Bob Test".into()), + email_address: "bob@test.com".into(), + routing_type: None, + mailbox_type: None, + item_id: None, + }, + }; + + let recipients = ArrayOfRecipients(vec![alice, bob]); + + // Serialize into XML. + let mut writer = { + let inner: Vec = Default::default(); + Writer::new(inner) + }; + recipients.serialize_as_element(&mut writer, "Recipients")?; + + // Read the contents of the `Writer`'s buffer. + let buf = writer.into_inner(); + let actual = std::str::from_utf8(buf.as_slice()) + .map_err(|e| Error::UnexpectedResponse(e.to_string().into_bytes()))?; + + // Ensure the structure of the XML document is correct. + let expected = "Alice Testalice@test.comBob Testbob@test.com"; + assert_eq!(expected, actual); + + Ok(()) + } + + /// Tests that deserializing a sequence of `` XML elements + /// results in an [`ArrayOfRecipients`] with one [`Recipient`] per + /// `` element. + #[test] + fn deserialize_array_of_recipients() -> Result<(), Error> { + // The raw XML to deserialize. + let xml = "Alice Testalice@test.comBob Testbob@test.com"; + + // Deserialize the raw XML, with `serde_path_to_error` to help + // troubleshoot any issue. + let mut de = quick_xml::de::Deserializer::from_reader(xml.as_bytes()); + let recipients: ArrayOfRecipients = serde_path_to_error::deserialize(&mut de)?; + + // Ensure we have the right number of recipients in the resulting + // `ArrayOfRecipients`. + assert_eq!(recipients.0.len(), 2); + + // Ensure the first recipient correctly has a name and address. + assert_eq!( + recipients.get(0).expect("no recipient at index 0"), + &Recipient { + mailbox: Mailbox { + name: Some("Alice Test".into()), + email_address: "alice@test.com".into(), + routing_type: None, + mailbox_type: None, + item_id: None, + }, + } + ); + + // Ensure the second recipient correctly has a name and address. + assert_eq!( + recipients.get(1).expect("no recipient at index 1"), + &Recipient { + mailbox: Mailbox { + name: Some("Bob Test".into()), + email_address: "bob@test.com".into(), + routing_type: None, + mailbox_type: None, + item_id: None, + }, + } + ); + + Ok(()) + } +} diff --git a/src/types/create_item.rs b/src/types/create_item.rs new file mode 100644 index 0000000..88ea7ba --- /dev/null +++ b/src/types/create_item.rs @@ -0,0 +1,140 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use serde::Deserialize; +use xml_struct::XmlSerialize; + +use crate::{ + types::sealed::EnvelopeBodyContents, ArrayOfRecipients, BaseFolderId, Items, MimeContent, + Operation, OperationResponse, ResponseClass, ResponseCode, MESSAGES_NS_URI, +}; + +/// The action an Exchange server will take upon creating a `Message` item. +/// +/// See +#[derive(Debug, XmlSerialize)] +#[xml_struct(text)] +pub enum MessageDisposition { + SaveOnly, + SendOnly, + SendAndSaveCopy, +} + +/// A request to create (and optionally send) one or more Exchange item(s). +/// +/// See +#[derive(Debug, XmlSerialize)] +#[xml_struct(default_ns = MESSAGES_NS_URI)] +pub struct CreateItem { + /// The action the Exchange server will take upon creating this item. + /// + /// This field is required for and only applicable to [`Message`] items. + /// + /// [`Message`]: `crate::Message` + /// + /// See + #[xml_struct(attribute)] + pub message_disposition: Option, + + /// The folder in which to store an item once it has been created. + /// + /// This is ignored if `message_disposition` is [`SendOnly`]. + /// + /// See + /// + /// [`SendOnly`]: [`MessageDisposition::SendOnly`] + pub saved_item_folder_id: Option, + + /// The item or items to create. + pub items: Vec, +} + +/// A new item that appears in a CreateItem request. +/// +/// See +// N.B.: Commented-out variants are not yet implemented. +#[non_exhaustive] +#[derive(Debug, XmlSerialize)] +pub enum Item { + // Item(Item), + Message(Message), + // CalendarItem(CalendarItem), + // Contact(Contact), + // Task(Task), + // MeetingMessage(MeetingMessage), + // MeetingRequest(MeetingRequest), + // MeetingResponse(MeetingResponse), + // MeetingCancellation(MeetingCancellation), +} + +/// An email message to create. +/// +/// This struct follows the same specification to [`common::Message`], but has a +/// few differences that allow the creation of new messages without forcing any +/// tradeoff on strictness when deserializing; for example not making the item +/// ID a required field. +/// +/// See +/// +/// [`common::message`]: crate::Message +#[derive(Debug, Default, XmlSerialize)] +pub struct Message { + /// The MIME content of the item. + pub mime_content: Option, + // Whether to request a delivery receipt. + pub is_delivery_receipt_requested: Option, + // The message ID for the message, semantically identical to the Message-ID + // header. + pub internet_message_id: Option, + // Recipients to include as Bcc, who won't be included in the MIME content. + pub bcc_recipients: Option, +} + +impl Operation for CreateItem { + type Response = CreateItemResponse; +} + +impl EnvelopeBodyContents for CreateItem { + fn name() -> &'static str { + "CreateItem" + } +} + +/// A response to a [`CreateItem`] request. +/// +/// See +#[derive(Debug, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct CreateItemResponse { + pub response_messages: ResponseMessages, +} + +impl OperationResponse for CreateItemResponse {} + +impl EnvelopeBodyContents for CreateItemResponse { + fn name() -> &'static str { + "CreateItemResponse" + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct ResponseMessages { + pub create_item_response_message: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct CreateItemResponseMessage { + /// The status of the corresponding request, i.e. whether it succeeded or + /// resulted in an error. + #[serde(rename = "@ResponseClass")] + pub response_class: ResponseClass, + + pub response_code: Option, + + pub message_text: Option, + + pub items: Items, +} diff --git a/src/types/get_item.rs b/src/types/get_item.rs index 73092e0..ce05972 100644 --- a/src/types/get_item.rs +++ b/src/types/get_item.rs @@ -6,8 +6,8 @@ use serde::Deserialize; use xml_struct::XmlSerialize; use crate::{ - types::sealed::EnvelopeBodyContents, BaseItemId, ItemShape, Operation, OperationResponse, - RealItem, ResponseClass, ResponseCode, MESSAGES_NS_URI, + types::sealed::EnvelopeBodyContents, BaseItemId, ItemShape, Items, Operation, + OperationResponse, ResponseClass, ResponseCode, MESSAGES_NS_URI, }; /// A request for the properties of one or more Exchange items, e.g. messages, @@ -76,9 +76,3 @@ pub struct GetItemResponseMessage { pub items: Items, } - -#[derive(Debug, Deserialize)] -pub struct Items { - #[serde(rename = "$value")] - pub inner: Vec, -}