himalaya 2.2.1

CLI to manage emails
//! # Gmail history list
//!
//! The `gmail history list` command, `users.history.list`.

use std::fmt;

use anyhow::Result;
use clap::{Parser, ValueEnum};
use io_gmail::v1::rest::history::{
    GmailHistory, GmailHistoryLabel, GmailHistoryMessage, GmailHistoryType,
    list::{GmailHistoryList, GmailHistoryListParams},
};
use pimalaya_cli::printer::Printer;
use schemars::JsonSchema;
use serde::Serialize;

use crate::{gmail::client::GmailClient, shared::output::Paginated};

/// List the changes applied to the mailbox since a given history id.
///
/// JSON includes `history-id`, `history`, and an optional `next_page` cursor.
/// Each record retains its message-id arrays and adds `messages-added-details`
/// with each arrival's `id`, nullable `thread-id`, and `label-ids` supplied by
/// Gmail. Missing labels become an empty array; text output shows counts.
#[derive(Debug, Parser)]
pub struct GmailHistoryListCommand {
    /// History id to start listing changes from.
    #[arg(long = "start-history-id", value_name = "ID")]
    pub start_history_id: String,
    /// Restrict the listing to changes affecting this label id.
    #[arg(long = "label-id", value_name = "ID")]
    pub label_id: Option<String>,
    /// History change types to include (repeatable).
    #[arg(long = "history-type", value_name = "TYPE")]
    pub history_types: Vec<HistoryTypeArg>,
    /// Maximum number of history records to return.
    #[arg(short = 's', long, value_name = "N")]
    pub max_results: Option<u32>,
    /// Page token from a previous listing, for pagination.
    #[arg(long, value_name = "TOKEN")]
    pub page_token: Option<String>,
}

impl GmailHistoryListCommand {
    /// Lists one page of history records and tables it.
    pub fn execute(self, printer: &mut impl Printer, client: &mut GmailClient) -> Result<()> {
        let types: Vec<GmailHistoryType> =
            self.history_types.iter().copied().map(Into::into).collect();

        let out = {
            let params = GmailHistoryListParams {
                start_history_id: &self.start_history_id,
                label_id: self.label_id.as_deref(),
                history_types: &types,
                max_results: self.max_results,
                page_token: self.page_token.as_deref(),
            };
            let c = GmailHistoryList::new(&client.auth, &client.user_id, &params)?;
            client.run(c)?
        };

        let resp = out.response;

        let history = resp
            .history
            .into_iter()
            .map(GmailHistoryRecordOutput::from)
            .collect();

        let output = GmailHistoryListOutput {
            history_id: resp.history_id,
            history,
        };

        printer.out(Paginated::new(output, resp.next_page_token))
    }
}

/// The kind of change a history record reports.
#[derive(Clone, Copy, Debug, ValueEnum)]
#[clap(rename_all = "camelCase")]
pub enum HistoryTypeArg {
    /// A message arrived in the mailbox.
    MessageAdded,
    /// A message left the mailbox.
    MessageDeleted,
    /// A label was put on a message.
    LabelAdded,
    /// A label was taken off a message.
    LabelRemoved,
}

impl From<HistoryTypeArg> for GmailHistoryType {
    fn from(arg: HistoryTypeArg) -> Self {
        match arg {
            HistoryTypeArg::MessageAdded => GmailHistoryType::MessageAdded,
            HistoryTypeArg::MessageDeleted => GmailHistoryType::MessageDeleted,
            HistoryTypeArg::LabelAdded => GmailHistoryType::LabelAdded,
            HistoryTypeArg::LabelRemoved => GmailHistoryType::LabelRemoved,
        }
    }
}

/// The `gmail history list` output, one summary line per record.
///
/// JSON carries affected message ids and added-message details; text shows
/// counts for incremental sync summaries.
#[derive(Serialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub(crate) struct GmailHistoryListOutput {
    #[serde(skip_serializing_if = "Option::is_none")]
    history_id: Option<String>,
    history: Vec<GmailHistoryRecordOutput>,
}

impl fmt::Display for GmailHistoryListOutput {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let history_id = self.history_id.as_deref().unwrap_or("(none)");
        writeln!(f, "New history id: {history_id}")?;

        if self.history.is_empty() {
            return writeln!(f, "No history changes since the given history id.");
        }

        for record in &self.history {
            writeln!(
                f,
                "{}: +{}msg -{}msg +{}lbl -{}lbl",
                record.id,
                record.messages_added.len(),
                record.messages_deleted.len(),
                record.labels_added.len(),
                record.labels_removed.len(),
            )?;
        }

        Ok(())
    }
}

/// A single change to the mailbox since the requested history id.
#[derive(Serialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub(crate) struct GmailHistoryRecordOutput {
    id: String,
    messages_added: Vec<String>,
    messages_added_details: Vec<GmailHistoryMessageOutput>,
    messages_deleted: Vec<String>,
    labels_added: Vec<GmailHistoryLabelOutput>,
    labels_removed: Vec<GmailHistoryLabelOutput>,
}

impl From<GmailHistory> for GmailHistoryRecordOutput {
    fn from(record: GmailHistory) -> Self {
        Self {
            id: record.id,
            messages_added_details: record
                .messages_added
                .iter()
                .map(|entry| GmailHistoryMessageOutput {
                    id: entry.message.id.clone(),
                    thread_id: entry.message.thread_id.clone(),
                    label_ids: entry.message.label_ids.clone(),
                })
                .collect(),
            messages_added: message_ids(record.messages_added),
            messages_deleted: message_ids(record.messages_deleted),
            labels_added: label_changes(record.labels_added),
            labels_removed: label_changes(record.labels_removed),
        }
    }
}

/// Identity and labels supplied by Gmail for an added message.
#[derive(Serialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub(crate) struct GmailHistoryMessageOutput {
    id: String,
    thread_id: Option<String>,
    label_ids: Vec<String>,
}

/// Labels added to or removed from one message in a history record.
#[derive(Serialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub(crate) struct GmailHistoryLabelOutput {
    message_id: String,
    label_ids: Vec<String>,
}

/// Collects the ids of the messages a history change applies to.
fn message_ids(messages: Vec<GmailHistoryMessage>) -> Vec<String> {
    messages
        .into_iter()
        .map(|message| message.message.id)
        .collect()
}

/// Collects the label changes of a history record, keyed by message.
fn label_changes(labels: Vec<GmailHistoryLabel>) -> Vec<GmailHistoryLabelOutput> {
    labels
        .into_iter()
        .map(|label| GmailHistoryLabelOutput {
            message_id: label.message.id,
            label_ids: label.label_ids,
        })
        .collect()
}