diff --git a/Cargo.lock b/Cargo.lock index aea09ea9..5e9e5727 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1061,7 +1061,7 @@ dependencies = [ [[package]] name = "io-email" version = "0.0.1" -source = "git+https://github.com/pimalaya/io-email#8bcdf92f8872f8436d8482814965e5750d87c4cc" +source = "git+https://github.com/pimalaya/io-email#15aeedd215b5edf463e8b3cae8bbd4ae223e5cff" dependencies = [ "chrono", "chumsky", diff --git a/src/shared/client.rs b/src/shared/client.rs index 820893b0..ee40939c 100644 --- a/src/shared/client.rs +++ b/src/shared/client.rs @@ -24,16 +24,17 @@ //! [`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). 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`. +//! Construction picks the first storage backend (`jmap → imap → +//! maildir`) allowed by the `BackendFlag` that is configured on the +//! account. When the account also has SMTP configured, an SMTP slot +//! is registered on the same client so `send_message` works for +//! IMAP/Maildir accounts; JMAP accounts already send via JMAP +//! submission. SMTP connection failures are logged and skipped — the +//! client still opens for reading. use std::ops::{Deref, DerefMut}; -use anyhow::{bail, Result}; +use anyhow::{Result, bail}; use crate::{ account::context::Account, @@ -47,25 +48,24 @@ pub struct EmailClient { } impl EmailClient { - /// Loads the configuration, picks the active account, builds the - /// 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: Config, mut account_config: AccountConfig, backend: Backend, ) -> Result { + use io_email::client::EmailClientStd; + + let mut inner = EmailClientStd::new(); + let mut configured = false; + #[cfg(feature = "jmap")] - if backend.allows_jmap() { + if !configured && backend.allows_jmap() { 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())?; @@ -73,21 +73,17 @@ impl EmailClient { let mut client = JmapClientStd::connect(&url, &tls, http_auth)?; client.session_get(&url)?; - return Ok(Self { - inner: EmailClientStd::Jmap(client), - account, - }); + inner = inner.with_jmap(client); + configured = true; } } #[cfg(feature = "imap")] - if backend.allows_imap() { + if !configured && backend.allows_imap() { 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: Option = imap_config.sasl.map(Sasl::try_from).transpose()?; @@ -95,30 +91,64 @@ impl EmailClient { let client = ImapClientStd::::connect(&server, &tls, imap_config.starttls, sasl)?; - return Ok(Self { - inner: EmailClientStd::Imap(client), - account, - }); + inner = inner.with_imap(client); + configured = true; } } #[cfg(feature = "maildir")] - if backend.allows_maildir() { + if !configured && backend.allows_maildir() { 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, - }); + inner = inner.with_maildir(client); + configured = true; } } - bail!("no backend matching `{backend}` is configured for this account") + if !configured { + bail!("no backend matching `{backend}` is configured for this account"); + } + + // Register SMTP alongside the storage backend so shared + // `send_message` works for IMAP/Maildir accounts. JMAP already + // sends via submission, but if both are present, SMTP wins + // because storage is registered first. + #[cfg(feature = "smtp")] + if let Some(smtp_config) = account_config.smtp.take() { + use std::net::Ipv4Addr; + + use io_smtp::{client::SmtpClientStd, rfc5321::types::ehlo_domain::EhloDomain}; + use pimalaya_stream::{sasl::Sasl, std::stream::StreamStd, tls::Tls}; + + let smtp = (|| -> Result> { + let mut tls: Tls = smtp_config.tls.into(); + tls.rustls.alpn = vec!["smtp".into()]; + let sasl: Option = smtp_config.sasl.map(Sasl::try_from).transpose()?; + let domain: EhloDomain<'static> = Ipv4Addr::new(127, 0, 0, 1).into(); + let server = crate::smtp::client::parse_smtp_server(&smtp_config.server)?; + Ok(SmtpClientStd::::connect( + &server, + &tls, + smtp_config.starttls, + domain, + sasl, + )?) + })(); + + match smtp { + Ok(client) => inner = inner.with_smtp(client), + Err(err) => { + log::warn!("SMTP backend disabled: {err}. Sending will be unavailable.") + } + } + } + + let account = Account::from(config).merge(Account::from(account_config)); + + Ok(Self { inner, account }) } }