use std::{ fmt, io::{stdin, BufRead}, }; use anyhow::{bail, Context, Result}; use clap::Parser; use io_jmap::{ rfc8620::{send::JmapRequest, session::capabilities::CORE}, rfc8621::capabilities::MAIL, }; use pimalaya_cli::printer::Printer; use serde::Serialize; use serde_json::Value; use crate::jmap::account::JmapAccount; /// Send a raw JMAP method-calls array and print the response. /// /// METHOD_CALLS must be a JSON array of JMAP method call tuples: /// /// '[["Mailbox/query", {"filter": {"role": "inbox"}}, "c0"]]' /// /// The `accountId` field is injected into each call's arguments /// automatically if not already present. Pass `-` or omit to read /// from stdin. #[derive(Debug, Parser)] pub struct JmapQueryCommand { /// Extra capability URNs to declare (core and mail are always included). #[arg(long = "using", value_name = "URN")] pub using: Vec, /// The JMAP methodCalls JSON array (or omit / pass `-` to read stdin). #[arg(trailing_var_arg = true)] #[arg(name = "method-calls", value_name = "METHOD_CALLS")] pub method_calls: Vec, } impl JmapQueryCommand { pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> { let mut client = account.new_jmap_client()?; let raw = if self.method_calls.is_empty() || self.method_calls.first().map(|s| s.as_str()) == Some("-") { stdin() .lock() .lines() .map_while(Result::ok) .collect::>() .join("\n") } else { self.method_calls.join(" ") }; let calls_value: Value = serde_json::from_str(&raw).context("METHOD_CALLS is not valid JSON")?; let Value::Array(calls_arr) = calls_value else { bail!("METHOD_CALLS must be a JSON array"); }; let account_id = client .session() .and_then(|s| s.primary_accounts.get(MAIL).cloned()) .unwrap_or_default(); let mut method_calls = Vec::with_capacity(calls_arr.len()); for (i, call) in calls_arr.into_iter().enumerate() { let Value::Array(mut tuple) = call else { bail!("method call #{i} must be a JSON array [name, args, callId]"); }; if tuple.len() != 3 { bail!("method call #{i} must have exactly 3 elements [name, args, callId]"); } let call_id = match tuple.remove(2) { Value::String(s) => s, v => bail!("method call #{i} callId must be a string, got {v}"), }; let mut args = tuple.remove(1); let name = match tuple.remove(0) { Value::String(s) => s, v => bail!("method call #{i} name must be a string, got {v}"), }; if let Value::Object(ref mut map) = args { map.entry("accountId") .or_insert_with(|| Value::String(account_id.clone())); } method_calls.push((name, args, call_id)); } let mut using = vec![CORE.to_string(), MAIL.to_string()]; for extra in self.using { if !using.contains(&extra) { using.push(extra); } } let request = JmapRequest { using, method_calls, created_ids: None, }; let response = client.send_raw(request)?; printer.out(RawResponse { method_responses: response.method_responses, }) } } /// Wraps the raw method_responses for display. #[derive(Serialize)] struct RawResponse { method_responses: Vec<(String, Value, String)>, } impl fmt::Display for RawResponse { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match serde_json::to_string_pretty(&self.method_responses) { Ok(s) => write!(f, "{s}"), Err(e) => write!(f, ""), } } }