dovemail 0.0.1

Use Microsoft's PNP CLI with email clients.
Documentation
//! Type definitions for the Microsoft Graph API's Outlook message object.

use std::borrow::Cow;
use std::fmt::{Display, Formatter};
use time::OffsetDateTime;

/// An Outlook message (i.e. email).
///
/// Note: this definition is not complete.
///
/// Spec: https://learn.microsoft.com/en-us/graph/api/resources/message?view=graph-rest-1.0#properties
///
/// Example: https://pnp.github.io/cli-microsoft365/cmd/outlook/message/message-get#response
#[derive(serde::Deserialize, PartialEq, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct OutlookMessage {
    pub id: String,
    pub parent_folder_id: String,
    pub conversation_id: String,
    pub conversation_index: String,
    pub internet_message_id: String,
    #[serde(with = "time::serde::iso8601")]
    pub created_date_time: OffsetDateTime,
    #[serde(with = "time::serde::iso8601")]
    pub last_modified_date_time: OffsetDateTime,
    #[serde(with = "time::serde::iso8601")]
    pub received_date_time: OffsetDateTime,
    #[serde(with = "time::serde::iso8601")]
    pub sent_date_time: OffsetDateTime,
    pub has_attachments: bool,
    pub subject: String,
    pub body_preview: String,
    pub importance: Importance,
    pub web_link: String,

    // The spec doesn't say these can be null, however I have observed isDeliveryReceiptRequested
    // to be frequently null.
    pub is_delivery_receipt_requested: Option<bool>,
    pub is_read_receipt_requested: Option<bool>,
    pub is_read: Option<bool>,
    pub is_draft: Option<bool>,

    pub body: ItemBody,
    pub sender: Option<Recipient>,
    pub from: Option<Recipient>,
    pub to_recipients: Vec<Recipient>,
    pub cc_recipients: Vec<Recipient>,
    pub bcc_recipients: Vec<Recipient>,
}

/// The importance of an Outlook message.
#[derive(serde::Deserialize, Debug, Copy, Clone, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum Importance {
    Low,
    Normal,
    High,
}

/// Item body content type.
#[derive(serde::Deserialize, Debug, Copy, Clone, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum ContentType {
    Text,
    Html,
}

/// Spec: https://learn.microsoft.com/en-us/graph/api/resources/itembody?view=graph-rest-1.0
#[derive(serde::Deserialize, PartialEq, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ItemBody {
    pub content: String,
    pub content_type: ContentType,
}

/// Spec: https://learn.microsoft.com/en-us/graph/api/resources/recipient?view=graph-rest-1.0
#[derive(serde::Deserialize, PartialEq, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Recipient {
    pub email_address: EmailAddress,
}

/// https://learn.microsoft.com/en-us/graph/api/resources/emailaddress?view=graph-rest-1.0
#[derive(serde::Deserialize, PartialEq, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct EmailAddress {
    pub name: String,
    pub address: String,
}

impl Display for Recipient {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.email_address)
    }
}

impl Display for EmailAddress {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} <{}>", self.name, self.address)
    }
}

impl Importance {
    /// Importance string value
    pub fn as_str(&self) -> &'static str {
        match self {
            Importance::Low => "low",
            Importance::Normal => "normal",
            Importance::High => "high",
        }
    }
}

impl ContentType {
    /// MIME type
    pub fn as_str(&self) -> &'static str {
        match self {
            ContentType::Text => "text/plain",
            ContentType::Html => "text/html",
        }
    }
}

impl ItemBody {
    pub fn to_sanitized(&self) -> Cow<str> {
        match self.content_type {
            ContentType::Text => {
                // for whatever reason, Outlook API will return line breaks as <carriage return> <backslash> <newline>
                let sanitized = self.content.replace("\r\\\n", "\n");
                Cow::Owned(sanitized)
            }
            ContentType::Html => Cow::Borrowed(&self.content),
        }
    }
}