feat: make alpn customizable per protocol

Refs: #670
This commit is contained in:
Clément DOUIN
2026-06-01 20:47:41 +02:00
parent 7d81d73043
commit e377aede15
11 changed files with 84 additions and 50 deletions
+5 -9
View File
@@ -114,13 +114,12 @@ fn check_imap(
imap_config: crate::config::ImapConfig,
) -> BackendCheck {
use io_imap::client::ImapClientStd;
use pimalaya_stream::{sasl::Sasl, tls::Tls};
use pimalaya_stream::sasl::Sasl;
use crate::imap::id::resolve_auto_id_params;
let result = (|| -> Result<()> {
let mut tls: Tls = imap_config.tls.clone().into();
tls.rustls.alpn = vec!["imap".into()];
let tls = imap_config.tls.clone().into_tls(imap_config.alpn.clone());
let sasl: Option<Sasl> = imap_config.sasl.clone().map(Sasl::try_from).transpose()?;
let auto_id = resolve_auto_id_params(&imap_config.id)?;
let server = crate::imap::client::parse_imap_server(&imap_config.server)?;
@@ -138,13 +137,11 @@ fn check_jmap(
jmap_config: crate::config::JmapConfig,
) -> BackendCheck {
use io_jmap::client::JmapClientStd;
use pimalaya_stream::tls::Tls;
use crate::jmap::client::{jmap_http_auth, parse_server_url};
let result = (|| -> Result<()> {
let mut tls: Tls = jmap_config.tls.clone().into();
tls.rustls.alpn = vec!["http/1.1".into()];
let tls = jmap_config.tls.clone().into_tls(jmap_config.alpn.clone());
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)?;
@@ -183,11 +180,10 @@ fn check_smtp(
use std::net::Ipv4Addr;
use io_smtp::{client::SmtpClientStd, rfc5321::types::ehlo_domain::EhloDomain};
use pimalaya_stream::{sasl::Sasl, tls::Tls};
use pimalaya_stream::sasl::Sasl;
let result = (|| -> Result<()> {
let mut tls: Tls = smtp_config.tls.clone().into();
tls.rustls.alpn = vec!["smtp".into()];
let tls = smtp_config.tls.clone().into_tls(smtp_config.alpn.clone());
let sasl: Option<Sasl> = smtp_config.sasl.clone().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)?;
+32 -6
View File
@@ -331,6 +331,13 @@ pub struct ImapConfig {
#[serde(default)]
pub starttls: bool,
/// ALPN protocol identifiers offered during the TLS handshake.
/// Defaults to `["imap"]` (RFC 7595, IANA registry). Set to `[]`
/// to skip ALPN negotiation entirely. Only relevant for the
/// rustls provider; `native-tls` ignores ALPN.
#[serde(default = "io_imap::client::default_alpn")]
pub alpn: Vec<String>,
/// Optional SASL credentials. When omitted, the connection skips
/// authentication entirely (no `AUTHENTICATE` command is sent);
/// to advertise the ANONYMOUS mechanism explicitly, set
@@ -397,6 +404,13 @@ pub struct SmtpConfig {
#[serde(default)]
pub starttls: bool,
/// ALPN protocol identifiers offered during the TLS handshake.
/// Defaults to `["smtp"]` (RFC 7595, IANA registry). Set to `[]`
/// to skip ALPN negotiation entirely. Only relevant for the
/// rustls provider; `native-tls` ignores ALPN.
#[serde(default = "io_smtp::client::default_alpn")]
pub alpn: Vec<String>,
/// Optional SASL credentials. See [`ImapConfig::sasl`].
pub sasl: Option<SaslConfig>,
}
@@ -434,21 +448,26 @@ pub enum RustlsCryptoConfig {
Ring,
}
impl From<TlsConfig> for Tls {
fn from(config: TlsConfig) -> Self {
impl TlsConfig {
/// Builds the runtime [`Tls`] handle the connect helpers expect.
/// `alpn` is the protocol-level ALPN list (e.g. `["imap"]`,
/// `["smtp"]`, `["http/1.1"]`); pass an empty vec to skip ALPN.
/// The TOML schema never exposes `tls.rustls.alpn` directly: the
/// per-protocol `*.alpn` field is folded in here.
pub fn into_tls(self, alpn: Vec<String>) -> Tls {
Tls {
provider: config.provider.map(|config| match config {
provider: self.provider.map(|p| match p {
TlsProviderConfig::Rustls => TlsProvider::Rustls,
TlsProviderConfig::NativeTls => TlsProvider::NativeTls,
}),
rustls: Rustls {
crypto: config.rustls.crypto.map(|config| match config {
crypto: self.rustls.crypto.map(|c| match c {
RustlsCryptoConfig::Aws => RustlsCrypto::Aws,
RustlsCryptoConfig::Ring => RustlsCrypto::Ring,
}),
alpn: Vec::new(),
alpn,
},
cert: config.cert,
cert: self.cert,
}
}
}
@@ -590,6 +609,13 @@ pub struct JmapConfig {
#[serde(default)]
pub tls: TlsConfig,
/// ALPN protocol identifiers offered during the TLS handshake.
/// Defaults to `["http/1.1"]` (JMAP rides on HTTP/1.1). Set to
/// `[]` to skip ALPN negotiation entirely. Only relevant for the
/// rustls provider; `native-tls` ignores ALPN.
#[serde(default = "io_jmap::client::default_alpn")]
pub alpn: Vec<String>,
/// Authentication configuration.
pub auth: JmapAuthConfig,
+2 -3
View File
@@ -30,7 +30,7 @@ use std::{
use anyhow::{Result, anyhow};
use io_imap::client::ImapClientStd as Inner;
use pimalaya_config::toml::TomlConfig;
use pimalaya_stream::{sasl::Sasl, tls::Tls};
use pimalaya_stream::sasl::Sasl;
use url::Url;
use crate::{
@@ -48,8 +48,7 @@ impl ImapClient {
/// discarded; IMAP-specific subcommands that need it should call
/// [`Inner::capability`] explicitly.
pub fn new(config: ImapConfig) -> Result<Self> {
let mut tls: Tls = config.tls.into();
tls.rustls.alpn = vec!["imap".into()];
let tls = config.tls.into_tls(config.alpn);
let sasl: Option<Sasl> = config.sasl.map(Sasl::try_from).transpose()?;
let auto_id = resolve_auto_id_params(&config.id)?;
let server = parse_imap_server(&config.server)?;
+1 -3
View File
@@ -31,7 +31,6 @@ use anyhow::{Result, anyhow};
use base64::{Engine, prelude::BASE64_STANDARD};
use io_jmap::client::JmapClientStd as Inner;
use pimalaya_config::toml::TomlConfig;
use pimalaya_stream::tls::Tls;
use secrecy::{ExposeSecret, SecretString};
use url::Url;
@@ -54,8 +53,7 @@ impl JmapClient {
/// Establishes the JMAP session (TLS, `/.well-known/jmap`
/// discovery).
pub fn new(config: JmapConfig) -> Result<Self> {
let mut tls: Tls = config.tls.clone().into();
tls.rustls.alpn = vec!["http/1.1".into()];
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)?;
+5 -3
View File
@@ -22,7 +22,6 @@ use io_jmap::{
rfc8621::{capabilities::MAIL, email::EmailProperty},
};
use pimalaya_cli::printer::{Message, Printer};
use pimalaya_stream::tls::Tls;
use url::Url;
use crate::jmap::client::{JmapClient, jmap_http_auth};
@@ -68,8 +67,11 @@ impl JmapEmailExportCommand {
let data = if same_authority(&api_url, &download_url) {
client.blob_download(&download_url)?
} else {
let mut tls: Tls = client.config.tls.clone().into();
tls.rustls.alpn = vec!["http/1.1".into()];
let tls = client
.config
.tls
.clone()
.into_tls(client.config.alpn.clone());
let http_auth = jmap_http_auth(client.config.auth.clone())?;
let mut download_client = JmapClientStd::connect(&download_url, &tls, http_auth)?;
download_client.blob_download(&download_url)?
+5 -3
View File
@@ -24,7 +24,6 @@ use io_jmap::{
rfc8621::{capabilities::MAIL, email::EmailImport},
};
use pimalaya_cli::printer::{Message, Printer};
use pimalaya_stream::tls::Tls;
use url::Url;
use crate::{
@@ -84,8 +83,11 @@ impl JmapEmailImportCommand {
.blob_upload(&upload_url, "message/rfc822", data)?
.blob_id
} else {
let mut tls: Tls = client.config.tls.clone().into();
tls.rustls.alpn = vec!["http/1.1".into()];
let tls = client
.config
.tls
.clone()
.into_tls(client.config.alpn.clone());
let http_auth = jmap_http_auth(client.config.auth.clone())?;
let mut upload_client = JmapClientStd::connect(&upload_url, &tls, http_auth)?;
upload_client
+5 -11
View File
@@ -60,12 +60,9 @@ impl EmailClient {
#[cfg(feature = "jmap")]
if backend.allows_jmap() {
if let Some(jmap_config) = account_config.jmap.take() {
use pimalaya_stream::tls::Tls;
use crate::jmap::client::{jmap_http_auth, parse_server_url};
let mut tls: Tls = jmap_config.tls.clone().into();
tls.rustls.alpn = vec!["http/1.1".into()];
let tls = jmap_config.tls.clone().into_tls(jmap_config.alpn.clone());
let http_auth = jmap_http_auth(jmap_config.auth.clone())?;
let url = parse_server_url(&jmap_config.server)?;
inner = inner.connect_jmap(&url, &tls, http_auth)?;
@@ -76,12 +73,11 @@ impl EmailClient {
if backend.allows_imap() {
if let Some(imap_config) = account_config.imap.take() {
use io_email::imap::client::ImapClientStd;
use pimalaya_stream::{sasl::Sasl, tls::Tls};
use pimalaya_stream::sasl::Sasl;
use crate::imap::id::resolve_auto_id_params;
let mut tls: Tls = imap_config.tls.into();
tls.rustls.alpn = vec!["imap".into()];
let tls = imap_config.tls.into_tls(imap_config.alpn);
let sasl: Option<Sasl> = imap_config.sasl.map(Sasl::try_from).transpose()?;
let auto_id = resolve_auto_id_params(&imap_config.id)?;
let server = crate::imap::client::parse_imap_server(&imap_config.server)?;
@@ -98,7 +94,6 @@ impl EmailClient {
let client = MaildirClient::new(maildir_config.root.to_string_lossy().into_owned());
inner = inner.with_maildir(client);
configured = true;
}
}
@@ -128,10 +123,9 @@ impl EmailClient {
use io_email::smtp::client::SmtpClientStd;
use io_smtp::rfc5321::types::ehlo_domain::EhloDomain;
use pimalaya_stream::{sasl::Sasl, tls::Tls};
use pimalaya_stream::sasl::Sasl;
let mut tls: Tls = smtp_config.tls.into();
tls.rustls.alpn = vec!["smtp".into()];
let tls = smtp_config.tls.into_tls(smtp_config.alpn);
let sasl: Option<Sasl> = 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)?;
+2 -3
View File
@@ -32,7 +32,7 @@ use std::{
use anyhow::{Result, anyhow};
use io_smtp::{client::SmtpClientStd as Inner, rfc5321::types::ehlo_domain::EhloDomain};
use pimalaya_config::toml::TomlConfig;
use pimalaya_stream::{sasl::Sasl, tls::Tls};
use pimalaya_stream::sasl::Sasl;
use url::Url;
use crate::{account::context::Account, cli::load_or_wizard, config::SmtpConfig};
@@ -45,8 +45,7 @@ impl SmtpClient {
/// Opens the SMTP connection (TCP/TLS/STARTTLS, greeting, EHLO,
/// SASL).
pub fn new(config: SmtpConfig) -> Result<Self> {
let mut tls: Tls = config.tls.into();
tls.rustls.alpn = vec!["smtp".into()];
let tls = config.tls.into_tls(config.alpn);
let sasl: Option<Sasl> = config.sasl.map(Sasl::try_from).transpose()?;
let domain: EhloDomain<'static> = Ipv4Addr::new(127, 0, 0, 1).into();
let server = parse_smtp_server(&config.server)?;
+3
View File
@@ -48,6 +48,7 @@ pub fn imap_to_config(w: WizardImapConfig) -> Result<ImapConfig> {
server,
tls: Default::default(),
starttls,
alpn: io_imap::client::default_alpn(),
sasl,
id: Default::default(),
})
@@ -66,6 +67,7 @@ pub fn smtp_to_config(w: WizardSmtpConfig) -> Result<SmtpConfig> {
server,
tls: Default::default(),
starttls,
alpn: io_smtp::client::default_alpn(),
sasl,
})
}
@@ -84,6 +86,7 @@ pub fn jmap_to_config(w: WizardJmapConfig) -> Result<JmapConfig> {
Ok(JmapConfig {
server: w.server,
tls: Default::default(),
alpn: io_jmap::client::default_alpn(),
auth,
identity_id: None,
drafts_mailbox_id: None,