himalaya 2.2.1

CLI to manage emails
//! # IMAP status
//!
//! The `imap status` command, RFC 3501 `STATUS`.

use io_imap::client::ImapClient as _;
use std::fmt;

use anyhow::Result;
use clap::Parser;
use io_imap::types::status::{StatusDataItem, StatusDataItemName};
use pimalaya_cli::printer::Printer;
use pimalaya_cli::table::{Cell, Row, Table};
use schemars::JsonSchema;
use serde::{Serialize, Serializer};

use crate::{
    account::context::Account,
    imap::{client::ImapClient, mailbox::arg::MailboxNameArg},
    shared::table::style_from_preset,
};

/// Get the status of the given mailbox (STATUS, RFC 3501).
///
/// Message counts and UID values, without selecting the mailbox.
#[derive(Debug, Parser)]
pub struct ImapMailboxStatusCommand {
    #[command(flatten)]
    pub mailbox_name: MailboxNameArg,
}

impl ImapMailboxStatusCommand {
    /// Asks for every status item and tables what came back.
    pub fn execute(
        self,
        printer: &mut impl Printer,
        account: &mut Account,
        client: &mut ImapClient,
    ) -> Result<()> {
        let mailbox = self.mailbox_name.inner.try_into()?;
        let item_names = vec![
            StatusDataItemName::Messages,
            StatusDataItemName::Recent,
            StatusDataItemName::Unseen,
            StatusDataItemName::UidNext,
            StatusDataItemName::UidValidity,
        ];

        let items = client.status(mailbox, item_names.into())?;

        let table = MailboxStatusTable {
            preset: account.table_preset().to_string(),
            status: items.into(),
        };

        printer.out(table)
    }
}

/// The `imap status` output, one field per data item the server returned.
#[derive(Clone, Debug, Serialize, JsonSchema)]
pub struct MailboxStatus {
    /// Total message count.
    pub messages: Option<u32>,
    /// Count of messages carrying `\Recent`.
    pub recent: Option<u32>,
    /// The UID the next message will be assigned.
    pub uid_next: Option<u32>,
    /// The UID validity, which changes when every UID does.
    pub uid_validity: Option<u32>,
    /// Count of messages lacking `\Seen`.
    pub unseen: Option<u32>,
    /// Count of messages carrying `\Deleted`, per RFC 9208.
    pub deleted: Option<u32>,
    /// Bytes an expunge would reclaim, per RFC 9208.
    pub deleted_storage: Option<u64>,
    /// The highest modification sequence, per RFC 7162 CONDSTORE.
    pub highest_mod_seq: Option<u64>,
}

impl From<Vec<StatusDataItem>> for MailboxStatus {
    fn from(items: Vec<StatusDataItem>) -> Self {
        let mut status = MailboxStatus {
            messages: None,
            recent: None,
            uid_next: None,
            uid_validity: None,
            unseen: None,
            deleted: None,
            deleted_storage: None,
            highest_mod_seq: None,
        };

        for item in items {
            match item {
                StatusDataItem::Messages(n) => status.messages = Some(n),
                StatusDataItem::Recent(n) => status.recent = Some(n),
                StatusDataItem::UidNext(n) => status.uid_next = Some(n.get()),
                StatusDataItem::UidValidity(n) => status.uid_validity = Some(n.get()),
                StatusDataItem::Unseen(n) => status.unseen = Some(n),
                StatusDataItem::Deleted(n) => status.deleted = Some(n),
                StatusDataItem::DeletedStorage(n) => status.deleted_storage = Some(n),
                StatusDataItem::HighestModSeq(n) => status.highest_mod_seq = Some(n),
            }
        }

        status
    }
}

/// The status rendered as a table of attributes and values.
pub struct MailboxStatusTable {
    /// The `comfy_table` preset string the table renders with.
    pub preset: String,
    /// The status the server reported.
    pub status: MailboxStatus,
}

impl fmt::Display for MailboxStatusTable {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut table = Table::new();

        table
            .load_style(style_from_preset(&self.preset))
            .set_header(Row::from([Cell::new("ATTRIBUTE"), Cell::new("VALUE")]));

        if let Some(n) = self.status.messages {
            table.add_row(Row::from([Cell::new("Messages"), Cell::new(n)]));
        }

        if let Some(n) = self.status.recent {
            table.add_row(Row::from([Cell::new("Recent"), Cell::new(n)]));
        }

        if let Some(n) = self.status.unseen {
            table.add_row(Row::from([Cell::new("Unseen"), Cell::new(n)]));
        }

        if let Some(n) = self.status.deleted {
            table.add_row(Row::from([Cell::new("Deleted"), Cell::new(n)]));
        }

        if let Some(n) = self.status.deleted_storage {
            table.add_row(Row::from([Cell::new("Deleted storage"), Cell::new(n)]));
        }

        if let Some(deleted) = self.status.highest_mod_seq {
            table.add_row(Row::from([
                Cell::new("Highest modified sequence"),
                Cell::new(deleted),
            ]));
        }

        if let Some(n) = self.status.uid_next {
            table.add_row(Row::from([Cell::new("UID next"), Cell::new(n)]));
        }

        if let Some(n) = self.status.uid_validity {
            table.add_row(Row::from([Cell::new("UID validity"), Cell::new(n)]));
        }

        writeln!(f)?;
        writeln!(f, "{table}")
    }
}

impl Serialize for MailboxStatusTable {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        self.status.serialize(serializer)
    }
}