mirror of
https://github.com/pimalaya/himalaya.git
synced 2026-06-17 21:37:55 +08:00
refactor: clean serializers
This commit is contained in:
@@ -12,7 +12,7 @@ use io_imap::{
|
||||
};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
use serde::{Serialize, Serializer};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::imap::{
|
||||
account::ImapAccount, envelope::search::parse_query, mailbox::arg::MailboxNameOptionalArg,
|
||||
@@ -116,22 +116,15 @@ fn parse_sort_key(s: &str) -> Result<SortKey> {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct SortResult {
|
||||
pub id: u32,
|
||||
}
|
||||
|
||||
pub struct SortResultsTable {
|
||||
results: Vec<SortResult>,
|
||||
ids: Vec<u32>,
|
||||
uid_mode: bool,
|
||||
}
|
||||
|
||||
impl SortResultsTable {
|
||||
pub fn new(ids: Vec<std::num::NonZeroU32>, uid_mode: bool) -> Self {
|
||||
let results = ids
|
||||
.into_iter()
|
||||
.map(|id| SortResult { id: id.get() })
|
||||
.collect();
|
||||
Self { results, uid_mode }
|
||||
let ids = ids.into_iter().map(|id| id.get()).collect();
|
||||
Self { ids, uid_mode }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,20 +139,13 @@ impl fmt::Display for SortResultsTable {
|
||||
.set_content_arrangement(ContentArrangement::DynamicFullWidth)
|
||||
.set_header(Row::from([Cell::new(id_header)]));
|
||||
|
||||
for result in &self.results {
|
||||
table.add_row(Row::from([Cell::new(result.id)]));
|
||||
for id in &self.ids {
|
||||
table.add_row(Row::from([Cell::new(id)]));
|
||||
}
|
||||
|
||||
writeln!(f)?;
|
||||
write!(f, "{table}")?;
|
||||
writeln!(f)?;
|
||||
writeln!(f, "Found {} message(s)", self.results.len())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for SortResultsTable {
|
||||
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
self.results.serialize(serializer)
|
||||
writeln!(f, "Found {} message(s)", self.ids.len())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ use io_imap::{
|
||||
};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use pimalaya_toolbox::{stream::imap::ImapSession, terminal::printer::Printer};
|
||||
use serde::{Serialize, Serializer};
|
||||
use serde::{ser::SerializeStruct, Serialize, Serializer};
|
||||
|
||||
use crate::imap::{
|
||||
account::ImapAccount,
|
||||
@@ -320,6 +320,8 @@ impl ThreadResultsTable {
|
||||
|
||||
impl Serialize for ThreadResultsTable {
|
||||
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
self.build_entries().serialize(serializer)
|
||||
let mut s = serializer.serialize_struct("ThreadResultsTable", 1)?;
|
||||
s.serialize_field("threads", &self.build_entries())?;
|
||||
s.end()
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -74,14 +74,14 @@ impl IdCommand {
|
||||
server_id: params
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|(key, val)| {
|
||||
(
|
||||
String::from_utf8(key.into_inner().into_owned()).unwrap(),
|
||||
.filter_map(|(key, val)| {
|
||||
Some((
|
||||
String::from_utf8(key.into_inner().into_owned()).ok()?,
|
||||
match val.into_option() {
|
||||
Some(val) => Some(String::from_utf8(val.into_owned()).unwrap()),
|
||||
Some(val) => Some(String::from_utf8(val.into_owned()).ok()?),
|
||||
None => None,
|
||||
},
|
||||
)
|
||||
))
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ use io_imap::{
|
||||
};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
use serde::{Serialize, Serializer};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::imap::account::ImapAccount;
|
||||
|
||||
@@ -65,17 +65,18 @@ impl ListMailboxesCommand {
|
||||
|
||||
let table = MailboxesTable {
|
||||
preset: account.table_preset,
|
||||
rows: mailboxes.into_iter().map(From::from).collect(),
|
||||
mailboxes: mailboxes.into_iter().map(From::from).collect(),
|
||||
};
|
||||
|
||||
printer.out(table)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct MailboxesTable {
|
||||
#[serde(skip)]
|
||||
pub preset: String,
|
||||
pub rows: Vec<MailboxRow>,
|
||||
pub mailboxes: Vec<MailboxRow>,
|
||||
}
|
||||
|
||||
impl fmt::Display for MailboxesTable {
|
||||
@@ -89,7 +90,7 @@ impl fmt::Display for MailboxesTable {
|
||||
Cell::new("DELIMITER"),
|
||||
Cell::new("ATTRIBUTES"),
|
||||
]))
|
||||
.add_rows(self.rows.iter().map(|mbox| {
|
||||
.add_rows(self.mailboxes.iter().map(|mbox| {
|
||||
let mut row = Row::new();
|
||||
|
||||
row.max_height(1)
|
||||
@@ -107,12 +108,6 @@ impl fmt::Display for MailboxesTable {
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for MailboxesTable {
|
||||
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
self.rows.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct MailboxRow {
|
||||
pub name: String,
|
||||
|
||||
@@ -38,10 +38,6 @@ pub struct ReadMessageCommand {
|
||||
/// Show HTML content instead of plain text.
|
||||
#[arg(long)]
|
||||
pub html: bool,
|
||||
|
||||
/// Terminal width for text wrapping.
|
||||
#[arg(long, short = 'w', default_value = "80")]
|
||||
pub width: usize,
|
||||
}
|
||||
|
||||
impl ReadMessageCommand {
|
||||
|
||||
@@ -1,378 +0,0 @@
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::{
|
||||
fs,
|
||||
io::{self, Read, Write},
|
||||
net::TcpStream,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use io_imap::{
|
||||
context::ImapContext,
|
||||
coroutines::{
|
||||
authenticate::*, authenticate_anonymous::ImapAuthenticateAnonymousParams,
|
||||
authenticate_plain::ImapAuthenticatePlainParams, capability::*,
|
||||
greeting_with_capability::*, login::ImapLoginParams, starttls::*,
|
||||
},
|
||||
types::{auth::AuthMechanism, response::Capability},
|
||||
};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use log::{debug, info};
|
||||
#[cfg(feature = "native-tls")]
|
||||
use native_tls::TlsConnector;
|
||||
#[cfg(any(feature = "rustls-aws", feature = "rustls-ring"))]
|
||||
use rustls::{
|
||||
crypto::{self, CryptoProvider},
|
||||
pki_types::{pem::PemObject, CertificateDer},
|
||||
ClientConfig, ClientConnection, StreamOwned,
|
||||
};
|
||||
#[cfg(any(feature = "rustls-aws", feature = "rustls-ring"))]
|
||||
use rustls_platform_verifier::{ConfigVerifierExt, Verifier};
|
||||
#[cfg(windows)]
|
||||
use uds_windows::UnixStream;
|
||||
|
||||
use crate::config::{ImapConfig, RustlsCryptoConfig, SaslMechanismConfig, TlsProviderConfig};
|
||||
|
||||
pub enum Stream {
|
||||
Tcp(TcpStream),
|
||||
Unix(UnixStream),
|
||||
#[cfg(any(feature = "rustls-aws", feature = "rustls-ring"))]
|
||||
Rustls(StreamOwned<ClientConnection, TcpStream>),
|
||||
#[cfg(feature = "native-tls")]
|
||||
NativeTls(native_tls::TlsStream<TcpStream>),
|
||||
}
|
||||
|
||||
pub fn connect(mut config: ImapConfig) -> Result<(ImapContext, Stream)> {
|
||||
info!("connecting to IMAP server using {}", config.url);
|
||||
|
||||
let mut context = ImapContext::new();
|
||||
let host = config.url.host_str().unwrap_or("127.0.0.1");
|
||||
|
||||
let (mut context, mut stream) = match config.url.scheme() {
|
||||
scheme if scheme.eq_ignore_ascii_case("imap") => {
|
||||
let port = config.url.port().unwrap_or(143);
|
||||
let mut stream = TcpStream::connect((host, port))?;
|
||||
|
||||
let mut coroutine = GetImapGreetingWithCapability::new(context);
|
||||
let mut arg = None;
|
||||
|
||||
loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
GetImapGreetingWithCapabilityResult::Io { io } => {
|
||||
arg = Some(handle(&mut stream, io)?)
|
||||
}
|
||||
GetImapGreetingWithCapabilityResult::Ok { context: c } => break context = c,
|
||||
GetImapGreetingWithCapabilityResult::Err { err, .. } => Err(err)?,
|
||||
}
|
||||
}
|
||||
|
||||
(context, Stream::Tcp(stream))
|
||||
}
|
||||
scheme if scheme.eq_ignore_ascii_case("imaps") => {
|
||||
let port = config.url.port().unwrap_or(993);
|
||||
let mut stream = TcpStream::connect((host, port))?;
|
||||
|
||||
if config.starttls {
|
||||
let mut coroutine = ImapStartTls::new(context);
|
||||
let mut arg = None;
|
||||
|
||||
loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
ImapStartTlsResult::Io { io } => arg = Some(handle(&mut stream, io)?),
|
||||
ImapStartTlsResult::Ok { context: c } => break context = c,
|
||||
ImapStartTlsResult::Err { err, .. } => Err(err)?,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let tls_provider = match config.tls.provider {
|
||||
#[cfg(any(feature = "rustls-aws", feature = "rustls-ring"))]
|
||||
Some(TlsProviderConfig::Rustls) => TlsProviderConfig::Rustls,
|
||||
#[cfg(not(feature = "rustls-aws"))]
|
||||
#[cfg(not(feature = "rustls-ring"))]
|
||||
Some(TlsProviderConfig::Rustls) => {
|
||||
bail!("Required cargo feature: `rustls-aws` or `rustls-ring`")
|
||||
}
|
||||
#[cfg(feature = "native-tls")]
|
||||
Some(TlsProviderConfig::NativeTls) => TlsProviderConfig::NativeTls,
|
||||
#[cfg(not(feature = "native-tls"))]
|
||||
Some(TlsProviderConfig::NativeTls) => {
|
||||
bail!("Required cargo feature: `native-tls`")
|
||||
}
|
||||
#[cfg(any(feature = "rustls-aws", feature = "rustls-ring"))]
|
||||
None => TlsProviderConfig::Rustls,
|
||||
#[cfg(not(feature = "rustls-aws"))]
|
||||
#[cfg(not(feature = "rustls-ring"))]
|
||||
#[cfg(feature = "native-tls")]
|
||||
None => TlsProviderConfig::NativeTls,
|
||||
#[cfg(not(feature = "rustls-aws"))]
|
||||
#[cfg(not(feature = "rustls-ring"))]
|
||||
#[cfg(not(feature = "native-tls"))]
|
||||
None => {
|
||||
bail!("Required cargo feature: `rustls-aws`, `rustls-ring` or `native-tls`")
|
||||
}
|
||||
};
|
||||
|
||||
debug!("using TLS provider: {tls_provider:?}");
|
||||
|
||||
let mut stream = match tls_provider {
|
||||
#[cfg(any(feature = "rustls-aws", feature = "rustls-ring"))]
|
||||
TlsProviderConfig::Rustls => {
|
||||
let crypto_provider = match config.tls.rustls.crypto {
|
||||
#[cfg(feature = "rustls-aws")]
|
||||
Some(RustlsCryptoConfig::Aws) => RustlsCryptoConfig::Aws,
|
||||
#[cfg(not(feature = "rustls-aws"))]
|
||||
Some(RustlsCryptoConfig::Aws) => {
|
||||
bail!("Required cargo feature: `rustls-aws`");
|
||||
}
|
||||
#[cfg(feature = "rustls-ring")]
|
||||
Some(RustlsCryptoConfig::Ring) => RustlsCryptoConfig::Ring,
|
||||
#[cfg(not(feature = "rustls-ring"))]
|
||||
Some(RustlsCryptoConfig::Ring) => {
|
||||
bail!("Required cargo feature: `rustls-ring`");
|
||||
}
|
||||
#[cfg(feature = "rustls-ring")]
|
||||
None => RustlsCryptoConfig::Ring,
|
||||
#[cfg(not(feature = "rustls-ring"))]
|
||||
#[cfg(feature = "rustls-aws")]
|
||||
None => RustlsCryptoConfig::Aws,
|
||||
#[cfg(not(feature = "rustls-aws"))]
|
||||
#[cfg(not(feature = "rustls-ring"))]
|
||||
None => {
|
||||
bail!("Required cargo feature: `rustls-aws` or `rustls-ring`");
|
||||
}
|
||||
};
|
||||
|
||||
debug!("using rustls crypto provider: {crypto_provider:?}");
|
||||
|
||||
let crypto_provider = match crypto_provider {
|
||||
#[cfg(feature = "rustls-aws")]
|
||||
RustlsCryptoConfig::Aws => crypto::aws_lc_rs::default_provider(),
|
||||
#[cfg(feature = "rustls-ring")]
|
||||
RustlsCryptoConfig::Ring => crypto::ring::default_provider(),
|
||||
#[allow(unreachable_patterns)]
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let crypto_provider = match crypto_provider.install_default() {
|
||||
Ok(()) => CryptoProvider::get_default().unwrap().clone(),
|
||||
Err(crypto_provider) => crypto_provider,
|
||||
};
|
||||
|
||||
let mut config = if let Some(pem_path) = &config.tls.cert {
|
||||
debug!("using TLS cert at {}", pem_path.display());
|
||||
let pem = fs::read(pem_path)?;
|
||||
|
||||
let Some(cert) = CertificateDer::pem_slice_iter(&pem).next() else {
|
||||
bail!("empty TLS cert at {}", pem_path.display())
|
||||
};
|
||||
|
||||
let verifier =
|
||||
Verifier::new_with_extra_roots(vec![cert?], crypto_provider)?;
|
||||
|
||||
ClientConfig::builder()
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(verifier))
|
||||
.with_no_client_auth()
|
||||
} else {
|
||||
debug!("using OS TLS certs");
|
||||
ClientConfig::with_platform_verifier()?
|
||||
};
|
||||
|
||||
config.alpn_protocols = vec![b"imap".to_vec()];
|
||||
|
||||
let server_name = host.to_string().try_into()?;
|
||||
let conn = ClientConnection::new(Arc::new(config), server_name)?;
|
||||
Stream::Rustls(StreamOwned::new(conn, stream))
|
||||
}
|
||||
#[cfg(feature = "native-tls")]
|
||||
TlsProviderConfig::NativeTls => {
|
||||
let mut builder = TlsConnector::builder();
|
||||
|
||||
if let Some(pem_path) = &config.tls.cert {
|
||||
debug!("using TLS cert at {}", pem_path.display());
|
||||
let pem = fs::read(pem_path)?;
|
||||
let cert = native_tls::Certificate::from_pem(&pem)?;
|
||||
builder.add_root_certificate(cert);
|
||||
}
|
||||
|
||||
let connector = builder.build()?;
|
||||
Stream::NativeTls(connector.connect(host, stream)?)
|
||||
}
|
||||
#[allow(unreachable_patterns)]
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
if config.starttls {
|
||||
let mut coroutine = GetImapCapability::new(context);
|
||||
let mut arg = None;
|
||||
|
||||
loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
GetImapCapabilityResult::Io { io } => arg = Some(handle(&mut stream, io)?),
|
||||
GetImapCapabilityResult::Ok { context: c } => break context = c,
|
||||
GetImapCapabilityResult::Err { err, .. } => Err(err)?,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut coroutine = GetImapGreetingWithCapability::new(context);
|
||||
let mut arg = None;
|
||||
|
||||
loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
GetImapGreetingWithCapabilityResult::Io { io } => {
|
||||
arg = Some(handle(&mut stream, io)?)
|
||||
}
|
||||
GetImapGreetingWithCapabilityResult::Ok { context: c } => {
|
||||
break context = c
|
||||
}
|
||||
GetImapGreetingWithCapabilityResult::Err { err, .. } => Err(err)?,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(context, stream)
|
||||
}
|
||||
scheme if scheme.eq_ignore_ascii_case("unix") => {
|
||||
let sock_path = config.url.path();
|
||||
let mut stream = UnixStream::connect(&sock_path)?;
|
||||
|
||||
let mut coroutine = GetImapGreetingWithCapability::new(context);
|
||||
let mut arg = None;
|
||||
|
||||
loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
GetImapGreetingWithCapabilityResult::Io { io } => {
|
||||
arg = Some(handle(&mut stream, io)?)
|
||||
}
|
||||
GetImapGreetingWithCapabilityResult::Ok { context: c } => break context = c,
|
||||
GetImapGreetingWithCapabilityResult::Err { err, .. } => Err(err)?,
|
||||
}
|
||||
}
|
||||
|
||||
(context, Stream::Unix(stream))
|
||||
}
|
||||
scheme => {
|
||||
bail!("Unknown scheme `{scheme}`, expected imap, imaps or unix");
|
||||
}
|
||||
};
|
||||
|
||||
if !context.authenticated {
|
||||
let mut candidates = vec![];
|
||||
|
||||
let ir = context.capability.contains(&Capability::SaslIr);
|
||||
|
||||
for mechanism in config.sasl.mechanisms {
|
||||
match mechanism {
|
||||
SaslMechanismConfig::Login => {
|
||||
let Some(auth) = config.sasl.login.take() else {
|
||||
debug!("missing SASL LOGIN configuration, skipping it");
|
||||
continue;
|
||||
};
|
||||
|
||||
if context.capability.contains(&Capability::LoginDisabled) {
|
||||
debug!("SASL LOGIN disabled by the server, skipping it");
|
||||
continue;
|
||||
}
|
||||
|
||||
let login = Capability::Auth(AuthMechanism::Login);
|
||||
if !context.capability.contains(&login) {
|
||||
debug!("SASL LOGIN disabled by the server, skipping it");
|
||||
continue;
|
||||
}
|
||||
|
||||
candidates.push(ImapAuthenticateCandidate::Login(ImapLoginParams::new(
|
||||
auth.username,
|
||||
auth.password.get()?,
|
||||
)?));
|
||||
}
|
||||
SaslMechanismConfig::Plain => {
|
||||
let Some(auth) = config.sasl.plain.take() else {
|
||||
debug!("missing SASL PLAIN configuration, skipping it");
|
||||
continue;
|
||||
};
|
||||
|
||||
let plain = Capability::Auth(AuthMechanism::Plain);
|
||||
if !context.capability.contains(&plain) {
|
||||
debug!("SASL PLAIN disabled by the server, skipping it");
|
||||
continue;
|
||||
}
|
||||
|
||||
candidates.push(ImapAuthenticateCandidate::Plain(
|
||||
ImapAuthenticatePlainParams::new(
|
||||
auth.authzid,
|
||||
auth.authcid,
|
||||
auth.passwd.get()?,
|
||||
ir,
|
||||
),
|
||||
));
|
||||
}
|
||||
SaslMechanismConfig::Anonymous => {
|
||||
// TODO: check if capability available
|
||||
|
||||
let message = config
|
||||
.sasl
|
||||
.anonymous
|
||||
.take()
|
||||
.and_then(|auth| auth.message)
|
||||
.unwrap_or_default();
|
||||
|
||||
candidates.push(ImapAuthenticateCandidate::Anonymous(
|
||||
ImapAuthenticateAnonymousParams::new(message, ir),
|
||||
));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let mut arg = None;
|
||||
let mut coroutine = ImapAuthenticate::new(context, candidates);
|
||||
|
||||
loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
ImapAuthenticateResult::Io { io } => arg = Some(handle(&mut stream, io)?),
|
||||
ImapAuthenticateResult::Ok { context: c, .. } => break context = c,
|
||||
ImapAuthenticateResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((context, stream))
|
||||
}
|
||||
|
||||
impl Read for Stream {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
match self {
|
||||
Self::Tcp(s) => s.read(buf),
|
||||
Self::Unix(s) => s.read(buf),
|
||||
#[cfg(any(feature = "rustls-aws", feature = "rustls-ring"))]
|
||||
Self::Rustls(s) => s.read(buf),
|
||||
#[cfg(feature = "native-tls")]
|
||||
Self::NativeTls(s) => s.read(buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for Stream {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
match self {
|
||||
Self::Tcp(s) => s.write(buf),
|
||||
Self::Unix(s) => s.write(buf),
|
||||
#[cfg(any(feature = "rustls-aws", feature = "rustls-ring"))]
|
||||
Self::Rustls(s) => s.write(buf),
|
||||
#[cfg(feature = "native-tls")]
|
||||
Self::NativeTls(s) => s.write(buf),
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
match self {
|
||||
Self::Tcp(s) => s.flush(),
|
||||
Self::Unix(s) => s.flush(),
|
||||
#[cfg(any(feature = "rustls-aws", feature = "rustls-ring"))]
|
||||
Self::Rustls(s) => s.flush(),
|
||||
#[cfg(feature = "native-tls")]
|
||||
Self::NativeTls(s) => s.flush(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use clap::Parser;
|
||||
use io_jmap::{
|
||||
rfc8620::coroutines::blob_download::{JmapBlobDownload, JmapBlobDownloadResult},
|
||||
rfc8620::types::session::capabilities,
|
||||
rfc8620::{
|
||||
coroutines::blob_download::{JmapBlobDownload, JmapBlobDownloadResult},
|
||||
types::session::capabilities::{self, MAIL},
|
||||
},
|
||||
rfc8621::coroutines::email_get::{JmapEmailGet, JmapEmailGetResult},
|
||||
};
|
||||
use io_stream::runtimes::std::handle;
|
||||
@@ -52,7 +54,7 @@ impl ExportEmailCommand {
|
||||
let account_id = jmap
|
||||
.session
|
||||
.primary_accounts
|
||||
.get(capabilities::MAIL)
|
||||
.get(MAIL)
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("");
|
||||
let blob_id = emails
|
||||
|
||||
@@ -6,10 +6,14 @@ use std::{
|
||||
use anyhow::{bail, Result};
|
||||
use clap::Parser;
|
||||
use io_jmap::{
|
||||
rfc8620::coroutines::blob_upload::{JmapBlobUpload, JmapBlobUploadResult},
|
||||
rfc8620::types::session::capabilities,
|
||||
rfc8621::coroutines::email_import::{JmapEmailImport, JmapEmailImportResult},
|
||||
rfc8621::types::email::EmailImport,
|
||||
rfc8620::{
|
||||
coroutines::blob_upload::{JmapBlobUpload, JmapBlobUploadResult},
|
||||
types::session::capabilities::{self, MAIL},
|
||||
},
|
||||
rfc8621::{
|
||||
coroutines::email_import::{JmapEmailImport, JmapEmailImportResult},
|
||||
types::email::EmailImport,
|
||||
},
|
||||
};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use pimalaya_toolbox::terminal::printer::{Message, Printer};
|
||||
@@ -64,7 +68,7 @@ impl ImportEmailCommand {
|
||||
let account_id = jmap
|
||||
.session
|
||||
.primary_accounts
|
||||
.get(capabilities::MAIL)
|
||||
.get(MAIL)
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("");
|
||||
let url: Url = jmap
|
||||
|
||||
+18
-1
@@ -4,6 +4,7 @@ use io_jmap::rfc8621::coroutines::email_parse::{JmapEmailParse, JmapEmailParseRe
|
||||
use io_stream::runtimes::std::handle;
|
||||
use log::warn;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
|
||||
@@ -49,13 +50,15 @@ impl ParseEmailCommand {
|
||||
warn!("blob `{id}` not valid MIME message, ignoring it");
|
||||
}
|
||||
|
||||
let mut bodies = Vec::new();
|
||||
|
||||
for (_blob_id, email) in parsed {
|
||||
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)?;
|
||||
bodies.push(body_value.value.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,6 +66,20 @@ impl ParseEmailCommand {
|
||||
}
|
||||
}
|
||||
|
||||
printer.out(ParsedBodies { bodies })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ParsedBodies {
|
||||
bodies: Vec<String>,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ParsedBodies {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
for body in &self.bodies {
|
||||
write!(f, "{body}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,7 +171,6 @@ impl JmapEmailQueryCommand {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct EmailsTable {
|
||||
#[serde(skip)]
|
||||
pub preset: String,
|
||||
|
||||
@@ -58,7 +58,6 @@ impl GetIdentityCommand {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct IdentitiesTable {
|
||||
#[serde(skip)]
|
||||
pub preset: String,
|
||||
|
||||
@@ -118,7 +118,6 @@ impl JmapMailboxQueryCommand {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct MailboxesTable {
|
||||
#[serde(skip)]
|
||||
pub preset: String,
|
||||
|
||||
+23
-16
@@ -7,11 +7,12 @@ use anyhow::{bail, Context, Result};
|
||||
use clap::Parser;
|
||||
use io_jmap::rfc8620::{
|
||||
coroutines::send::{JmapRequest, JmapSend, JmapSendResult},
|
||||
types::session::capabilities,
|
||||
types::session::capabilities::{CORE, MAIL},
|
||||
};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
|
||||
@@ -53,50 +54,52 @@ impl QueryCommand {
|
||||
self.method_calls.join(" ")
|
||||
};
|
||||
|
||||
let calls_value: serde_json::Value =
|
||||
let calls_value: Value =
|
||||
serde_json::from_str(&raw).context("METHOD_CALLS is not valid JSON")?;
|
||||
|
||||
let serde_json::Value::Array(calls_arr) = calls_value else {
|
||||
let Value::Array(calls_arr) = calls_value else {
|
||||
bail!("METHOD_CALLS must be a JSON array");
|
||||
};
|
||||
|
||||
let account_id = jmap
|
||||
.session
|
||||
.primary_accounts
|
||||
.get(capabilities::MAIL)
|
||||
.get(MAIL)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
// Parse and inject accountId into each call's args.
|
||||
let mut method_calls = Vec::with_capacity(calls_arr.len());
|
||||
for (i, call) in calls_arr.into_iter().enumerate() {
|
||||
let serde_json::Value::Array(mut tuple) = call else {
|
||||
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) {
|
||||
serde_json::Value::String(s) => s,
|
||||
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) {
|
||||
serde_json::Value::String(s) => s,
|
||||
Value::String(s) => s,
|
||||
v => bail!("method call #{i} name must be a string, got {v}"),
|
||||
};
|
||||
|
||||
// Inject accountId if the args object doesn't already have it.
|
||||
if let serde_json::Value::Object(ref mut map) = args {
|
||||
if let Value::Object(ref mut map) = args {
|
||||
map.entry("accountId")
|
||||
.or_insert_with(|| serde_json::Value::String(account_id.clone()));
|
||||
.or_insert_with(|| Value::String(account_id.clone()));
|
||||
}
|
||||
|
||||
method_calls.push((name, args, call_id));
|
||||
}
|
||||
|
||||
let mut using = vec![
|
||||
capabilities::CORE.to_string(),
|
||||
capabilities::MAIL.to_string(),
|
||||
];
|
||||
let mut using = vec![CORE.to_string(), MAIL.to_string()];
|
||||
for extra in self.using {
|
||||
if !using.contains(&extra) {
|
||||
using.push(extra);
|
||||
@@ -122,17 +125,21 @@ impl QueryCommand {
|
||||
}
|
||||
};
|
||||
|
||||
printer.out(RawResponse(response.method_responses))
|
||||
printer.out(RawResponse {
|
||||
method_responses: response.method_responses,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps the raw method_responses for display.
|
||||
#[derive(Serialize)]
|
||||
struct RawResponse(Vec<(String, serde_json::Value, String)>);
|
||||
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.0) {
|
||||
match serde_json::to_string_pretty(&self.method_responses) {
|
||||
Ok(s) => write!(f, "{s}"),
|
||||
Err(e) => write!(f, "<serialization error: {e}>"),
|
||||
}
|
||||
|
||||
@@ -108,7 +108,6 @@ impl QuerySubmissionCommand {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct SubmissionsTable {
|
||||
#[serde(skip)]
|
||||
pub preset: String,
|
||||
|
||||
@@ -53,7 +53,6 @@ impl GetThreadCommand {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct ThreadsTable {
|
||||
#[serde(skip)]
|
||||
pub preset: String,
|
||||
|
||||
Reference in New Issue
Block a user