clean part 3

This commit is contained in:
Clément DOUIN
2026-05-19 01:01:44 +02:00
parent cd27969e14
commit 5500b02cfc
25 changed files with 567 additions and 1125 deletions
+11 -16
View File
@@ -1,4 +1,4 @@
//! Himalaya wrapper around [`io_imap::client::ImapClient`] that
//! Himalaya wrapper around [`io_imap::client::ImapClientStd`] that
//! bundles the merged [`Account`] alongside the live IMAP client.
//!
//! This is what every IMAP-specific subcommand receives: the dispatch
@@ -11,36 +11,31 @@ use std::{
};
use anyhow::{anyhow, Result};
use io_imap::client::ImapClient as Inner;
use io_imap::client::ImapClientStd as Inner;
use pimalaya_config::toml::TomlConfig;
use pimalaya_stream::{sasl::Sasl, std::stream::StreamStd, tls::Tls};
use crate::{
account::context::Account, cli::load_or_wizard, config::ImapConfig, imap::session::ImapSession,
};
use crate::{account::context::Account, cli::load_or_wizard, config::ImapConfig};
pub struct ImapClient {
inner: Inner,
inner: Inner<StreamStd>,
pub account: Account,
}
impl ImapClient {
/// Opens the IMAP connection (TCP/TLS/STARTTLS, greeting, SASL)
/// then wraps the resulting stream + context in an
/// [`io_imap::client::ImapClient`] alongside `account`.
/// then wraps the resulting client alongside `account`.
pub fn new(config: ImapConfig, account: Account) -> Result<Self> {
let session = ImapSession::new(
config.url,
config.tls.try_into()?,
config.starttls,
config.sasl.try_into()?,
)?;
let inner = Inner::from_parts(session.stream, session.context);
let mut tls: Tls = config.tls.into();
tls.rustls.alpn = vec!["imap".into()];
let sasl: Sasl = config.sasl.try_into()?;
let inner = Inner::<StreamStd>::connect(&config.url, &tls, config.starttls, Some(sasl))?;
Ok(Self { inner, account })
}
}
impl Deref for ImapClient {
type Target = Inner;
type Target = Inner<StreamStd>;
fn deref(&self) -> &Self::Target {
&self.inner
-1
View File
@@ -5,4 +5,3 @@ pub mod flag;
pub mod id;
pub mod mailbox;
pub mod message;
pub mod session;
-238
View File
@@ -1,238 +0,0 @@
//! Transitional IMAP session helper ported from `pimalaya-toolbox`.
//!
//! Will be replaced by `io_imap::client::ImapClient` once the
//! protocol-specific subcommands switch over.
#[cfg(unix)]
use std::os::unix::net::UnixStream;
use std::{
io::{Read, Write},
net::TcpStream,
};
use anyhow::{bail, Result};
use io_imap::{
context::ImapContext,
rfc3501::{
capability::{ImapCapabilityGet, ImapCapabilityGetResult},
greeting_with_capability::{
ImapGreetingWithCapabilityGet, ImapGreetingWithCapabilityGetResult,
},
login::{ImapSessionLogin, ImapSessionLoginParams, ImapSessionLoginResult},
starttls::{ImapStartTls, ImapStartTlsResult},
},
sasl::authenticate_plain::{
ImapSessionAuthenticatePlain, ImapSessionAuthenticatePlainParams,
ImapSessionAuthenticatePlainResult,
},
types::response::Capability,
};
use log::info;
use pimalaya_stream::{
sasl::{Sasl, SaslMechanism},
std::{
stream::Stream,
tls::{upgrade_tls, Tls},
},
};
#[cfg(windows)]
use uds_windows::UnixStream;
use url::Url;
const READ_BUFFER_SIZE: usize = 16 * 1024;
#[derive(Debug)]
pub struct ImapSession {
pub context: ImapContext,
pub stream: Stream,
}
fn drive_greeting_with_capability<S: Read + Write>(
stream: &mut S,
context: ImapContext,
) -> Result<ImapContext> {
let mut buf = [0u8; READ_BUFFER_SIZE];
let mut coroutine = ImapGreetingWithCapabilityGet::new(context);
let mut arg: Option<&[u8]> = None;
loop {
match coroutine.resume(arg.take()) {
ImapGreetingWithCapabilityGetResult::Ok(context) => return Ok(context),
ImapGreetingWithCapabilityGetResult::WantsRead => {
let n = stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapGreetingWithCapabilityGetResult::WantsWrite(bytes) => {
stream.write_all(&bytes)?;
arg = None;
}
ImapGreetingWithCapabilityGetResult::Err { err, .. } => bail!(err),
}
}
}
fn drive_capability<S: Read + Write>(stream: &mut S, context: ImapContext) -> Result<ImapContext> {
let mut buf = [0u8; READ_BUFFER_SIZE];
let mut coroutine = ImapCapabilityGet::new(context);
let mut arg: Option<&[u8]> = None;
loop {
match coroutine.resume(arg.take()) {
ImapCapabilityGetResult::Ok(context) => return Ok(context),
ImapCapabilityGetResult::WantsRead => {
let n = stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapCapabilityGetResult::WantsWrite(bytes) => {
stream.write_all(&bytes)?;
arg = None;
}
ImapCapabilityGetResult::Err { err, .. } => bail!(err),
}
}
}
fn drive_starttls<S: Read + Write>(stream: &mut S, context: ImapContext) -> Result<ImapContext> {
let mut buf = [0u8; READ_BUFFER_SIZE];
let mut coroutine = ImapStartTls::new(context);
let mut arg: Option<&[u8]> = None;
loop {
match coroutine.resume(arg.take()) {
ImapStartTlsResult::WantsStartTls { context, .. } => return Ok(context),
ImapStartTlsResult::WantsRead => {
let n = stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapStartTlsResult::WantsWrite(bytes) => {
stream.write_all(&bytes)?;
arg = None;
}
ImapStartTlsResult::Err { err, .. } => bail!(err),
}
}
}
impl ImapSession {
pub fn new(url: Url, tls: Tls, starttls: bool, mut sasl: Sasl) -> Result<Self> {
info!("connecting to IMAP server using {url}");
let context = ImapContext::new();
let host = url.host_str().unwrap_or("127.0.0.1");
let (mut context, mut stream) = match url.scheme() {
scheme if scheme.eq_ignore_ascii_case("imap") => {
let port = url.port().unwrap_or(143);
let mut tcp = TcpStream::connect((host, port))?;
let context = drive_greeting_with_capability(&mut tcp, context)?;
(context, Stream::Tcp(tcp))
}
scheme if scheme.eq_ignore_ascii_case("imaps") => {
let port = url.port().unwrap_or(993);
let mut tcp = TcpStream::connect((host, port))?;
let context = if starttls {
drive_starttls(&mut tcp, context)?
} else {
context
};
let mut stream = upgrade_tls(host, tcp, &tls, &[b"imap"])?;
let context = if starttls {
drive_capability(&mut stream, context)?
} else {
drive_greeting_with_capability(&mut stream, context)?
};
(context, stream)
}
scheme if scheme.eq_ignore_ascii_case("unix") => {
let sock_path = url.path();
let mut unix = UnixStream::connect(sock_path)?;
let context = drive_greeting_with_capability(&mut unix, context)?;
(context, Stream::Unix(unix))
}
scheme => {
bail!("Unknown scheme {scheme}, expected imap, imaps or unix");
}
};
if !context.authenticated {
let ir = context.capability.contains(&Capability::SaslIr);
let mechanism = sasl
.mechanism
.or(Some(SaslMechanism::Plain).filter(|_| sasl.plain.is_some()))
.or(Some(SaslMechanism::Login).filter(|_| sasl.login.is_some()));
match mechanism {
None => bail!("no SASL mechanism configured"),
Some(SaslMechanism::Login) => {
let Some(auth) = sasl.login.take() else {
bail!("missing SASL LOGIN configuration");
};
let mut buf = [0u8; READ_BUFFER_SIZE];
let mut coroutine = ImapSessionLogin::new(
context,
ImapSessionLoginParams::new(auth.username, auth.password)?,
);
let mut arg: Option<&[u8]> = None;
context = loop {
match coroutine.resume(arg.take()) {
ImapSessionLoginResult::Ok(c) => break c,
ImapSessionLoginResult::WantsRead => {
let n = stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapSessionLoginResult::WantsWrite(bytes) => {
stream.write_all(&bytes)?;
arg = None;
}
ImapSessionLoginResult::Err { err, .. } => bail!(err),
}
};
}
Some(SaslMechanism::Plain) => {
let Some(auth) = sasl.plain.take() else {
bail!("missing SASL PLAIN configuration");
};
let mut buf = [0u8; READ_BUFFER_SIZE];
let mut coroutine = ImapSessionAuthenticatePlain::new(
context,
ImapSessionAuthenticatePlainParams::new(
auth.authzid,
auth.authcid,
auth.passwd,
ir,
),
);
let mut arg: Option<&[u8]> = None;
context = loop {
match coroutine.resume(arg.take()) {
ImapSessionAuthenticatePlainResult::Ok(c) => break c,
ImapSessionAuthenticatePlainResult::WantsRead => {
let n = stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapSessionAuthenticatePlainResult::WantsWrite(bytes) => {
stream.write_all(&bytes)?;
arg = None;
}
ImapSessionAuthenticatePlainResult::Err { err, .. } => bail!(err),
}
};
}
Some(SaslMechanism::Anonymous) => {
unimplemented!("ANONYMOUS SASL mechanism not yet implemented")
}
}
}
Ok(Self { context, stream })
}
}