mirror of
https://github.com/pimalaya/himalaya.git
synced 2026-06-18 05:47:54 +08:00
clean part 3
This commit is contained in:
+68
-104
@@ -1,151 +1,115 @@
|
||||
//! Cross-protocol [`EmailClient`] for the shared subcommands
|
||||
//! (`mailboxes`, `envelopes`, `flags`, `messages`, `attachments`).
|
||||
//!
|
||||
//! Wraps [`io_email::client::EmailClient`] and bundles the active
|
||||
//! Wraps [`io_email::client::EmailClientStd`] and bundles the active
|
||||
//! [`Account`] (display, identity, composer/reader registries) the
|
||||
//! shared commands need alongside the I/O client. Implements
|
||||
//! [`Deref`]/[`DerefMut`] onto the inner client so callers can call
|
||||
//! its methods directly.
|
||||
//!
|
||||
//! Construction is backend-asymmetric (IMAP needs TLS + SASL, JMAP
|
||||
//! needs an HTTP credential, Maildir just needs a root path). Each
|
||||
//! `new_<protocol>` constructor delegates to the transitional
|
||||
//! [`ImapSession`] / [`JmapSession`] helpers for the handshake/auth
|
||||
//! flow then bridges the resulting `(stream, context)` pairs into
|
||||
//! [`io_imap::client::ImapClient`] / [`io_jmap::client::JmapClient`]
|
||||
//! via their `from_parts` constructors.
|
||||
//!
|
||||
//! [`ImapSession`]: crate::imap::session::ImapSession
|
||||
//! [`JmapSession`]: crate::jmap::session::JmapSession
|
||||
//! needs an HTTP credential, Maildir just needs a root path). The
|
||||
//! single [`EmailClient::new`] entry-point loads the configuration,
|
||||
//! picks the merged account, then walks the configured backends in
|
||||
//! `jmap → imap → maildir` order and opens the first one allowed by
|
||||
//! the `BackendFlag`.
|
||||
|
||||
use std::{
|
||||
ops::{Deref, DerefMut},
|
||||
path::PathBuf,
|
||||
};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use io_email::client::SendMessageOpts;
|
||||
use pimalaya_config::toml::TomlConfig;
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
use crate::{
|
||||
account::context::Account,
|
||||
cli::{load_or_wizard, BackendFlag},
|
||||
backend::Backend,
|
||||
config::{AccountConfig, Config},
|
||||
};
|
||||
|
||||
pub struct EmailClient {
|
||||
inner: io_email::client::EmailClient,
|
||||
inner: io_email::client::EmailClientStd,
|
||||
pub account: Account,
|
||||
/// Pre-computed options for [`io_email::client::EmailClient::send_message`].
|
||||
/// Populated by the per-protocol constructors with the bits each
|
||||
/// backend needs (currently only the JMAP identity / drafts
|
||||
/// mailbox ids); other fields are filled in at send time from the
|
||||
/// outgoing message itself.
|
||||
pub send_opts: SendMessageOpts,
|
||||
}
|
||||
|
||||
impl EmailClient {
|
||||
/// Loads the configuration, picks the active account, builds the
|
||||
/// merged [`Account`], then constructs an [`EmailClient`] for the
|
||||
/// first backend allowed by `backend` that is configured on the
|
||||
/// account. Selection order is `jmap → imap → maildir`. Bails when
|
||||
/// no backend matches.
|
||||
/// merged [`Account`], then opens the first backend allowed by
|
||||
/// `backend` that is configured on the account. Selection order
|
||||
/// is `jmap → imap → maildir`. Bails when no backend matches.
|
||||
pub fn new(
|
||||
config_paths: &[PathBuf],
|
||||
account_name: Option<&str>,
|
||||
backend: BackendFlag,
|
||||
config: Config,
|
||||
mut account_config: AccountConfig,
|
||||
backend: Backend,
|
||||
) -> Result<Self> {
|
||||
let mut config = load_or_wizard(config_paths)?;
|
||||
let (_, mut ac) = config
|
||||
.take_account(account_name)?
|
||||
.ok_or_else(|| anyhow!("Cannot find account"))?;
|
||||
|
||||
#[cfg(feature = "jmap")]
|
||||
if backend.allows_jmap() {
|
||||
if let Some(jmap_config) = ac.jmap.take() {
|
||||
let account = Account::from(config).merge(Account::from(ac));
|
||||
return EmailClient::new_jmap(jmap_config, account);
|
||||
if let Some(jmap_config) = account_config.jmap.take() {
|
||||
use io_email::client::EmailClientStd;
|
||||
use io_jmap::client::JmapClientStd;
|
||||
use pimalaya_stream::tls::Tls;
|
||||
|
||||
use crate::jmap::client::{jmap_http_auth, parse_server_url};
|
||||
|
||||
let account = Account::from(config).merge(Account::from(account_config));
|
||||
let mut tls: Tls = jmap_config.tls.clone().into();
|
||||
tls.rustls.alpn = vec!["http/1.1".into()];
|
||||
let http_auth = jmap_http_auth(jmap_config.auth.clone())?;
|
||||
let url = parse_server_url(&jmap_config.server)?;
|
||||
let mut client = JmapClientStd::connect(&url, &tls, http_auth)?;
|
||||
client.session_get(&url)?;
|
||||
|
||||
return Ok(Self {
|
||||
inner: EmailClientStd::Jmap(client),
|
||||
account,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
if backend.allows_imap() {
|
||||
if let Some(imap_config) = ac.imap.take() {
|
||||
let account = Account::from(config).merge(Account::from(ac));
|
||||
return EmailClient::new_imap(imap_config, account);
|
||||
if let Some(imap_config) = account_config.imap.take() {
|
||||
use io_email::client::EmailClientStd;
|
||||
use io_imap::client::ImapClientStd;
|
||||
use pimalaya_stream::{sasl::Sasl, std::stream::StreamStd, tls::Tls};
|
||||
|
||||
let account = Account::from(config).merge(Account::from(account_config));
|
||||
let mut tls: Tls = imap_config.tls.into();
|
||||
tls.rustls.alpn = vec!["imap".into()];
|
||||
let sasl: Sasl = imap_config.sasl.try_into()?;
|
||||
let client = ImapClientStd::<StreamStd>::connect(
|
||||
&imap_config.url,
|
||||
&tls,
|
||||
imap_config.starttls,
|
||||
Some(sasl),
|
||||
)?;
|
||||
|
||||
return Ok(Self {
|
||||
inner: EmailClientStd::Imap(client),
|
||||
account,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "maildir")]
|
||||
if backend.allows_maildir() {
|
||||
if let Some(maildir_config) = ac.maildir.take() {
|
||||
let account = Account::from(config).merge(Account::from(ac));
|
||||
return EmailClient::new_maildir(maildir_config, account);
|
||||
if let Some(maildir_config) = account_config.maildir.take() {
|
||||
use io_email::client::EmailClientStd;
|
||||
use io_maildir::client::MaildirClient;
|
||||
|
||||
let account = Account::from(config).merge(Account::from(account_config));
|
||||
let client = MaildirClient::new(maildir_config.root);
|
||||
|
||||
return Ok(Self {
|
||||
inner: EmailClientStd::Maildir(client),
|
||||
account,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
bail!("no backend matching `{backend}` is configured for this account")
|
||||
}
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
pub fn new_imap(config: crate::config::ImapConfig, account: Account) -> Result<Self> {
|
||||
use io_imap::client::ImapClient;
|
||||
|
||||
use crate::imap::session::ImapSession;
|
||||
|
||||
let session = ImapSession::new(
|
||||
config.url,
|
||||
config.tls.try_into()?,
|
||||
config.starttls,
|
||||
config.sasl.try_into()?,
|
||||
)?;
|
||||
let client = ImapClient::from_parts(session.stream, session.context);
|
||||
Ok(Self {
|
||||
inner: client.into(),
|
||||
account,
|
||||
send_opts: SendMessageOpts::default(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "jmap")]
|
||||
pub fn new_jmap(config: crate::config::JmapConfig, account: Account) -> Result<Self> {
|
||||
use io_jmap::client::JmapClient;
|
||||
|
||||
use crate::jmap::session::JmapSession;
|
||||
|
||||
let send_opts = SendMessageOpts {
|
||||
jmap_identity_id: config.identity_id.clone(),
|
||||
jmap_drafts_mailbox_id: config.drafts_mailbox_id.clone(),
|
||||
..SendMessageOpts::default()
|
||||
};
|
||||
|
||||
let session = JmapSession::new(
|
||||
config.server,
|
||||
config.tls.try_into()?,
|
||||
config.auth.try_into()?,
|
||||
)?;
|
||||
let client = JmapClient::from_parts(session.stream, session.http_auth, session.session);
|
||||
Ok(Self {
|
||||
inner: client.into(),
|
||||
account,
|
||||
send_opts,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "maildir")]
|
||||
pub fn new_maildir(config: crate::config::MaildirConfig, account: Account) -> Result<Self> {
|
||||
use io_maildir::client::MaildirClient;
|
||||
|
||||
let client = MaildirClient::new(config.root);
|
||||
Ok(Self {
|
||||
inner: client.into(),
|
||||
account,
|
||||
send_opts: SendMessageOpts::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for EmailClient {
|
||||
type Target = io_email::client::EmailClient;
|
||||
type Target = io_email::client::EmailClientStd;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
|
||||
use std::io::{stdout, Write};
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use mail_parser::{Address as ParserAddress, HeaderValue, MessageParser};
|
||||
use pimalaya_cli::printer::{Message, Printer};
|
||||
|
||||
use crate::shared::client::EmailClient;
|
||||
@@ -35,10 +36,67 @@ pub fn route(
|
||||
}
|
||||
|
||||
if send {
|
||||
let opts = client.send_opts.clone();
|
||||
client.send_message(raw, opts)?;
|
||||
let (from, to) = extract_envelope(&raw)?;
|
||||
let to_refs: Vec<&str> = to.iter().map(String::as_str).collect();
|
||||
client.send_message(raw, &from, &to_refs)?;
|
||||
return printer.out(Message::new("Message successfully sent"));
|
||||
}
|
||||
|
||||
printer.out(Message::new("Message saved"))
|
||||
}
|
||||
|
||||
/// Extracts the envelope sender from `From:` and envelope recipients
|
||||
/// from `To:` / `Cc:` / `Bcc:`. Returns an error when `From:` is
|
||||
/// missing or no recipient header carries at least one address.
|
||||
pub fn extract_envelope(raw: &[u8]) -> Result<(String, Vec<String>)> {
|
||||
let parsed = MessageParser::default()
|
||||
.parse(raw)
|
||||
.ok_or_else(|| anyhow!("failed to parse outgoing message"))?;
|
||||
|
||||
let mut from_emails = Vec::new();
|
||||
if let Some(header) = parsed.header("From").cloned() {
|
||||
if let HeaderValue::Address(addr) = header {
|
||||
collect_emails(addr, &mut from_emails);
|
||||
}
|
||||
}
|
||||
let from = from_emails
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("outgoing message is missing a `From:` header"))?;
|
||||
|
||||
let mut to = Vec::new();
|
||||
for name in ["To", "Cc", "Bcc"] {
|
||||
if let Some(header) = parsed.header(name).cloned() {
|
||||
if let HeaderValue::Address(addr) = header {
|
||||
collect_emails(addr, &mut to);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if to.is_empty() {
|
||||
bail!("outgoing message has no recipients (`To:` / `Cc:` / `Bcc:`)");
|
||||
}
|
||||
|
||||
Ok((from, to))
|
||||
}
|
||||
|
||||
fn collect_emails(addr: ParserAddress<'_>, out: &mut Vec<String>) {
|
||||
match addr {
|
||||
ParserAddress::List(list) => {
|
||||
for a in list {
|
||||
if let Some(email) = a.address {
|
||||
out.push(email.into_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
ParserAddress::Group(groups) => {
|
||||
for g in groups {
|
||||
for a in g.addresses {
|
||||
if let Some(email) = a.address {
|
||||
out.push(email.into_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,13 @@ use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use pimalaya_cli::printer::{Message, Printer};
|
||||
|
||||
use crate::shared::client::EmailClient;
|
||||
use crate::shared::{client::EmailClient, messages::output::extract_envelope};
|
||||
|
||||
/// Send a message via the active account.
|
||||
///
|
||||
/// Supported over JMAP. JMAP requires `identity-id` and
|
||||
/// `drafts-mailbox-id` to be set on the account's `[jmap]` config block.
|
||||
/// Routes through SMTP or JMAP depending on the account's configured
|
||||
/// outgoing backend. The envelope sender is taken from the `From:`
|
||||
/// header and recipients are collected from `To:` / `Cc:` / `Bcc:`.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct MessageSendCommand {
|
||||
/// The raw message, including headers and body.
|
||||
@@ -34,8 +35,10 @@ impl MessageSendCommand {
|
||||
.join("\r\n")
|
||||
};
|
||||
|
||||
let opts = client.send_opts.clone();
|
||||
client.send_message(raw.into_bytes(), opts)?;
|
||||
let raw = raw.into_bytes();
|
||||
let (from, to) = extract_envelope(&raw)?;
|
||||
let to_refs: Vec<&str> = to.iter().map(String::as_str).collect();
|
||||
client.send_message(raw, &from, &to_refs)?;
|
||||
printer.out(Message::new("Message successfully sent"))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user