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
Generated
+9 -9
View File
@@ -1078,7 +1078,7 @@ dependencies = [
[[package]]
name = "io-email"
version = "0.0.1"
source = "git+https://github.com/pimalaya/io-email#5d01da7acb98d8394077d40780f1cded639f4574"
source = "git+https://github.com/pimalaya/io-email#050738cbaf9e177683c7e4eeeb8b2ef145c4abe2"
dependencies = [
"chrono",
"chumsky",
@@ -1103,7 +1103,7 @@ dependencies = [
[[package]]
name = "io-http"
version = "0.0.3"
source = "git+https://github.com/pimalaya/io-http#73b264b85209d86de48afed25957ab582b54c272"
source = "git+https://github.com/pimalaya/io-http#23f3668558059c2db3603b1afdda5ce0f840e46a"
dependencies = [
"anyhow",
"base64",
@@ -1119,7 +1119,7 @@ dependencies = [
[[package]]
name = "io-imap"
version = "0.0.1"
source = "git+https://github.com/pimalaya/io-imap#518da423e551684fa8202a9cc4fede99f1be3f23"
source = "git+https://github.com/pimalaya/io-imap#5c915ae5536e5e5ac48f3284c1827111a71939f7"
dependencies = [
"anyhow",
"base64",
@@ -1134,7 +1134,7 @@ dependencies = [
[[package]]
name = "io-jmap"
version = "0.0.1"
source = "git+https://github.com/pimalaya/io-jmap#2463ecb104d0ff24b8c3ba2f4eaa09562ce5a1cd"
source = "git+https://github.com/pimalaya/io-jmap#18103bdbf883798b48566f6d45e4dfed8458d0e6"
dependencies = [
"anyhow",
"io-http",
@@ -1170,7 +1170,7 @@ dependencies = [
[[package]]
name = "io-smtp"
version = "0.0.1"
source = "git+https://github.com/pimalaya/io-smtp#3b8cf0d806c23339a46a540e159de3d4cd9898f3"
source = "git+https://github.com/pimalaya/io-smtp#8ade022516b83358d66772b74bd42216763b5ec3"
dependencies = [
"anyhow",
"base64",
@@ -1664,7 +1664,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pimalaya-cli"
version = "0.0.1"
source = "git+https://github.com/pimalaya/cli#313c2e999ea9157fa42c84a6ae867451d8308114"
source = "git+https://github.com/pimalaya/cli#64cd78d702ed9251021f4ed8f549ac262b0e6985"
dependencies = [
"anyhow",
"clap",
@@ -1704,7 +1704,7 @@ dependencies = [
[[package]]
name = "pimalaya-stream"
version = "0.0.1"
source = "git+https://github.com/pimalaya/stream#6449136364c9ef81248e3b4ab6491dd154293481"
source = "git+https://github.com/pimalaya/stream#152911be92647ae47c2052c1bf1270f02eefbdd1"
dependencies = [
"anyhow",
"log",
@@ -2484,9 +2484,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-segmentation"
version = "1.13.2"
version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
[[package]]
name = "unicode-width"
+15
View File
@@ -165,6 +165,11 @@ imap.server = "example.com"
# Enable STARTTLS (only valid when the server resolves to `imap://`).
#imap.starttls = false
# ALPN protocol identifiers offered during the TLS handshake (rustls only;
# native-tls ignores it). Defaults to ["imap"]; set to [] to skip ALPN.
#imap.alpn = ["imap"]
#imap.alpn = []
# Pick exactly one SASL mechanism among `anonymous`, `login`, `plain`,
# `oauthbearer`, `xoauth2`, `scram-sha-256`. Omit the whole `imap.sasl` table
# to skip authentication entirely (no `AUTHENTICATE` command sent).
@@ -231,6 +236,11 @@ imap.sasl.plain.passwd.raw = "***"
#jmap.tls.rustls.crypto = "ring"
#jmap.tls.cert = "/path/to/custom/cert.pem"
# ALPN protocol identifiers offered during the TLS handshake. Defaults to
# ["http/1.1"] (JMAP rides on HTTP/1.1); set to [] to skip ALPN.
#jmap.alpn = ["http/1.1"]
#jmap.alpn = []
# Pick exactly one of `header`, `bearer`, `basic`.
# Raw `Authorization` header value, used verbatim.
@@ -289,6 +299,11 @@ smtp.server = "example.com"
# Enable STARTTLS (only valid when the server resolves to `smtp://`).
#smtp.starttls = false
# ALPN protocol identifiers offered during the TLS handshake. Defaults to
# ["smtp"]; set to [] to skip ALPN.
#smtp.alpn = ["smtp"]
#smtp.alpn = []
# Pick exactly one SASL mechanism among `anonymous`, `login`, `plain`,
# `oauthbearer`, `xoauth2`, `scram-sha-256`. Omit the whole `smtp.sasl` table
# to skip authentication entirely.
+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,