mirror of
https://github.com/pimalaya/himalaya.git
synced 2026-06-15 11:27:53 +08:00
aff4fddfb4
Refs: #688
196 lines
6.4 KiB
Rust
196 lines
6.4 KiB
Rust
// This file is part of Himalaya, a CLI to manage emails.
|
|
//
|
|
// Copyright (C) 2022-2026 soywod <pimalaya.org@posteo.net>
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify it under
|
|
// the terms of the GNU Affero General Public License as published by the Free
|
|
// Software Foundation, either version 3 of the License, or (at your option) any
|
|
// later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful, but WITHOUT
|
|
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
// details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
use std::{collections::HashMap, fmt};
|
|
|
|
use anyhow::{Result, anyhow};
|
|
use clap::Parser;
|
|
use comfy_table::{Cell, Row, Table};
|
|
use io_imap::types::{
|
|
IntoStatic,
|
|
core::{IString, NString},
|
|
};
|
|
use pimalaya_cli::printer::Printer;
|
|
use serde::Serialize;
|
|
|
|
use crate::{config::ImapIdConfig, imap::client::ImapClient};
|
|
|
|
/// Get information about the IMAP server.
|
|
///
|
|
/// This command allows you to exchange parameters with the IMAP
|
|
/// server accordingly to the [RFC 2971]. Some providers like mail.qq
|
|
/// enforce sending ID command before selecting a mailbox.
|
|
///
|
|
/// [RFC 2971]: https://www.rfc-editor.org/rfc/rfc2971.html
|
|
#[derive(Debug, Parser)]
|
|
pub struct ImapIdCommand {
|
|
#[arg(short, long, num_args = 1..)]
|
|
#[arg(value_name = "KEY:VAL", value_parser = parameter_parser)]
|
|
parameter: Option<Vec<(IString<'static>, NString<'static>)>>,
|
|
}
|
|
|
|
impl ImapIdCommand {
|
|
pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> {
|
|
let mut params: HashMap<IString<'static>, NString<'static>> = HashMap::new();
|
|
for key in ["name", "version", "vendor", "support-url"] {
|
|
let (k, v) = build_canned_pair(key)?;
|
|
params.insert(k, v);
|
|
}
|
|
|
|
if let Some(more) = self.parameter {
|
|
params.extend(more);
|
|
}
|
|
|
|
let params = client.id(Some(params.into_iter().collect()))?;
|
|
|
|
let table = ServerIdTable {
|
|
preset: client.account.table_preset().to_string(),
|
|
server_id: params
|
|
.unwrap_or_default()
|
|
.into_iter()
|
|
.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()).ok()?),
|
|
None => None,
|
|
},
|
|
))
|
|
})
|
|
.collect(),
|
|
};
|
|
|
|
printer.out(table)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
pub struct ServerIdTable {
|
|
#[serde(skip)]
|
|
pub preset: String,
|
|
pub server_id: HashMap<String, Option<String>>,
|
|
}
|
|
|
|
impl fmt::Display for ServerIdTable {
|
|
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("PARAMETER"), Cell::new("VALUE")]));
|
|
|
|
for (key, val) in &self.server_id {
|
|
table.add_row(Row::from([
|
|
Cell::new(key),
|
|
match val {
|
|
Some(val) => Cell::new(val),
|
|
None => Cell::new(""),
|
|
},
|
|
]));
|
|
}
|
|
|
|
writeln!(f)?;
|
|
writeln!(f, "{table}")
|
|
}
|
|
}
|
|
|
|
/// Resolves an [`ImapIdConfig`] into the wire-level parameter list
|
|
/// passed to the io-imap auth coroutines.
|
|
///
|
|
/// [`None`] when `auto = false`; otherwise a vec where each entry
|
|
/// maps the user-supplied key to either himalaya's canned value
|
|
/// (when the user set `true` and the key is well-known) or `NIL`.
|
|
/// Unknown keys with `true` log a warning and fall back to `NIL`.
|
|
pub fn resolve_auto_id_params(
|
|
config: &ImapIdConfig,
|
|
) -> Result<Option<Vec<(IString<'static>, NString<'static>)>>> {
|
|
if !config.auto {
|
|
return Ok(None);
|
|
}
|
|
|
|
let mut params = Vec::with_capacity(config.fields.len());
|
|
for (key, &use_canned) in &config.fields {
|
|
let ikey = IString::try_from(key.clone())
|
|
.map_err(|err| anyhow!("Invalid IMAP ID parameter key `{key}`: {err}"))?
|
|
.into_static();
|
|
|
|
let nval = if use_canned {
|
|
match canned_value(key) {
|
|
Some(value) => NString::try_from(value)
|
|
.map_err(|err| {
|
|
anyhow!("Invalid canned IMAP ID value `{value}` for `{key}`: {err}")
|
|
})?
|
|
.into_static(),
|
|
None => {
|
|
log::warn!("imap.id.fields.{key} = true: no canned value defined, sending NIL");
|
|
NString::NIL
|
|
}
|
|
}
|
|
} else {
|
|
NString::NIL
|
|
};
|
|
|
|
params.push((ikey, nval));
|
|
}
|
|
Ok(Some(params))
|
|
}
|
|
|
|
fn parameter_parser(param: &str) -> Result<(IString<'static>, NString<'static>), String> {
|
|
let Some((key, val)) = param.split_once(':') else {
|
|
return Err(format!("Invalid parameter `{param}`: missing `:`"));
|
|
};
|
|
|
|
let Ok(ikey) = IString::try_from(key.trim()) else {
|
|
return Err(format!("Invalid parameter key `{key}`"));
|
|
};
|
|
|
|
let nval = if val.trim().is_empty() {
|
|
NString::NIL
|
|
} else {
|
|
let Ok(nval) = NString::try_from(val.trim()) else {
|
|
return Err(format!("Invalid parameter value `{val}` for `{key}`"));
|
|
};
|
|
|
|
nval
|
|
};
|
|
|
|
Ok((ikey.into_static(), nval.into_static()))
|
|
}
|
|
|
|
fn canned_value(key: &str) -> Option<&'static str> {
|
|
match key {
|
|
"name" => Some(env!("CARGO_PKG_NAME")),
|
|
"version" => Some(env!("CARGO_PKG_VERSION")),
|
|
"vendor" => Some("Pimalaya"),
|
|
"support-url" => Some("https://github.com/pimalaya/himalaya"),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn build_canned_pair(key: &str) -> Result<(IString<'static>, NString<'static>)> {
|
|
let ikey = IString::try_from(key)
|
|
.map_err(|err| anyhow!("Invalid IMAP ID parameter key `{key}`: {err}"))?
|
|
.into_static();
|
|
let value =
|
|
canned_value(key).ok_or_else(|| anyhow!("No canned IMAP ID value defined for `{key}`"))?;
|
|
let nval = NString::try_from(value)
|
|
.map_err(|err| anyhow!("Invalid canned IMAP ID value `{value}` for `{key}`: {err}"))?
|
|
.into_static();
|
|
Ok((ikey, nval))
|
|
}
|