mirror of
https://github.com/pimalaya/himalaya.git
synced 2026-06-17 13:17:55 +08:00
e377aede15
Refs: #670
134 lines
4.5 KiB
Rust
134 lines
4.5 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/>.
|
|
|
|
//! Himalaya wrapper around [`io_jmap::client::JmapClientStd`] that
|
|
//! bundles the merged [`Account`] alongside the live JMAP client.
|
|
//!
|
|
//! Built up front by the dispatch layer (`crate::cli`) via
|
|
//! [`build_jmap_client`] and handed down to every JMAP-specific
|
|
//! subcommand.
|
|
|
|
use std::{
|
|
ops::{Deref, DerefMut},
|
|
path::PathBuf,
|
|
};
|
|
|
|
use anyhow::{Result, anyhow};
|
|
use base64::{Engine, prelude::BASE64_STANDARD};
|
|
use io_jmap::client::JmapClientStd as Inner;
|
|
use pimalaya_config::toml::TomlConfig;
|
|
use secrecy::{ExposeSecret, SecretString};
|
|
use url::Url;
|
|
|
|
use crate::{
|
|
account::context::Account,
|
|
cli::load_or_wizard,
|
|
config::{JmapAuthConfig, JmapConfig},
|
|
};
|
|
|
|
pub struct JmapClient {
|
|
inner: Inner,
|
|
/// The original JMAP config block, kept around so commands like
|
|
/// `email import` / `email export` can spin up their own
|
|
/// auxiliary sessions (e.g. against the upload/download URL when
|
|
/// it lives on a different authority than the API URL).
|
|
pub config: JmapConfig,
|
|
}
|
|
|
|
impl JmapClient {
|
|
/// Establishes the JMAP session (TLS, `/.well-known/jmap`
|
|
/// discovery).
|
|
pub fn new(config: JmapConfig) -> Result<Self> {
|
|
let tls = config.tls.clone().into_tls(config.alpn.clone());
|
|
|
|
let http_auth = jmap_http_auth(config.auth.clone())?;
|
|
let url = parse_server_url(&config.server)?;
|
|
|
|
let mut inner = Inner::connect(&url, &tls, http_auth)?;
|
|
inner.session_get(&url)?;
|
|
|
|
Ok(Self { inner, config })
|
|
}
|
|
}
|
|
|
|
impl Deref for JmapClient {
|
|
type Target = Inner;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.inner
|
|
}
|
|
}
|
|
|
|
impl DerefMut for JmapClient {
|
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
&mut self.inner
|
|
}
|
|
}
|
|
|
|
/// Loads the configuration, picks the active account, builds the
|
|
/// merged [`Account`] then opens the JMAP session. Bails when the
|
|
/// account has no `[jmap]` block. Returns the live client paired
|
|
/// with the merged account so subcommands receive both as sibling
|
|
/// arguments.
|
|
pub fn build_jmap_client(
|
|
config_paths: &[PathBuf],
|
|
account_name: Option<&str>,
|
|
) -> Result<(Account, JmapClient)> {
|
|
let mut config = load_or_wizard(config_paths)?;
|
|
let (name, mut ac) = config
|
|
.take_account(account_name)?
|
|
.ok_or_else(|| anyhow!("Cannot find account"))?;
|
|
let jmap_config = ac
|
|
.jmap
|
|
.take()
|
|
.ok_or_else(|| anyhow!("JMAP config is missing for account `{name}`"))?;
|
|
let account = Account::from(config).merge(Account::from(ac));
|
|
let client = JmapClient::new(jmap_config)?;
|
|
Ok((account, client))
|
|
}
|
|
|
|
/// Parses the JMAP `server` field into a [`Url`], defaulting bare
|
|
/// authorities (e.g. `mail.example.com`) to `https://`.
|
|
pub fn parse_server_url(server: &str) -> Result<Url> {
|
|
match Url::parse(server) {
|
|
Ok(url) => Ok(url),
|
|
Err(url::ParseError::RelativeUrlWithoutBase) => {
|
|
Ok(Url::parse(&format!("https://{server}"))?)
|
|
}
|
|
Err(err) => Err(err.into()),
|
|
}
|
|
}
|
|
|
|
/// Converts a [`JmapAuthConfig`] into the pre-formatted HTTP
|
|
/// `Authorization` header value [`JmapClientStd::connect`] expects.
|
|
///
|
|
/// [`JmapClientStd::connect`]: io_jmap::client::JmapClientStd::connect
|
|
pub fn jmap_http_auth(config: JmapAuthConfig) -> Result<SecretString> {
|
|
match config {
|
|
JmapAuthConfig::Header(token) => Ok(token.get()?),
|
|
JmapAuthConfig::Bearer { token } => {
|
|
let token = token.get()?;
|
|
Ok(format!("Bearer {}", token.expose_secret()).into())
|
|
}
|
|
JmapAuthConfig::Basic { username, password } => {
|
|
let creds = format!("{}:{}", username, password.get()?.expose_secret());
|
|
let encoded = BASE64_STANDARD.encode(creds.into_bytes());
|
|
Ok(format!("Basic {encoded}").into())
|
|
}
|
|
}
|
|
}
|