mirror of
https://github.com/pimalaya/himalaya.git
synced 2026-06-17 21:37:55 +08:00
refactor: clean jmap api
This commit is contained in:
+7
-4
@@ -267,11 +267,14 @@ impl TryFrom<SaslConfig> for Sasl {
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct JmapConfig {
|
||||
/// The HTTPS base URL of the JMAP server.
|
||||
/// The JMAP server address.
|
||||
///
|
||||
/// Must use the `https://` or `jmap://` scheme. Session discovery
|
||||
/// (`GET /.well-known/jmap`) is performed automatically on connection.
|
||||
pub url: Url,
|
||||
/// Accepts either a bare authority (`fastmail.com`, `mail.example.com:8080`)
|
||||
/// for automatic discovery via `GET /.well-known/jmap`, or a full URL
|
||||
/// (`https://api.fastmail.com/jmap/api/`) to connect directly to the
|
||||
/// session endpoint. Supported schemes: `http`, `https`, `jmap` (→ http),
|
||||
/// `jmaps` (→ https).
|
||||
pub server: String,
|
||||
|
||||
/// TLS configuration.
|
||||
#[serde(default)]
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ pub type JmapAccount = Account<JmapConfig>;
|
||||
impl JmapAccount {
|
||||
pub fn new_jmap_session(&self) -> Result<JmapSession> {
|
||||
JmapSession::new(
|
||||
self.backend.url.clone(),
|
||||
self.backend.server.clone(),
|
||||
self.backend.tls.clone().try_into()?,
|
||||
self.backend.auth.clone().try_into()?,
|
||||
)
|
||||
|
||||
@@ -5,9 +5,9 @@ use pimalaya_toolbox::terminal::printer::Printer;
|
||||
use crate::jmap::{
|
||||
account::JmapAccount,
|
||||
email::{
|
||||
copy::CopyEmailCommand, delete::DeleteEmailCommand, get::JmapEmailGetCommand,
|
||||
import::ImportEmailCommand, parse::ParseEmailCommand, query::JmapEmailQueryCommand,
|
||||
update::JmapEmailUpdateCommand,
|
||||
copy::JmapEmailCopyCommand, delete::JmapEmailDestroyCommand, export::ExportEmailCommand,
|
||||
get::JmapEmailGetCommand, import::ImportEmailCommand, parse::ParseEmailCommand,
|
||||
query::JmapEmailQueryCommand, read::ReadEmailCommand, update::JmapEmailUpdateCommand,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -17,11 +17,13 @@ use crate::jmap::{
|
||||
pub enum JmapEmailCommand {
|
||||
Get(JmapEmailGetCommand),
|
||||
Query(JmapEmailQueryCommand),
|
||||
Read(ReadEmailCommand),
|
||||
#[command(alias = "edit")]
|
||||
Update(JmapEmailUpdateCommand),
|
||||
#[command(aliases = ["remove", "rm"])]
|
||||
Delete(DeleteEmailCommand),
|
||||
Copy(CopyEmailCommand),
|
||||
Delete(JmapEmailDestroyCommand),
|
||||
Copy(JmapEmailCopyCommand),
|
||||
Export(ExportEmailCommand),
|
||||
Import(ImportEmailCommand),
|
||||
Parse(ParseEmailCommand),
|
||||
}
|
||||
@@ -31,9 +33,11 @@ impl JmapEmailCommand {
|
||||
match self {
|
||||
Self::Get(cmd) => cmd.execute(printer, account),
|
||||
Self::Query(cmd) => cmd.execute(printer, account),
|
||||
Self::Read(cmd) => cmd.execute(printer, account),
|
||||
Self::Update(cmd) => cmd.execute(printer, account),
|
||||
Self::Delete(cmd) => cmd.execute(printer, account),
|
||||
Self::Copy(cmd) => cmd.execute(printer, account),
|
||||
Self::Export(cmd) => cmd.execute(printer, account),
|
||||
Self::Import(cmd) => cmd.execute(printer, account),
|
||||
Self::Parse(cmd) => cmd.execute(printer, account),
|
||||
}
|
||||
|
||||
+19
-12
@@ -13,9 +13,9 @@ use crate::jmap::account::JmapAccount;
|
||||
|
||||
/// Copy JMAP emails from another account (Email/copy).
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct CopyEmailCommand {
|
||||
pub struct JmapEmailCopyCommand {
|
||||
/// Email ID(s) to copy.
|
||||
#[arg(value_name = "EMAIL-ID", required = true, num_args = 1..)]
|
||||
#[arg(value_name = "ID", required = true)]
|
||||
pub ids: Vec<String>,
|
||||
|
||||
/// Source account ID to copy from.
|
||||
@@ -23,25 +23,25 @@ pub struct CopyEmailCommand {
|
||||
pub from_account: String,
|
||||
|
||||
/// Destination mailbox ID(s) to place copies in.
|
||||
#[arg(long, value_name = "MAILBOX-ID", num_args = 0..)]
|
||||
#[arg(long, value_name = "MAILBOX-ID", required = false)]
|
||||
pub mailbox_id: Vec<String>,
|
||||
}
|
||||
|
||||
impl CopyEmailCommand {
|
||||
impl JmapEmailCopyCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
let mailbox_ids: HashMap<String, bool> =
|
||||
self.mailbox_id.iter().map(|m| (m.clone(), true)).collect();
|
||||
self.mailbox_id.into_iter().map(|m| (m, true)).collect();
|
||||
|
||||
let emails: HashMap<String, EmailCopy> = self
|
||||
.ids
|
||||
.iter()
|
||||
.into_iter()
|
||||
.map(|id| {
|
||||
(
|
||||
id.clone(),
|
||||
EmailCopy {
|
||||
id: id.clone(),
|
||||
id,
|
||||
mailbox_ids: mailbox_ids.clone(),
|
||||
keywords: None,
|
||||
received_at: None,
|
||||
@@ -61,14 +61,21 @@ impl CopyEmailCommand {
|
||||
}
|
||||
};
|
||||
|
||||
for (id, err) in ¬_created {
|
||||
let mut ctx = anyhow!("Failed to copy email `{id}`");
|
||||
if !not_created.is_empty() {
|
||||
let mut ctx = anyhow!("Copy JMAP email(s) error");
|
||||
|
||||
if let Some(desc) = &err.description {
|
||||
ctx = anyhow!(desc.clone()).context(ctx);
|
||||
for (id, err) in not_created {
|
||||
if let Some(desc) = &err.description {
|
||||
ctx = anyhow!("{id}: {desc}").context(ctx);
|
||||
}
|
||||
|
||||
if !err.properties.is_empty() {
|
||||
let props = err.properties.join(", ");
|
||||
ctx = anyhow!("{id}: Invalid properties {props}").context(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
bail!(ctx);
|
||||
bail!(ctx)
|
||||
}
|
||||
|
||||
printer.out(Message::new("Email(s) successfully copied"))
|
||||
|
||||
@@ -8,13 +8,13 @@ use crate::jmap::account::JmapAccount;
|
||||
|
||||
/// Delete JMAP emails (Email/set destroy).
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct DeleteEmailCommand {
|
||||
pub struct JmapEmailDestroyCommand {
|
||||
/// Email ID(s) to delete.
|
||||
#[arg(value_name = "ID", required = true, num_args = 1..)]
|
||||
#[arg(value_name = "ID", required = true)]
|
||||
pub ids: Vec<String>,
|
||||
}
|
||||
|
||||
impl DeleteEmailCommand {
|
||||
impl JmapEmailDestroyCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
@@ -35,14 +35,21 @@ impl DeleteEmailCommand {
|
||||
}
|
||||
};
|
||||
|
||||
for (id, err) in ¬_destroyed {
|
||||
let mut ctx = anyhow!("Failed to delete email `{id}`");
|
||||
if !not_destroyed.is_empty() {
|
||||
let mut ctx = anyhow!("Destroy JMAP email(s) error");
|
||||
|
||||
if let Some(desc) = &err.description {
|
||||
ctx = anyhow!(desc.clone()).context(ctx);
|
||||
for (id, err) in not_destroyed {
|
||||
if let Some(desc) = &err.description {
|
||||
ctx = anyhow!("{id}: {desc}").context(ctx);
|
||||
}
|
||||
|
||||
if !err.properties.is_empty() {
|
||||
let props = err.properties.join(", ");
|
||||
ctx = anyhow!("{id}: Invalid properties {props}").context(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
bail!(ctx);
|
||||
bail!(ctx)
|
||||
}
|
||||
|
||||
printer.out(Message::new("Email(s) successfully deleted"))
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use clap::Parser;
|
||||
use io_jmap::coroutines::{
|
||||
blob_download::{DownloadJmapBlob, DownloadJmapBlobResult},
|
||||
email_get::{GetJmapEmails, GetJmapEmailsResult},
|
||||
};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use pimalaya_toolbox::terminal::printer::{Message, Printer};
|
||||
use url::Url;
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
|
||||
/// Export a raw RFC 5322 message to stdout (Email/get + blob download).
|
||||
///
|
||||
/// Fetches the blobId via Email/get then downloads the raw message blob.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct ExportEmailCommand {
|
||||
/// The email ID to export.
|
||||
#[arg(value_name = "ID")]
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
impl ExportEmailCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let tls = account.backend.tls.clone().try_into()?;
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
let properties = Some(vec!["id".to_owned(), "blobId".to_owned()]);
|
||||
|
||||
let mut arg = None;
|
||||
let mut coroutine = GetJmapEmails::new(
|
||||
jmap.context,
|
||||
vec![self.id.clone()],
|
||||
properties,
|
||||
false,
|
||||
false,
|
||||
0,
|
||||
)?;
|
||||
|
||||
let emails = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
GetJmapEmailsResult::Io(io) => arg = Some(handle(&mut jmap.stream, io)?),
|
||||
GetJmapEmailsResult::Ok {
|
||||
context, emails, ..
|
||||
} => {
|
||||
jmap.context = context;
|
||||
break emails;
|
||||
}
|
||||
GetJmapEmailsResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
};
|
||||
|
||||
let account_id = jmap.context.account_id.as_deref().unwrap_or("");
|
||||
let blob_id = emails
|
||||
.into_iter()
|
||||
.next()
|
||||
.and_then(|e| e.blob_id)
|
||||
.ok_or_else(|| anyhow!("Email `{}` not found or has no blobId", self.id))?;
|
||||
|
||||
let url: Url = jmap
|
||||
.context
|
||||
.session
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.download_url
|
||||
.replace("{accountId}", account_id)
|
||||
.replace("{blobId}", &blob_id)
|
||||
.replace("{type}", "message%2Frfc822")
|
||||
.replace("{name}", "message.eml")
|
||||
.parse()?;
|
||||
|
||||
let mut stream = jmap.connect_if_different(&url, &tls)?;
|
||||
let stream = stream.as_mut().unwrap_or(&mut jmap.stream);
|
||||
|
||||
let mut coroutine = DownloadJmapBlob::new(jmap.context, &url)?;
|
||||
let mut arg = None;
|
||||
|
||||
let data = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
DownloadJmapBlobResult::Io(io) => arg = Some(handle(&mut *stream, io)?),
|
||||
DownloadJmapBlobResult::Ok { data, .. } => break data,
|
||||
DownloadJmapBlobResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
};
|
||||
|
||||
printer.out(Message::new(String::from_utf8(data)?))
|
||||
}
|
||||
}
|
||||
+11
-44
@@ -5,20 +5,16 @@ use io_stream::runtimes::std::handle;
|
||||
use log::warn;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
use crate::jmap::{account::JmapAccount, email::query::EmailsTable};
|
||||
|
||||
/// Get a JMAP email by ID (Email/get).
|
||||
/// Get JMAP emails by ID (Email/get).
|
||||
///
|
||||
/// Downloads and displays the full message content including body.
|
||||
/// Fetches and displays email envelopes as a table.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct JmapEmailGetCommand {
|
||||
/// The email ID(s) to retrieve.
|
||||
#[arg(value_name = "ID", required = true)]
|
||||
pub ids: Vec<String>,
|
||||
|
||||
/// Output raw RFC 5322 message headers.
|
||||
#[arg(long, short)]
|
||||
pub raw: bool,
|
||||
}
|
||||
|
||||
impl JmapEmailGetCommand {
|
||||
@@ -26,7 +22,7 @@ impl JmapEmailGetCommand {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
let mut coroutine =
|
||||
GetJmapEmails::new(jmap.context, self.ids.clone(), None, true, true, None)?;
|
||||
GetJmapEmails::new(jmap.context, self.ids.clone(), None, false, false, 0)?;
|
||||
let mut arg = None;
|
||||
|
||||
let (emails, not_found) = loop {
|
||||
@@ -40,44 +36,15 @@ impl JmapEmailGetCommand {
|
||||
};
|
||||
|
||||
for id in not_found {
|
||||
warn!("email `{id}` not found");
|
||||
warn!("email `{id}` not found, ignoring it");
|
||||
}
|
||||
|
||||
for email in emails {
|
||||
if self.raw {
|
||||
if let Some(headers) = &email.headers {
|
||||
for h in headers {
|
||||
printer.log(format!("{}: {}", h.name, h.value))?;
|
||||
}
|
||||
}
|
||||
printer.log("")?;
|
||||
}
|
||||
let table = EmailsTable {
|
||||
preset: account.table_preset,
|
||||
arrangement: account.table_arrangement,
|
||||
emails,
|
||||
};
|
||||
|
||||
if let Some(body_values) = &email.body_values {
|
||||
if let Some(text_parts) = &email.text_body {
|
||||
for part in text_parts {
|
||||
if let Some(part_id) = &part.part_id {
|
||||
if let Some(body_value) = body_values.get(part_id) {
|
||||
printer.out(&body_value.value)?;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(html_parts) = &email.html_body {
|
||||
for part in html_parts {
|
||||
if let Some(part_id) = &part.part_id {
|
||||
if let Some(body_value) = body_values.get(part_id) {
|
||||
printer.out(&body_value.value)?;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
printer.out(table)
|
||||
}
|
||||
}
|
||||
|
||||
+84
-19
@@ -1,44 +1,104 @@
|
||||
use std::collections::HashMap;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
io::{stdin, BufRead, IsTerminal},
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use clap::Parser;
|
||||
use io_jmap::{
|
||||
coroutines::email_import::{ImportJmapEmail, ImportJmapEmailResult},
|
||||
coroutines::{
|
||||
blob_upload::{UploadJmapBlob, UploadJmapBlobResult},
|
||||
email_import::{ImportJmapEmail, ImportJmapEmailResult},
|
||||
},
|
||||
types::email::EmailImport,
|
||||
};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use pimalaya_toolbox::terminal::printer::{Message, Printer};
|
||||
use url::Url;
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
|
||||
/// Import an RFC 5322 message blob into a mailbox (Email/import).
|
||||
/// Import an RFC 5322 message into a mailbox (upload + Email/import).
|
||||
///
|
||||
/// The blob must already be uploaded to the JMAP server.
|
||||
/// Reads the raw message from stdin or as trailing arguments. Use
|
||||
/// `--upload-only` to stop after the upload and print the blobId.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct ImportEmailCommand {
|
||||
/// Blob ID of the RFC 5322 message to import.
|
||||
#[arg(value_name = "BLOB-ID")]
|
||||
pub blob_id: String,
|
||||
|
||||
/// Mailbox ID(s) to place the imported email in.
|
||||
#[arg(long, value_name = "MAILBOX-ID", num_args = 0..)]
|
||||
#[arg(long, value_name = "MAILBOX-ID")]
|
||||
pub mailbox_id: Vec<String>,
|
||||
|
||||
/// Keywords to set on the imported email (e.g. `$seen`).
|
||||
#[arg(long, value_name = "KEYWORD", num_args = 0..)]
|
||||
#[arg(long, value_name = "KEYWORD")]
|
||||
pub keyword: Vec<String>,
|
||||
|
||||
/// Override the `receivedAt` time (RFC 3339).
|
||||
/// Override the `receivedAt` timestamp (RFC 3339).
|
||||
#[arg(long, value_name = "DATE")]
|
||||
pub received_at: Option<String>,
|
||||
|
||||
/// Only upload the blob and print the blobId; skip Email/import.
|
||||
#[arg(long)]
|
||||
pub upload_only: bool,
|
||||
|
||||
/// The raw RFC 5322 message (headers + body). Read from stdin if omitted.
|
||||
#[arg(trailing_var_arg = true)]
|
||||
#[arg(name = "message", value_name = "MESSAGE")]
|
||||
pub message: Vec<String>,
|
||||
}
|
||||
|
||||
impl ImportEmailCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let tls = account.backend.tls.clone().try_into()?;
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
let data: Vec<u8> = if stdin().is_terminal() || printer.is_json() {
|
||||
self.message
|
||||
.join(" ")
|
||||
.replace('\r', "")
|
||||
.replace('\n', "\r\n")
|
||||
.into_bytes()
|
||||
} else {
|
||||
let lines: Vec<String> = stdin()
|
||||
.lock()
|
||||
.lines()
|
||||
.map_while(Result::ok)
|
||||
.collect();
|
||||
lines.join("\r\n").into_bytes()
|
||||
};
|
||||
|
||||
let account_id = jmap.context.account_id.as_deref().unwrap_or("");
|
||||
let url: Url = jmap
|
||||
.context
|
||||
.session
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.upload_url
|
||||
.replace("{accountId}", account_id)
|
||||
.parse()?;
|
||||
|
||||
let mut extra_stream = jmap.connect_if_different(&url, &tls)?;
|
||||
let upload_stream = extra_stream.as_mut().unwrap_or(&mut jmap.stream);
|
||||
|
||||
let mut coroutine = UploadJmapBlob::new(jmap.context, &url, "message/rfc822", data)?;
|
||||
let mut arg = None;
|
||||
|
||||
let blob_id = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
UploadJmapBlobResult::Io(io) => arg = Some(handle(&mut *upload_stream, io)?),
|
||||
UploadJmapBlobResult::Ok { context, blob_id, .. } => {
|
||||
jmap.context = context;
|
||||
break blob_id;
|
||||
}
|
||||
UploadJmapBlobResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
};
|
||||
|
||||
if self.upload_only {
|
||||
return printer.out(Message::new(blob_id));
|
||||
}
|
||||
|
||||
let mailbox_ids: HashMap<String, bool> =
|
||||
self.mailbox_id.iter().map(|m| (m.clone(), true)).collect();
|
||||
self.mailbox_id.into_iter().map(|m| (m, true)).collect();
|
||||
|
||||
let keywords = if self.keyword.is_empty() {
|
||||
None
|
||||
@@ -47,19 +107,19 @@ impl ImportEmailCommand {
|
||||
};
|
||||
|
||||
let import = EmailImport {
|
||||
blob_id: self.blob_id.clone(),
|
||||
blob_id: blob_id.clone(),
|
||||
mailbox_ids,
|
||||
keywords,
|
||||
received_at: self.received_at,
|
||||
};
|
||||
|
||||
let mut emails = HashMap::new();
|
||||
emails.insert(self.blob_id.clone(), import);
|
||||
emails.insert(blob_id.clone(), import);
|
||||
|
||||
let mut coroutine = ImportJmapEmail::new(jmap.context, emails)?;
|
||||
let mut arg = None;
|
||||
|
||||
let not_created = loop {
|
||||
let errs = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
ImportJmapEmailResult::Io(io) => arg = Some(handle(&mut jmap.stream, io)?),
|
||||
ImportJmapEmailResult::Ok { not_created, .. } => break not_created,
|
||||
@@ -67,16 +127,21 @@ impl ImportEmailCommand {
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(err) = not_created.get(&self.blob_id) {
|
||||
let mut ctx = anyhow!("Failed to import email from blob `{}`", self.blob_id);
|
||||
if let Some(err) = errs.get(&blob_id) {
|
||||
let mut ctx = anyhow!("Import JMAP email from blob `{blob_id}` error");
|
||||
|
||||
if let Some(desc) = &err.description {
|
||||
ctx = anyhow!(desc.clone()).context(ctx);
|
||||
ctx = anyhow!("{desc}").context(ctx);
|
||||
}
|
||||
|
||||
if !err.properties.is_empty() {
|
||||
let props = err.properties.join(", ");
|
||||
ctx = anyhow!("Invalid properties {props}").context(ctx);
|
||||
}
|
||||
|
||||
bail!(ctx);
|
||||
}
|
||||
|
||||
printer.out(Message::new("Email successfully imported from blob"))
|
||||
printer.out(Message::new("Email successfully imported"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
pub mod command;
|
||||
pub mod copy;
|
||||
pub mod delete;
|
||||
pub mod export;
|
||||
pub mod get;
|
||||
pub mod import;
|
||||
pub mod parse;
|
||||
pub mod query;
|
||||
pub mod read;
|
||||
pub mod update;
|
||||
|
||||
+7
-13
@@ -2,6 +2,7 @@ use anyhow::{bail, Result};
|
||||
use clap::Parser;
|
||||
use io_jmap::coroutines::email_parse::{ParseJmapEmails, ParseJmapEmailsResult};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use log::warn;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
@@ -13,7 +14,7 @@ use crate::jmap::account::JmapAccount;
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct ParseEmailCommand {
|
||||
/// Blob ID(s) to parse as RFC 5322 messages.
|
||||
#[arg(value_name = "BLOB-ID", required = true, num_args = 1..)]
|
||||
#[arg(value_name = "ID", required = true)]
|
||||
pub blob_ids: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -27,26 +28,19 @@ impl ParseEmailCommand {
|
||||
let (parsed, not_parsable, not_found) = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
ParseJmapEmailsResult::Io(io) => arg = Some(handle(&mut jmap.stream, io)?),
|
||||
ParseJmapEmailsResult::Ok {
|
||||
context,
|
||||
parsed,
|
||||
not_parsable,
|
||||
not_found,
|
||||
..
|
||||
} => {
|
||||
jmap.context = context;
|
||||
ParseJmapEmailsResult::Ok { parsed, not_parsable, not_found, .. } => {
|
||||
break (parsed, not_parsable, not_found);
|
||||
}
|
||||
ParseJmapEmailsResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
};
|
||||
|
||||
for id in ¬_found {
|
||||
printer.log(format!("Blob `{id}` not found."))?;
|
||||
for id in not_found {
|
||||
warn!("blob `{id}` not found, ignoring it");
|
||||
}
|
||||
|
||||
for id in ¬_parsable {
|
||||
printer.log(format!("Blob `{id}` is not a valid RFC 5322 message."))?;
|
||||
for id in not_parsable {
|
||||
warn!("blob `{id}` not valid MIME message, ignoring it");
|
||||
}
|
||||
|
||||
for (_blob_id, email) in parsed {
|
||||
|
||||
+15
-15
@@ -169,21 +169,6 @@ impl JmapEmailQueryCommand {
|
||||
}
|
||||
}
|
||||
|
||||
fn format_addresses(addrs: &[EmailAddress]) -> String {
|
||||
addrs
|
||||
.iter()
|
||||
.map(|a| {
|
||||
if let Some(name) = &a.name {
|
||||
if !name.is_empty() {
|
||||
return name.clone();
|
||||
}
|
||||
}
|
||||
a.email.clone()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct EmailsTable {
|
||||
@@ -279,3 +264,18 @@ impl From<SortArg> for EmailSortProperty {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn format_addresses(addrs: &[EmailAddress]) -> String {
|
||||
addrs
|
||||
.iter()
|
||||
.map(|a| {
|
||||
if let Some(name) = &a.name {
|
||||
if !name.is_empty() {
|
||||
return name.clone();
|
||||
}
|
||||
}
|
||||
a.email.clone()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
use anyhow::{bail, Result};
|
||||
use clap::Parser;
|
||||
use io_jmap::{
|
||||
coroutines::email_get::{GetJmapEmails, GetJmapEmailsResult},
|
||||
types::email::EmailAddress,
|
||||
};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use log::warn;
|
||||
use pimalaya_toolbox::terminal::printer::{Message, Printer};
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
|
||||
/// Read the content of a JMAP email (Email/get with body).
|
||||
///
|
||||
/// Shows headers and plain text body by default.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct ReadEmailCommand {
|
||||
/// The email ID(s) to read.
|
||||
#[arg(value_name = "ID", required = true)]
|
||||
pub ids: Vec<String>,
|
||||
|
||||
/// Show HTML body instead of plain text.
|
||||
#[arg(long)]
|
||||
pub html: bool,
|
||||
}
|
||||
|
||||
impl ReadEmailCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
let mut arg = None;
|
||||
let mut coroutine = GetJmapEmails::new(
|
||||
jmap.context,
|
||||
self.ids.clone(),
|
||||
None,
|
||||
!self.html,
|
||||
self.html,
|
||||
0,
|
||||
)?;
|
||||
|
||||
let (emails, not_found) = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
GetJmapEmailsResult::Io(io) => arg = Some(handle(&mut jmap.stream, io)?),
|
||||
GetJmapEmailsResult::Ok {
|
||||
emails, not_found, ..
|
||||
} => break (emails, not_found),
|
||||
GetJmapEmailsResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
};
|
||||
|
||||
for id in not_found {
|
||||
warn!("email `{id}` not found, ignoring it");
|
||||
}
|
||||
|
||||
let mut content = String::new();
|
||||
|
||||
for email in &emails {
|
||||
if self.html {
|
||||
if let Some(body_values) = &email.body_values {
|
||||
if let Some(html_parts) = &email.html_body {
|
||||
for part in html_parts {
|
||||
if let Some(part_id) = &part.part_id {
|
||||
if let Some(body_value) = body_values.get(part_id) {
|
||||
content.push_str(&body_value.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let Some(addrs) = &email.from {
|
||||
content.push_str(&format!("From: {}\n", format_addresses(addrs)));
|
||||
}
|
||||
if let Some(addrs) = &email.to {
|
||||
content.push_str(&format!("To: {}\n", format_addresses(addrs)));
|
||||
}
|
||||
if let Some(addrs) = &email.cc {
|
||||
content.push_str(&format!("Cc: {}\n", format_addresses(addrs)));
|
||||
}
|
||||
if let Some(subject) = &email.subject {
|
||||
content.push_str(&format!("Subject: {subject}\n"));
|
||||
}
|
||||
if let Some(date) = &email.sent_at {
|
||||
content.push_str(&format!("Date: {date}\n"));
|
||||
}
|
||||
|
||||
if let Some(body_values) = &email.body_values {
|
||||
if let Some(text_parts) = &email.text_body {
|
||||
for part in text_parts {
|
||||
if let Some(part_id) = &part.part_id {
|
||||
if let Some(body_value) = body_values.get(part_id) {
|
||||
content.push('\n');
|
||||
content.push_str(&body_value.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
printer.out(Message::new(content))
|
||||
}
|
||||
}
|
||||
|
||||
fn format_addresses(addrs: &[EmailAddress]) -> String {
|
||||
addrs
|
||||
.iter()
|
||||
.map(|a| match &a.name {
|
||||
Some(name) if !name.is_empty() => format!("{name} <{}>", a.email),
|
||||
_ => a.email.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
}
|
||||
+20
-17
@@ -12,31 +12,31 @@ use crate::jmap::account::JmapAccount;
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct JmapEmailUpdateCommand {
|
||||
/// Email ID(s) to update.
|
||||
#[arg(value_name = "EMAIL_ID", required = true, num_args = 1..)]
|
||||
#[arg(value_name = "ID", required = true)]
|
||||
pub ids: Vec<String>,
|
||||
|
||||
/// Add keyword(s) to the email(s).
|
||||
#[arg(long, value_name = "KEYWORD", num_args = 0..)]
|
||||
#[arg(long, value_name = "KEYWORD", required = false)]
|
||||
pub add_keyword: Vec<String>,
|
||||
|
||||
/// Remove keyword(s) from the email(s).
|
||||
#[arg(long, value_name = "KEYWORD", num_args = 0..)]
|
||||
#[arg(long, value_name = "KEYWORD", required = false)]
|
||||
pub remove_keyword: Vec<String>,
|
||||
|
||||
/// Replace all keywords atomically (no fetch required).
|
||||
#[arg(long, value_name = "KEYWORD", num_args = 0..)]
|
||||
/// Replace all keywords atomically.
|
||||
#[arg(long, value_name = "KEYWORD")]
|
||||
pub keywords: Option<Vec<String>>,
|
||||
|
||||
/// Add email(s) to a mailbox.
|
||||
#[arg(long, value_name = "MAILBOX-ID", num_args = 1..)]
|
||||
#[arg(long, value_name = "MAILBOX-ID", required = false)]
|
||||
pub add_mailbox: Vec<String>,
|
||||
|
||||
/// Remove email(s) from a mailbox.
|
||||
#[arg(long, value_name = "MAILBOX-ID", num_args = 1..)]
|
||||
#[arg(long, value_name = "MAILBOX-ID", required = false)]
|
||||
pub remove_mailbox: Vec<String>,
|
||||
|
||||
/// Replace all mailbox memberships atomically.
|
||||
#[arg(long, value_name = "MAILBOX-ID", num_args = 0..)]
|
||||
#[arg(long, value_name = "MAILBOX-ID")]
|
||||
pub mailboxes: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
@@ -84,18 +84,21 @@ impl JmapEmailUpdateCommand {
|
||||
}
|
||||
};
|
||||
|
||||
for (id, err) in ¬_updated {
|
||||
let mut ctx = anyhow!("Failed to update email `{id}`");
|
||||
if !not_updated.is_empty() {
|
||||
let mut ctx = anyhow!("Update JMAP email(s) error");
|
||||
|
||||
if let Some(desc) = &err.description {
|
||||
ctx = anyhow!(desc.clone()).context(ctx);
|
||||
for (id, err) in not_updated {
|
||||
if let Some(desc) = &err.description {
|
||||
ctx = anyhow!("{id}: {desc}").context(ctx);
|
||||
}
|
||||
|
||||
if !err.properties.is_empty() {
|
||||
let props = err.properties.join(", ");
|
||||
ctx = anyhow!("{id}: Invalid properties {props}").context(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
if !err.properties.is_empty() {
|
||||
ctx = anyhow!("Invalid properties: {}", err.properties.join(", ")).context(ctx);
|
||||
}
|
||||
|
||||
bail!(ctx);
|
||||
bail!(ctx)
|
||||
}
|
||||
|
||||
printer.out(Message::new("Email(s) successfully updated"))
|
||||
|
||||
@@ -5,7 +5,7 @@ use pimalaya_toolbox::terminal::printer::Printer;
|
||||
use crate::jmap::{
|
||||
account::JmapAccount,
|
||||
identity::{
|
||||
create::CreateIdentityCommand, delete::DeleteIdentityCommand, get::GetIdentityCommand,
|
||||
create::JmapIdentityCreateCommand, delete::DeleteIdentityCommand, get::GetIdentityCommand,
|
||||
update::UpdateIdentityCommand,
|
||||
},
|
||||
};
|
||||
@@ -18,7 +18,7 @@ pub enum IdentityCommand {
|
||||
Get(GetIdentityCommand),
|
||||
/// Create a new identity (Identity/set).
|
||||
#[command(aliases = ["add", "new"])]
|
||||
Create(CreateIdentityCommand),
|
||||
Create(JmapIdentityCreateCommand),
|
||||
/// Update an existing identity (Identity/set).
|
||||
#[command(alias = "edit")]
|
||||
Update(UpdateIdentityCommand),
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::jmap::account::JmapAccount;
|
||||
|
||||
/// Create a JMAP sender identity (Identity/set).
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct CreateIdentityCommand {
|
||||
pub struct JmapIdentityCreateCommand {
|
||||
/// Display name for the sender.
|
||||
pub name: String,
|
||||
|
||||
@@ -27,7 +27,7 @@ pub struct CreateIdentityCommand {
|
||||
pub html_signature: Option<String>,
|
||||
}
|
||||
|
||||
impl CreateIdentityCommand {
|
||||
impl JmapIdentityCreateCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
@@ -40,13 +40,15 @@ impl CreateIdentityCommand {
|
||||
html_signature: self.html_signature,
|
||||
};
|
||||
|
||||
let create_id = "new";
|
||||
|
||||
let mut args = IdentitySetArgs::default();
|
||||
args.create(self.email.clone(), identity);
|
||||
args.create(create_id, identity);
|
||||
|
||||
let mut coroutine = SetJmapIdentities::new(jmap.context, args)?;
|
||||
let mut arg = None;
|
||||
|
||||
let not_created = loop {
|
||||
let errs = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
SetJmapIdentitiesResult::Io(io) => arg = Some(handle(&mut jmap.stream, io)?),
|
||||
SetJmapIdentitiesResult::Ok { not_created, .. } => break not_created,
|
||||
@@ -54,11 +56,16 @@ impl CreateIdentityCommand {
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(err) = not_created.get(&self.email) {
|
||||
let mut ctx = anyhow!("Failed to create identity `{}`", self.email);
|
||||
if let Some(err) = errs.get(create_id) {
|
||||
let mut ctx = anyhow!("Create identity for `{}` error", &self.email);
|
||||
|
||||
if let Some(desc) = &err.description {
|
||||
ctx = anyhow!(desc.clone()).context(ctx);
|
||||
ctx = anyhow!("{desc}").context(ctx);
|
||||
}
|
||||
|
||||
if !err.properties.is_empty() {
|
||||
let props = err.properties.join(", ");
|
||||
ctx = anyhow!("Invalid properties {props}").context(ctx);
|
||||
}
|
||||
|
||||
bail!(ctx);
|
||||
|
||||
@@ -37,14 +37,21 @@ impl DeleteIdentityCommand {
|
||||
}
|
||||
};
|
||||
|
||||
for (id, err) in ¬_destroyed {
|
||||
let mut ctx = anyhow!("Failed to delete identity `{id}`");
|
||||
if !not_destroyed.is_empty() {
|
||||
let mut ctx = anyhow!("Destroy JMAP identities error");
|
||||
|
||||
if let Some(desc) = &err.description {
|
||||
ctx = anyhow!(desc.clone()).context(ctx);
|
||||
for (id, err) in not_destroyed {
|
||||
if let Some(desc) = &err.description {
|
||||
ctx = anyhow!("{id}: {desc}").context(ctx);
|
||||
}
|
||||
|
||||
if !err.properties.is_empty() {
|
||||
let props = err.properties.join(", ");
|
||||
ctx = anyhow!("{id}: Invalid properties {props}").context(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
bail!(ctx);
|
||||
bail!(ctx)
|
||||
}
|
||||
|
||||
printer.out(Message::new("Identity successfully deleted"))
|
||||
|
||||
@@ -44,7 +44,7 @@ impl GetIdentityCommand {
|
||||
}
|
||||
};
|
||||
|
||||
for id in ¬_found {
|
||||
for id in not_found {
|
||||
warn!("identity `{id}` not found");
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ impl UpdateIdentityCommand {
|
||||
let mut coroutine = SetJmapIdentities::new(jmap.context, args)?;
|
||||
let mut arg = None;
|
||||
|
||||
let not_updated = loop {
|
||||
let errs = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
SetJmapIdentitiesResult::Io(io) => arg = Some(handle(&mut jmap.stream, io)?),
|
||||
SetJmapIdentitiesResult::Ok { not_updated, .. } => break not_updated,
|
||||
@@ -54,11 +54,16 @@ impl UpdateIdentityCommand {
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(err) = not_updated.get(&self.id) {
|
||||
let mut ctx = anyhow!("Failed to update identity `{}`", self.id);
|
||||
if let Some(err) = errs.get(&self.id) {
|
||||
let mut ctx = anyhow!("Update identity `{}` error", &self.id);
|
||||
|
||||
if let Some(desc) = &err.description {
|
||||
ctx = anyhow!(desc.clone()).context(ctx);
|
||||
ctx = anyhow!("{desc}").context(ctx);
|
||||
}
|
||||
|
||||
if !err.properties.is_empty() {
|
||||
let props = err.properties.join(", ");
|
||||
ctx = anyhow!("Invalid properties {props}").context(ctx);
|
||||
}
|
||||
|
||||
bail!(ctx);
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use clap::Parser;
|
||||
use io_jmap::coroutines::email_set::{EmailSetArgs, SetJmapEmails, SetJmapEmailsResult};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
|
||||
/// Add keywords to JMAP emails.
|
||||
///
|
||||
/// Standard JMAP keywords: `$seen`, `$flagged`, `$answered`, `$draft`.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct AddKeywordCommand {
|
||||
/// Email ID(s) to add the keyword(s) to.
|
||||
#[arg(value_name = "EMAIL_ID", num_args = 1..)]
|
||||
pub ids: Vec<String>,
|
||||
|
||||
/// The keyword(s) to add.
|
||||
#[arg(long, short, num_args = 1..)]
|
||||
pub keyword: Vec<String>,
|
||||
}
|
||||
|
||||
impl AddKeywordCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
let mut args = EmailSetArgs::default();
|
||||
|
||||
for id in &self.ids {
|
||||
for kw in &self.keyword {
|
||||
args.set_keyword(id.clone(), kw.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let mut coroutine = SetJmapEmails::new(jmap.context, args)?;
|
||||
let mut arg = None;
|
||||
|
||||
let not_updated = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
SetJmapEmailsResult::Io(io) => arg = Some(handle(&mut jmap.stream, io)?),
|
||||
SetJmapEmailsResult::Ok { not_updated, .. } => break not_updated,
|
||||
SetJmapEmailsResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
};
|
||||
|
||||
for (id, err) in ¬_updated {
|
||||
let mut ctx = anyhow!("failed to add keyword to email `{id}`");
|
||||
if let Some(desc) = &err.description {
|
||||
ctx = anyhow!(desc.clone()).context(ctx);
|
||||
}
|
||||
if !err.properties.is_empty() {
|
||||
ctx = anyhow!("invalid properties: {}", err.properties.join(", ")).context(ctx);
|
||||
}
|
||||
bail!(ctx);
|
||||
}
|
||||
|
||||
printer.log(format!(
|
||||
"Keyword(s) `{}` added to {} email(s).",
|
||||
self.keyword.join(", "),
|
||||
self.ids.len()
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
|
||||
use crate::jmap::{
|
||||
account::JmapAccount,
|
||||
keyword::{add::AddKeywordCommand, remove::RemoveKeywordCommand, set::SetKeywordsCommand},
|
||||
};
|
||||
|
||||
/// Manage JMAP email keywords (flags).
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum KeywordCommand {
|
||||
Add(AddKeywordCommand),
|
||||
Remove(RemoveKeywordCommand),
|
||||
Set(SetKeywordsCommand),
|
||||
}
|
||||
|
||||
impl KeywordCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
match self {
|
||||
Self::Add(cmd) => cmd.execute(printer, account),
|
||||
Self::Remove(cmd) => cmd.execute(printer, account),
|
||||
Self::Set(cmd) => cmd.execute(printer, account),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
pub mod add;
|
||||
pub mod command;
|
||||
pub mod remove;
|
||||
pub mod set;
|
||||
@@ -1,61 +0,0 @@
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use clap::Parser;
|
||||
use io_jmap::coroutines::email_set::{EmailSetArgs, SetJmapEmails, SetJmapEmailsResult};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
|
||||
/// Remove keywords from JMAP emails.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct RemoveKeywordCommand {
|
||||
/// Email ID(s) to remove the keyword(s) from.
|
||||
#[arg(value_name = "EMAIL_ID", num_args = 1..)]
|
||||
pub ids: Vec<String>,
|
||||
|
||||
/// The keyword(s) to remove.
|
||||
#[arg(long, short, num_args = 1..)]
|
||||
pub keyword: Vec<String>,
|
||||
}
|
||||
|
||||
impl RemoveKeywordCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
let mut args = EmailSetArgs::default();
|
||||
|
||||
for id in &self.ids {
|
||||
for kw in &self.keyword {
|
||||
args.unset_keyword(id.clone(), kw.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let mut coroutine = SetJmapEmails::new(jmap.context, args)?;
|
||||
let mut arg = None;
|
||||
|
||||
let not_updated = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
SetJmapEmailsResult::Io(io) => arg = Some(handle(&mut jmap.stream, io)?),
|
||||
SetJmapEmailsResult::Ok { not_updated, .. } => break not_updated,
|
||||
SetJmapEmailsResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
};
|
||||
|
||||
for (id, err) in ¬_updated {
|
||||
let mut ctx = anyhow!("failed to remove keyword from email `{id}`");
|
||||
if let Some(desc) = &err.description {
|
||||
ctx = anyhow!(desc.clone()).context(ctx);
|
||||
}
|
||||
if !err.properties.is_empty() {
|
||||
ctx = anyhow!("invalid properties: {}", err.properties.join(", ")).context(ctx);
|
||||
}
|
||||
bail!(ctx);
|
||||
}
|
||||
|
||||
printer.log(format!(
|
||||
"Keyword(s) `{}` removed from {} email(s).",
|
||||
self.keyword.join(", "),
|
||||
self.ids.len()
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use clap::Parser;
|
||||
use io_jmap::coroutines::email_set::{EmailSetArgs, SetJmapEmails, SetJmapEmailsResult};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
|
||||
/// Replace all keywords on JMAP emails.
|
||||
///
|
||||
/// Replaces the entire set of keywords atomically — no need to know
|
||||
/// the current keywords first.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct SetKeywordsCommand {
|
||||
/// Email ID(s) to update.
|
||||
#[arg(value_name = "EMAIL_ID", num_args = 1..)]
|
||||
pub ids: Vec<String>,
|
||||
|
||||
/// Keywords to set (replaces all existing keywords).
|
||||
#[arg(long, short, num_args = 1..)]
|
||||
pub keyword: Vec<String>,
|
||||
}
|
||||
|
||||
impl SetKeywordsCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
let keywords: HashMap<String, bool> =
|
||||
self.keyword.iter().map(|kw| (kw.clone(), true)).collect();
|
||||
|
||||
let mut args = EmailSetArgs::default();
|
||||
for id in &self.ids {
|
||||
args.replace_keywords(id.clone(), keywords.clone());
|
||||
}
|
||||
|
||||
let mut coroutine = SetJmapEmails::new(jmap.context, args)?;
|
||||
let mut arg = None;
|
||||
|
||||
let not_updated = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
SetJmapEmailsResult::Io(io) => arg = Some(handle(&mut jmap.stream, io)?),
|
||||
SetJmapEmailsResult::Ok { not_updated, .. } => break not_updated,
|
||||
SetJmapEmailsResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
};
|
||||
|
||||
for (id, err) in ¬_updated {
|
||||
let mut ctx = anyhow!("failed to set keywords on email `{id}`");
|
||||
if let Some(desc) = &err.description {
|
||||
ctx = anyhow!(desc.clone()).context(ctx);
|
||||
}
|
||||
if !err.properties.is_empty() {
|
||||
ctx = anyhow!("invalid properties: {}", err.properties.join(", ")).context(ctx);
|
||||
}
|
||||
bail!(ctx);
|
||||
}
|
||||
|
||||
printer.log(format!("Keywords set on {} email(s).", self.ids.len()))
|
||||
}
|
||||
}
|
||||
@@ -56,18 +56,21 @@ impl JmapMailboxCreateCommand {
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(err) = not_created.get(&self.name) {
|
||||
if !not_created.is_empty() {
|
||||
let mut ctx = anyhow!("Create JMAP mailbox `{}` error", self.name);
|
||||
|
||||
if let Some(desc) = &err.description {
|
||||
ctx = anyhow!(desc.clone()).context(ctx);
|
||||
for (_, err) in not_created {
|
||||
if let Some(desc) = &err.description {
|
||||
ctx = anyhow!(desc.clone()).context(ctx);
|
||||
}
|
||||
|
||||
if !err.properties.is_empty() {
|
||||
let props = err.properties.join(", ");
|
||||
ctx = anyhow!("Invalid properties {props}").context(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
if !err.properties.is_empty() {
|
||||
ctx = anyhow!("Invalid properties: {}", err.properties.join(", ")).context(ctx);
|
||||
}
|
||||
|
||||
bail!(ctx);
|
||||
bail!(ctx)
|
||||
}
|
||||
|
||||
printer.out(Message::new("Mailbox successfully created"))
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::jmap::account::JmapAccount;
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct JmapMailboxDestroyCommand {
|
||||
/// The ID of the mailbox to delete.
|
||||
#[arg(value_name = "ID", required = true, num_args = 1..)]
|
||||
#[arg(value_name = "ID", required = true)]
|
||||
pub ids: Vec<String>,
|
||||
|
||||
/// Destroy all emails in the mailbox when deleting.
|
||||
@@ -37,20 +37,21 @@ impl JmapMailboxDestroyCommand {
|
||||
}
|
||||
};
|
||||
|
||||
for ref id in self.ids {
|
||||
if let Some(err) = not_destroyed.get(id) {
|
||||
let mut ctx = anyhow!("Update JMAP mailbox `{id}` error");
|
||||
if !not_destroyed.is_empty() {
|
||||
let mut ctx = anyhow!("Destroy JMAP mailbox(es) error");
|
||||
|
||||
for (id, err) in not_destroyed {
|
||||
if let Some(desc) = &err.description {
|
||||
ctx = anyhow!(desc.clone()).context(ctx);
|
||||
ctx = anyhow!("{id}: {desc}").context(ctx);
|
||||
}
|
||||
|
||||
if !err.properties.is_empty() {
|
||||
ctx = anyhow!("Invalid properties: {}", err.properties.join(", ")).context(ctx);
|
||||
let props = err.properties.join(", ");
|
||||
ctx = anyhow!("{id}: Invalid properties {props}").context(ctx);
|
||||
}
|
||||
|
||||
bail!(ctx);
|
||||
}
|
||||
|
||||
bail!(ctx)
|
||||
}
|
||||
|
||||
printer.out(Message::new("Mailbox successfully deleted"))
|
||||
|
||||
@@ -35,7 +35,7 @@ impl JmapMailboxGetCommand {
|
||||
};
|
||||
|
||||
for id in not_found {
|
||||
warn!("mailbox `{id}` not found");
|
||||
warn!("mailbox `{id}` not found, ignoring it");
|
||||
}
|
||||
|
||||
let table = MailboxesTable {
|
||||
|
||||
@@ -72,7 +72,7 @@ impl JmapMailboxUpdateCommand {
|
||||
let mut arg = None;
|
||||
let mut coroutine = SetJmapMailboxes::new(jmap.context, args)?;
|
||||
|
||||
let not_updated = loop {
|
||||
let errs = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
SetJmapMailboxesResult::Io(io) => arg = Some(handle(&mut jmap.stream, io)?),
|
||||
SetJmapMailboxesResult::Ok { not_updated, .. } => break not_updated,
|
||||
@@ -80,15 +80,16 @@ impl JmapMailboxUpdateCommand {
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(err) = not_updated.get(&self.id) {
|
||||
if let Some(err) = errs.get(&self.id) {
|
||||
let mut ctx = anyhow!("Update JMAP mailbox `{}` error", self.id);
|
||||
|
||||
if let Some(desc) = &err.description {
|
||||
ctx = anyhow!(desc.clone()).context(ctx);
|
||||
ctx = anyhow!("{desc}").context(ctx);
|
||||
}
|
||||
|
||||
if !err.properties.is_empty() {
|
||||
ctx = anyhow!("Invalid properties: {}", err.properties.join(", ")).context(ctx);
|
||||
let props = err.properties.join(", ");
|
||||
ctx = anyhow!("Invalid properties {props}").context(ctx);
|
||||
}
|
||||
|
||||
bail!(ctx);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use clap::Parser;
|
||||
use io_jmap::coroutines::email_submission_cancel::{
|
||||
CancelJmapEmailSubmissions, CancelJmapEmailSubmissionsResult,
|
||||
};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use pimalaya_toolbox::terminal::printer::{Message, Printer};
|
||||
|
||||
@@ -12,7 +15,7 @@ use crate::jmap::account::JmapAccount;
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct CancelSubmissionCommand {
|
||||
/// Submission ID(s) to cancel.
|
||||
#[arg(value_name = "SUBMISSION_ID", num_args = 1..)]
|
||||
#[arg(value_name = "ID", required = true)]
|
||||
pub ids: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -20,75 +23,35 @@ impl CancelSubmissionCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
// EmailSubmission/set update: set undoStatus to "canceled"
|
||||
let update: std::collections::HashMap<String, serde_json::Value> = self
|
||||
.ids
|
||||
.iter()
|
||||
.map(|id| {
|
||||
(
|
||||
id.clone(),
|
||||
serde_json::json!({ "undoStatus": "canceled" }),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let args = serde_json::json!({
|
||||
"update": update
|
||||
});
|
||||
|
||||
// Use the raw query approach via SubmitJmapEmail isn't suitable here;
|
||||
// we need a direct EmailSubmission/set update. Use the query command
|
||||
// pattern with a raw request instead.
|
||||
//
|
||||
// For now, build the request directly via the send coroutine.
|
||||
use io_jmap::{
|
||||
coroutines::send::{JmapBatch, SendJmapRequest, SendJmapRequestResult},
|
||||
types::session::capabilities,
|
||||
};
|
||||
|
||||
let account_id = jmap.context.account_id.clone().unwrap_or_default();
|
||||
let api_url = jmap
|
||||
.context
|
||||
.api_url()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "http://localhost".parse().unwrap());
|
||||
|
||||
let mut json_args = args.clone();
|
||||
json_args["accountId"] = serde_json::json!(account_id);
|
||||
|
||||
let mut batch = JmapBatch::new();
|
||||
batch.add("EmailSubmission/set", json_args);
|
||||
let request = batch.into_request(vec![
|
||||
capabilities::CORE.into(),
|
||||
capabilities::MAIL.into(),
|
||||
capabilities::SUBMISSION.into(),
|
||||
]);
|
||||
|
||||
let mut send = SendJmapRequest::new(jmap.context, &api_url, request)
|
||||
.map_err(|e| anyhow!("{e}"))?;
|
||||
let mut coroutine =
|
||||
CancelJmapEmailSubmissions::new(jmap.context, self.ids.clone())
|
||||
.map_err(|e| anyhow!("{e}"))?;
|
||||
let mut arg = None;
|
||||
|
||||
loop {
|
||||
match send.resume(arg.take()) {
|
||||
SendJmapRequestResult::Io(io) => arg = Some(handle(&mut jmap.stream, io)?),
|
||||
SendJmapRequestResult::Ok { context, response, .. } => {
|
||||
jmap.context = context;
|
||||
if let Some((name, args, _)) =
|
||||
response.method_responses.into_iter().next()
|
||||
{
|
||||
if name == "error" {
|
||||
bail!("EmailSubmission/set error: {args}");
|
||||
}
|
||||
}
|
||||
break;
|
||||
let not_updated = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
CancelJmapEmailSubmissionsResult::Io(io) => {
|
||||
arg = Some(handle(&mut jmap.stream, io)?)
|
||||
}
|
||||
SendJmapRequestResult::Err { err, .. } => bail!(err),
|
||||
CancelJmapEmailSubmissionsResult::Ok { not_updated, .. } => {
|
||||
break not_updated
|
||||
}
|
||||
CancelJmapEmailSubmissionsResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
};
|
||||
|
||||
for (id, err) in ¬_updated {
|
||||
let mut ctx = anyhow!("Cancel submission `{id}` error");
|
||||
if let Some(desc) = &err.description {
|
||||
ctx = anyhow!("{desc}").context(ctx);
|
||||
}
|
||||
if !err.properties.is_empty() {
|
||||
let props = err.properties.join(", ");
|
||||
ctx = anyhow!("Invalid properties {props}").context(ctx);
|
||||
}
|
||||
bail!(ctx);
|
||||
}
|
||||
|
||||
printer.out(Message::new(format!(
|
||||
"{} submission(s) canceled.",
|
||||
self.ids.len()
|
||||
)))
|
||||
printer.out(Message::new(format!("{} submission(s) canceled.", self.ids.len())))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ use crate::jmap::{
|
||||
|
||||
/// Manage JMAP email submissions.
|
||||
#[derive(Debug, Subcommand)]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
pub enum SubmissionCommand {
|
||||
/// Fetch submissions by ID (EmailSubmission/get).
|
||||
Get(GetSubmissionCommand),
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use anyhow::{bail, Result};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use clap::Parser;
|
||||
use io_jmap::{
|
||||
coroutines::email_submission_set::{SubmitJmapEmail, SubmitJmapEmailResult},
|
||||
@@ -7,7 +9,7 @@ use io_jmap::{
|
||||
use io_stream::runtimes::std::handle;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
use crate::jmap::{account::JmapAccount, submission::query::SubmissionsTable};
|
||||
|
||||
/// Submit a JMAP email for sending (EmailSubmission/set).
|
||||
///
|
||||
@@ -40,7 +42,10 @@ impl CreateSubmissionCommand {
|
||||
let rcpt_to = self
|
||||
.rcpt_to
|
||||
.into_iter()
|
||||
.map(|addr| EmailAddressWithParameters { email: addr, parameters: None })
|
||||
.map(|addr| EmailAddressWithParameters {
|
||||
email: addr,
|
||||
parameters: None,
|
||||
})
|
||||
.collect();
|
||||
Some(Envelope {
|
||||
mail_from: EmailAddressWithParameters {
|
||||
@@ -59,33 +64,44 @@ impl CreateSubmissionCommand {
|
||||
envelope,
|
||||
};
|
||||
|
||||
let mut submissions = std::collections::HashMap::new();
|
||||
submissions.insert("send-1".to_string(), submission);
|
||||
let mut submissions = HashMap::new();
|
||||
submissions.insert(self.email_id.clone(), submission);
|
||||
|
||||
let mut coroutine = SubmitJmapEmail::new(jmap.context, submissions)?;
|
||||
let mut arg = None;
|
||||
|
||||
loop {
|
||||
let (created, errs) = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
SubmitJmapEmailResult::Io(io) => arg = Some(handle(&mut jmap.stream, io)?),
|
||||
SubmitJmapEmailResult::Ok { context, not_created, .. } => {
|
||||
jmap.context = context;
|
||||
|
||||
if let Some(err) = not_created.get("send-1") {
|
||||
bail!(
|
||||
"failed to send email `{}`: {} — {}",
|
||||
self.email_id,
|
||||
err.error_type,
|
||||
err.description.as_deref().unwrap_or("no description")
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
SubmitJmapEmailResult::Ok {
|
||||
created,
|
||||
not_created,
|
||||
..
|
||||
} => break (created, not_created),
|
||||
SubmitJmapEmailResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(err) = errs.get(&self.email_id) {
|
||||
let mut ctx = anyhow!("Send email `{}` error", &self.email_id);
|
||||
|
||||
if let Some(desc) = &err.description {
|
||||
ctx = anyhow!("{desc}").context(ctx);
|
||||
}
|
||||
|
||||
if !err.properties.is_empty() {
|
||||
let props = err.properties.join(", ");
|
||||
ctx = anyhow!("Invalid properties {props}").context(ctx);
|
||||
}
|
||||
|
||||
bail!(ctx);
|
||||
}
|
||||
|
||||
printer.log(format!("Email `{}` successfully sent.", self.email_id))
|
||||
let table = SubmissionsTable {
|
||||
preset: account.table_preset,
|
||||
submissions: created.into_values().collect(),
|
||||
};
|
||||
|
||||
printer.out(table)
|
||||
}
|
||||
}
|
||||
|
||||
+14
-15
@@ -4,15 +4,16 @@ use io_jmap::coroutines::email_submission_get::{
|
||||
GetJmapEmailSubmissions, GetJmapEmailSubmissionsResult,
|
||||
};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use log::warn;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
use crate::jmap::{account::JmapAccount, submission::query::SubmissionsTable};
|
||||
|
||||
/// Get JMAP email submissions by ID (EmailSubmission/get).
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct GetSubmissionCommand {
|
||||
/// Submission ID(s) to retrieve.
|
||||
#[arg(value_name = "SUBMISSION_ID", num_args = 1..)]
|
||||
#[arg(value_name = "ID", required = true)]
|
||||
pub ids: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -20,32 +21,30 @@ impl GetSubmissionCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
let mut coroutine =
|
||||
GetJmapEmailSubmissions::new(jmap.context, Some(self.ids.clone()))?;
|
||||
let mut coroutine = GetJmapEmailSubmissions::new(jmap.context, Some(self.ids.clone()))?;
|
||||
let mut arg = None;
|
||||
|
||||
let (submissions, not_found) = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
GetJmapEmailSubmissionsResult::Io(io) => {
|
||||
arg = Some(handle(&mut jmap.stream, io)?)
|
||||
}
|
||||
GetJmapEmailSubmissionsResult::Io(io) => arg = Some(handle(&mut jmap.stream, io)?),
|
||||
GetJmapEmailSubmissionsResult::Ok {
|
||||
context,
|
||||
submissions,
|
||||
not_found,
|
||||
..
|
||||
} => {
|
||||
jmap.context = context;
|
||||
break (submissions, not_found);
|
||||
}
|
||||
} => break (submissions, not_found),
|
||||
GetJmapEmailSubmissionsResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
};
|
||||
|
||||
for id in ¬_found {
|
||||
printer.log(format!("Submission `{id}` not found."))?;
|
||||
for id in not_found {
|
||||
warn!("submission `{id}` not found, ignoring it");
|
||||
}
|
||||
|
||||
printer.out(serde_json::to_value(&submissions)?)
|
||||
let table = SubmissionsTable {
|
||||
preset: account.table_preset,
|
||||
submissions,
|
||||
};
|
||||
|
||||
printer.out(table)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use std::fmt;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use clap::Parser;
|
||||
use clap::{Parser, ValueEnum};
|
||||
use comfy_table::{Cell, Row, Table};
|
||||
use io_jmap::{
|
||||
coroutines::email_submission_query::{
|
||||
QueryJmapEmailSubmissions, QueryJmapEmailSubmissionsResult,
|
||||
},
|
||||
types::email_submission::EmailSubmission,
|
||||
types::email_submission::{EmailSubmission, EmailSubmissionFilter, UndoStatus},
|
||||
};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
@@ -15,12 +15,30 @@ use serde::Serialize;
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
|
||||
/// CLI proxy for [`UndoStatus`].
|
||||
#[derive(Clone, Debug, ValueEnum)]
|
||||
pub enum UndoStatusArg {
|
||||
Pending,
|
||||
Final,
|
||||
Canceled,
|
||||
}
|
||||
|
||||
impl From<UndoStatusArg> for UndoStatus {
|
||||
fn from(arg: UndoStatusArg) -> Self {
|
||||
match arg {
|
||||
UndoStatusArg::Pending => UndoStatus::Pending,
|
||||
UndoStatusArg::Final => UndoStatus::Final,
|
||||
UndoStatusArg::Canceled => UndoStatus::Canceled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Query JMAP email submissions (EmailSubmission/query + EmailSubmission/get).
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct QuerySubmissionCommand {
|
||||
/// Filter by undo status (`pending`, `final`, `canceled`).
|
||||
#[arg(long, value_name = "STATUS")]
|
||||
pub undo_status: Option<String>,
|
||||
pub undo_status: Option<UndoStatusArg>,
|
||||
|
||||
/// Filter by sent-before date (RFC 3339).
|
||||
#[arg(long, value_name = "DATE")]
|
||||
@@ -44,17 +62,23 @@ impl QuerySubmissionCommand {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
let filter = {
|
||||
use io_jmap::types::email_submission::EmailSubmissionFilter;
|
||||
let f = EmailSubmissionFilter {
|
||||
undo_status: self.undo_status,
|
||||
undo_status: self.undo_status.map(Into::into),
|
||||
before: self.before,
|
||||
after: self.after,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let has_one = f.undo_status.is_some() || f.before.is_some() || f.after.is_some();
|
||||
if has_one { Some(f) } else { None }
|
||||
|
||||
if has_one {
|
||||
Some(f)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let mut arg = None;
|
||||
let mut coroutine = QueryJmapEmailSubmissions::new(
|
||||
jmap.context,
|
||||
filter,
|
||||
@@ -62,17 +86,13 @@ impl QuerySubmissionCommand {
|
||||
Some(self.page.saturating_sub(1) * self.page_size),
|
||||
Some(self.page_size),
|
||||
)?;
|
||||
let mut arg = None;
|
||||
|
||||
let submissions = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
QueryJmapEmailSubmissionsResult::Io(io) => {
|
||||
arg = Some(handle(&mut jmap.stream, io)?)
|
||||
}
|
||||
QueryJmapEmailSubmissionsResult::Ok { context, submissions, .. } => {
|
||||
jmap.context = context;
|
||||
break submissions;
|
||||
}
|
||||
QueryJmapEmailSubmissionsResult::Ok { submissions, .. } => break submissions,
|
||||
QueryJmapEmailSubmissionsResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
};
|
||||
@@ -110,9 +130,9 @@ impl fmt::Display for SubmissionsTable {
|
||||
.add_rows(self.submissions.iter().map(|s| {
|
||||
Row::from([
|
||||
Cell::new(s.id.as_deref().unwrap_or("")),
|
||||
Cell::new(&s.email_id),
|
||||
Cell::new(&s.identity_id),
|
||||
Cell::new(s.undo_status.as_deref().unwrap_or("")),
|
||||
Cell::new(s.email_id.as_deref().unwrap_or("")),
|
||||
Cell::new(s.identity_id.as_deref().unwrap_or("")),
|
||||
Cell::new(s.undo_status.as_ref().map(|s| s.to_string()).unwrap_or_default()),
|
||||
Cell::new(s.send_at.as_deref().unwrap_or("")),
|
||||
])
|
||||
}));
|
||||
|
||||
+41
-11
@@ -1,8 +1,16 @@
|
||||
use std::fmt;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use clap::Parser;
|
||||
use io_jmap::coroutines::thread_get::{GetJmapThreads, GetJmapThreadsResult};
|
||||
use comfy_table::{Cell, Row, Table};
|
||||
use io_jmap::{
|
||||
coroutines::thread_get::{GetJmapThreads, GetJmapThreadsResult},
|
||||
types::thread::Thread,
|
||||
};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use log::warn;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
|
||||
@@ -33,17 +41,39 @@ impl GetThreadCommand {
|
||||
}
|
||||
};
|
||||
|
||||
for id in ¬_found {
|
||||
printer.log(format!("Thread `{id}` not found."))?;
|
||||
for id in not_found {
|
||||
warn!("thread `{id}` not found, ignoring it");
|
||||
}
|
||||
|
||||
for thread in threads {
|
||||
printer.out(serde_json::json!({
|
||||
"id": thread.id,
|
||||
"emailIds": thread.email_ids,
|
||||
}))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
printer.out(ThreadsTable {
|
||||
preset: account.table_preset,
|
||||
threads,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct ThreadsTable {
|
||||
#[serde(skip)]
|
||||
pub preset: String,
|
||||
pub threads: Vec<Thread>,
|
||||
}
|
||||
|
||||
impl fmt::Display for ThreadsTable {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut table = Table::new();
|
||||
|
||||
table
|
||||
.load_preset(&self.preset)
|
||||
.set_header(Row::from([Cell::new("ID"), Cell::new("EMAIL IDS")]))
|
||||
.add_rows(
|
||||
self.threads
|
||||
.iter()
|
||||
.map(|t| Row::from([Cell::new(&t.id), Cell::new(t.email_ids.join(", "))])),
|
||||
);
|
||||
|
||||
writeln!(f)?;
|
||||
writeln!(f, "{table}")
|
||||
}
|
||||
}
|
||||
|
||||
+83
-14
@@ -1,10 +1,15 @@
|
||||
use std::fmt;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use clap::Parser;
|
||||
use io_jmap::coroutines::vacation_response_get::{
|
||||
GetJmapVacationResponse, GetJmapVacationResponseResult,
|
||||
use comfy_table::{Cell, Row, Table};
|
||||
use io_jmap::{
|
||||
coroutines::vacation_response_get::{GetJmapVacationResponse, GetJmapVacationResponseResult},
|
||||
types::{session::capabilities::VACATION_RESPONSE, vacation_response::VacationResponse},
|
||||
};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
use pimalaya_toolbox::terminal::printer::{Message, Printer};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
|
||||
@@ -16,25 +21,89 @@ impl GetVacationCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
// Skip the request if the server does not advertise the
|
||||
// vacation-response capability.
|
||||
let has_vacation = jmap
|
||||
.context
|
||||
.session
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.capabilities
|
||||
.contains_key(VACATION_RESPONSE);
|
||||
|
||||
if !has_vacation {
|
||||
bail!("Vacation response is not supported by the server");
|
||||
}
|
||||
|
||||
let mut coroutine = GetJmapVacationResponse::new(jmap.context)?;
|
||||
let mut arg = None;
|
||||
|
||||
let vacation = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
GetJmapVacationResponseResult::Io(io) => {
|
||||
arg = Some(handle(&mut jmap.stream, io)?)
|
||||
}
|
||||
GetJmapVacationResponseResult::Ok { context, vacation_response, .. } => {
|
||||
jmap.context = context;
|
||||
break vacation_response;
|
||||
}
|
||||
GetJmapVacationResponseResult::Io(io) => arg = Some(handle(&mut jmap.stream, io)?),
|
||||
GetJmapVacationResponseResult::Ok {
|
||||
vacation_response, ..
|
||||
} => break vacation_response,
|
||||
GetJmapVacationResponseResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
};
|
||||
|
||||
match vacation {
|
||||
Some(v) => printer.out(serde_json::to_value(&v)?),
|
||||
None => printer.log("No vacation response configured."),
|
||||
}
|
||||
let Some(vacation) = vacation else {
|
||||
return printer.out(Message::new("No vacation response configured"));
|
||||
};
|
||||
|
||||
let table = VacationTable {
|
||||
preset: account.table_preset,
|
||||
vacation,
|
||||
};
|
||||
|
||||
printer.out(table)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct VacationTable {
|
||||
#[serde(skip)]
|
||||
pub preset: String,
|
||||
pub vacation: VacationResponse,
|
||||
}
|
||||
|
||||
impl fmt::Display for VacationTable {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut table = Table::new();
|
||||
let v = &self.vacation;
|
||||
|
||||
table
|
||||
.load_preset(&self.preset)
|
||||
.set_header(Row::from([Cell::new("KEY"), Cell::new("VALUE")]));
|
||||
|
||||
table.add_row(Row::from([
|
||||
Cell::new("Enabled"),
|
||||
Cell::new(if v.is_enabled { "true" } else { "" }),
|
||||
]));
|
||||
|
||||
if let Some(d) = &v.from_date {
|
||||
table.add_row(Row::from([Cell::new("From"), Cell::new(d)]));
|
||||
}
|
||||
|
||||
if let Some(d) = &v.to_date {
|
||||
table.add_row(Row::from([Cell::new("To"), Cell::new(d)]));
|
||||
}
|
||||
|
||||
if let Some(s) = &v.subject {
|
||||
table.add_row(Row::from([Cell::new("Subject"), Cell::new(s)]));
|
||||
}
|
||||
|
||||
if let Some(b) = &v.text_body {
|
||||
table.add_row(Row::from([Cell::new("Body (plain)"), Cell::new(b)]));
|
||||
}
|
||||
|
||||
if let Some(b) = &v.html_body {
|
||||
table.add_row(Row::from([Cell::new("Body (HTML)"), Cell::new(b)]));
|
||||
}
|
||||
|
||||
writeln!(f)?;
|
||||
writeln!(f, "{table}")
|
||||
}
|
||||
}
|
||||
|
||||
+19
-12
@@ -1,10 +1,8 @@
|
||||
use anyhow::{bail, Result};
|
||||
use clap::Parser;
|
||||
use io_jmap::{
|
||||
coroutines::vacation_response_set::{
|
||||
SetJmapVacationResponse, SetJmapVacationResponseResult,
|
||||
},
|
||||
types::vacation_response::VacationResponseUpdate,
|
||||
coroutines::vacation_response_set::{SetJmapVacationResponse, SetJmapVacationResponseResult},
|
||||
types::{session::capabilities::VACATION_RESPONSE, vacation_response::VacationResponseUpdate},
|
||||
};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use pimalaya_toolbox::terminal::printer::{Message, Printer};
|
||||
@@ -47,6 +45,20 @@ impl SetVacationCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
// Skip the request if the server does not advertise the
|
||||
// vacation-response capability.
|
||||
let has_vacation = jmap
|
||||
.context
|
||||
.session
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.capabilities
|
||||
.contains_key(VACATION_RESPONSE);
|
||||
|
||||
if !has_vacation {
|
||||
bail!("Vacation response is not supported by the server");
|
||||
}
|
||||
|
||||
let is_enabled = if self.enable {
|
||||
Some(true)
|
||||
} else if self.disable {
|
||||
@@ -69,17 +81,12 @@ impl SetVacationCommand {
|
||||
|
||||
loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
SetJmapVacationResponseResult::Io(io) => {
|
||||
arg = Some(handle(&mut jmap.stream, io)?)
|
||||
}
|
||||
SetJmapVacationResponseResult::Ok { context, .. } => {
|
||||
jmap.context = context;
|
||||
break;
|
||||
}
|
||||
SetJmapVacationResponseResult::Io(io) => arg = Some(handle(&mut jmap.stream, io)?),
|
||||
SetJmapVacationResponseResult::Ok { .. } => break,
|
||||
SetJmapVacationResponseResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
}
|
||||
|
||||
printer.out(Message::new("Vacation response updated."))
|
||||
printer.out(Message::new("Vacation response successfully updated"))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user