DNS, DKIM and ACME improvements - part 3

This commit is contained in:
Maurus Decimus
2026-04-02 11:05:52 +02:00
parent d8fa19d105
commit c54ec2397a
22 changed files with 377 additions and 217 deletions

View File

@@ -379,9 +379,7 @@ impl OAuthApiHandler for Server {
.await?;
// Build response
let base_url = HttpContext::new(&session, req)
.resolve_response_url(self)
.await;
let base_url = HttpContext::new(&session, req).resolve_response_url(self);
Ok(JsonResponse::new(DeviceAuthResponse {
verification_uri: format!("{base_url}/authorize"),
verification_uri_complete: format!("{base_url}/authorize/?code={user_code}"),
@@ -399,10 +397,7 @@ impl OAuthApiHandler for Server {
req: HttpRequest,
session: HttpSessionData,
) -> trc::Result<HttpResponse> {
let base_url = HttpContext::new(&session, &req)
.resolve_response_url(self)
.await
.to_string();
let base_url = HttpContext::new(&session, &req).resolve_response_url(self);
Ok(JsonResponse::new(OAuthMetadata {
authorization_endpoint: format!("{base_url}/authorize/code",),

View File

@@ -60,9 +60,7 @@ impl OpenIdHandler for Server {
req: HttpRequest,
session: HttpSessionData,
) -> trc::Result<HttpResponse> {
let base_url = HttpContext::new(&session, &req)
.resolve_response_url(self)
.await;
let base_url = HttpContext::new(&session, &req).resolve_response_url(self);
Ok(JsonResponse::new(OpenIdMetadata {
authorization_endpoint: format!("{base_url}/authorize/code",),

View File

@@ -62,9 +62,7 @@ impl TokenHandler for Server {
let mut response = TokenResponse::error(ErrorType::InvalidGrant);
let issuer = HttpContext::new(&session, req)
.resolve_response_url(self)
.await;
let issuer = HttpContext::new(&session, req).resolve_response_url(self);
if grant_type.eq_ignore_ascii_case("authorization_code") {
response = if let (Some(code), Some(client_id), Some(redirect_uri)) = (

View File

@@ -1,308 +0,0 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::{Server, manager::application::Resource};
use http_proto::*;
use quick_xml::Reader;
use quick_xml::events::Event;
use registry::schema::enums::NetworkListenerProtocol;
use registry::schema::structs::NetworkListener;
use std::fmt::Write;
use std::future::Future;
use utils::url_params::UrlParams;
pub trait Autoconfig: Sync + Send {
fn handle_autoconfig_request(
&self,
req: &HttpRequest,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
fn handle_autodiscover_request(
&self,
body: Option<Vec<u8>>,
) -> impl Future<Output = trc::Result<HttpResponse>> + Send;
}
impl Autoconfig for Server {
async fn handle_autoconfig_request(&self, req: &HttpRequest) -> trc::Result<HttpResponse> {
// Obtain parameters
let params = UrlParams::new(req.uri().query());
let emailaddress = params
.get("emailaddress")
.unwrap_or_default()
.to_lowercase();
let Some((_, domain)) = emailaddress.rsplit_once('@') else {
return Err(trc::ResourceEvent::BadParameters
.into_err()
.details("Missing domain in email address"));
};
let listeners = self.registry().list::<NetworkListener>().await?;
let server_name = &self.core.network.server_name;
// Build XML response
let mut config = String::with_capacity(1024);
config.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
config.push_str("<clientConfig version=\"1.1\">\n");
let _ = writeln!(&mut config, "\t<emailProvider id=\"{domain}\">");
let _ = writeln!(&mut config, "\t\t<domain>{domain}</domain>");
let _ = writeln!(&mut config, "\t\t<displayName>{emailaddress}</displayName>");
let _ = writeln!(
&mut config,
"\t\t<displayShortName>{domain}</displayShortName>"
);
for listener in listeners {
let listener = listener.object;
let Some(port) = listener.bind.as_slice().first().map(|l| l.0.port()) else {
continue;
};
let (protocol, tag) = match listener.protocol {
NetworkListenerProtocol::Smtp if port != 25 => ("smtp", "outgoingServer"),
NetworkListenerProtocol::Imap => ("imap", "incomingServer"),
NetworkListenerProtocol::Pop3 => ("pop3", "incomingServer"),
_ => continue,
};
let _ = writeln!(&mut config, "\t\t<{tag} type=\"{protocol}\">");
let _ = writeln!(&mut config, "\t\t\t<hostname>{server_name}</hostname>");
let _ = writeln!(&mut config, "\t\t\t<port>{port}</port>");
let _ = writeln!(
&mut config,
"\t\t\t<socketType>{}</socketType>",
if listener.tls_implicit {
"SSL"
} else {
"STARTTLS"
}
);
let _ = writeln!(&mut config, "\t\t\t<username>{emailaddress}</username>");
let _ = writeln!(
&mut config,
"\t\t\t<authentication>password-cleartext</authentication>"
);
let _ = writeln!(&mut config, "\t\t</{tag}>");
}
config.push_str("\t</emailProvider>\n");
for (tag, protocol, url) in [
("addressBook", "carddav", "card"),
("calendar", "caldav", "cal"),
("fileShare", "webdav", "file"),
] {
let _ = writeln!(&mut config, "\t<{tag} type=\"{protocol}\">");
let _ = writeln!(&mut config, "\t\t<username>{emailaddress}</username>");
let _ = writeln!(
&mut config,
"\t\t<authentication>http-basic</authentication>"
);
let _ = writeln!(
&mut config,
"\t\t<serverURL>https://{server_name}/dav/{url}</serverURL>"
);
let _ = writeln!(&mut config, "\t</{tag}>");
}
let _ = writeln!(
&mut config,
"\t<clientConfigUpdate url=\"https://autoconfig.{domain}/mail/config-v1.1.xml\"></clientConfigUpdate>"
);
config.push_str("</clientConfig>\n");
Ok(
Resource::new("application/xml; charset=utf-8", config.into_bytes())
.into_http_response(),
)
}
async fn handle_autodiscover_request(
&self,
body: Option<Vec<u8>>,
) -> trc::Result<HttpResponse> {
// Obtain parameters
let emailaddress = parse_autodiscover_request(body.as_deref().unwrap_or_default())
.map_err(|err| {
trc::ResourceEvent::BadParameters
.into_err()
.details("Failed to parse autodiscover request")
.ctx(trc::Key::Reason, err)
})?;
let listeners = self.registry().list::<NetworkListener>().await?;
let server_name = &self.core.network.server_name;
// Build XML response
let mut config = String::with_capacity(1024);
let _ = writeln!(&mut config, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
let _ = writeln!(
&mut config,
"<Autodiscover xmlns=\"http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006\">"
);
let _ = writeln!(
&mut config,
"\t<Response xmlns=\"http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a\">"
);
let _ = writeln!(&mut config, "\t\t<User>");
let _ = writeln!(
&mut config,
"\t\t\t<DisplayName>{emailaddress}</DisplayName>"
);
let _ = writeln!(
&mut config,
"\t\t\t<AutoDiscoverSMTPAddress>{emailaddress}</AutoDiscoverSMTPAddress>"
);
// DeploymentId is a required field of User but we are not a MS Exchange server so use a random value
let _ = writeln!(
&mut config,
"\t\t\t<DeploymentId>644560b8-a1ce-429c-8ace-23395843f701</DeploymentId>"
);
let _ = writeln!(&mut config, "\t\t</User>");
let _ = writeln!(&mut config, "\t\t<Account>");
let _ = writeln!(&mut config, "\t\t\t<AccountType>email</AccountType>");
let _ = writeln!(&mut config, "\t\t\t<Action>settings</Action>");
for listener in listeners {
let listener = listener.object;
let Some(port) = listener.bind.as_slice().first().map(|l| l.0.port()) else {
continue;
};
let protocol = match listener.protocol {
NetworkListenerProtocol::Imap => "IMAP",
NetworkListenerProtocol::Pop3 => "POP3",
NetworkListenerProtocol::Smtp if port != 25 => "SMTP",
_ => continue,
};
let _ = writeln!(&mut config, "\t\t\t<Protocol>");
let _ = writeln!(&mut config, "\t\t\t\t<Type>{protocol}</Type>",);
let _ = writeln!(&mut config, "\t\t\t\t<Server>{server_name}</Server>");
let _ = writeln!(&mut config, "\t\t\t\t<Port>{port}</Port>");
let _ = writeln!(&mut config, "\t\t\t\t<LoginName>{emailaddress}</LoginName>");
let _ = writeln!(&mut config, "\t\t\t\t<AuthRequired>on</AuthRequired>");
let _ = writeln!(&mut config, "\t\t\t\t<DirectoryPort>0</DirectoryPort>");
let _ = writeln!(&mut config, "\t\t\t\t<ReferralPort>0</ReferralPort>");
let _ = writeln!(
&mut config,
"\t\t\t\t<SSL>{}</SSL>",
if listener.tls_implicit { "on" } else { "off" }
);
if listener.tls_implicit {
let _ = writeln!(&mut config, "\t\t\t\t<Encryption>TLS</Encryption>");
}
let _ = writeln!(&mut config, "\t\t\t\t<SPA>off</SPA>");
let _ = writeln!(&mut config, "\t\t\t</Protocol>");
}
let _ = writeln!(&mut config, "\t\t</Account>");
let _ = writeln!(&mut config, "\t</Response>");
let _ = writeln!(&mut config, "</Autodiscover>");
Ok(
Resource::new("application/xml; charset=utf-8", config.into_bytes())
.into_http_response(),
)
}
}
fn parse_autodiscover_request(bytes: &[u8]) -> Result<String, String> {
if bytes.is_empty() {
return Err("Empty request body".to_string());
}
let mut reader = Reader::from_reader(bytes);
reader.config_mut().trim_text(true);
let mut buf = Vec::with_capacity(128);
'outer: for tag_name in ["Autodiscover", "Request", "EMailAddress"] {
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let found_tag_name = e.name();
if tag_name
.as_bytes()
.eq_ignore_ascii_case(found_tag_name.as_ref())
{
continue 'outer;
} else if tag_name == "EMailAddress" {
// Skip unsupported tags under Request, such as AcceptableResponseSchema
let mut tag_count = 0;
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::End(_)) => {
if tag_count == 0 {
break;
} else {
tag_count -= 1;
}
}
Ok(Event::Start(_)) => {
tag_count += 1;
}
Ok(Event::Eof) => {
return Err(format!(
"Expected value, found unexpected EOF at position {}.",
reader.buffer_position()
));
}
_ => (),
}
}
} else {
return Err(format!(
"Expected tag {}, found unexpected tag {} at position {}.",
tag_name,
String::from_utf8_lossy(found_tag_name.as_ref()),
reader.buffer_position()
));
}
}
Ok(Event::Decl(_) | Event::Text(_)) => (),
Err(e) => {
return Err(format!(
"Error at position {}: {:?}",
reader.buffer_position(),
e
));
}
Ok(event) => {
return Err(format!(
"Expected tag {}, found unexpected event {event:?} at position {}.",
tag_name,
reader.buffer_position()
));
}
}
}
}
if let Ok(Event::Text(text)) = reader.read_event_into(&mut buf)
&& let Ok(text) = text.xml_content()
&& text.contains('@')
{
return Ok(text.trim().to_lowercase());
}
Err(format!(
"Expected email address, found unexpected value at position {}.",
reader.buffer_position()
))
}
#[cfg(test)]
mod tests {
#[test]
fn parse_autodiscover() {
let r = r#"<?xml version="1.0" encoding="utf-8"?>
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006">
<Request>
<EMailAddress>email@example.com</EMailAddress>
<AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema>
</Request>
</Autodiscover>"#;
assert_eq!(
super::parse_autodiscover_request(r.as_bytes()).unwrap(),
"email@example.com"
);
}
}

View File

@@ -5,7 +5,6 @@
*/
pub mod auth;
pub mod autoconfig;
pub mod form;
pub mod management;
pub mod request;

View File

@@ -13,7 +13,6 @@ use crate::{
registration::ClientRegistrationHandler, token::TokenHandler,
},
},
autoconfig::Autoconfig,
form::FormHandler,
management::{ManagementApi, ToManageHttpResponse},
};
@@ -200,14 +199,14 @@ impl ParseHttp for Server {
self.authenticate_headers(&req, &session).await?;
self.handle_session_resource(
ctx.resolve_response_url(self).await,
ctx.resolve_response_url(self),
&access_token,
)
.await
.map(|s| s.into_http_response())
} else {
Ok(Session::new(
ctx.resolve_response_url(self).await,
ctx.resolve_response_url(self),
&self.core.jmap.capabilities,
)
.into_http_response())
@@ -323,9 +322,10 @@ impl ParseHttp for Server {
self.is_http_anonymous_request_allowed(session.remote_ip)
.await?;
let todo = "fix autoconfig generation";
return self.handle_autoconfig_request(&req).await;
return self
.handle_autoconfig_request(req.uri().query())
.await
.map(|resource| resource.into_http_response());
}
("autoconfig", &Method::GET) => {
if path.next().unwrap_or_default() == "mail"
@@ -335,7 +335,10 @@ impl ParseHttp for Server {
self.is_http_anonymous_request_allowed(session.remote_ip)
.await?;
return self.handle_autoconfig_request(&req).await;
return self
.handle_autoconfig_request(req.uri().query())
.await
.map(|resource| resource.into_http_response());
}
}
(_, &Method::OPTIONS) => {
@@ -455,7 +458,10 @@ impl ParseHttp for Server {
self.is_http_anonymous_request_allowed(session.remote_ip)
.await?;
return self.handle_autoconfig_request(&req).await;
return self
.handle_autoconfig_request(req.uri().query())
.await
.map(|resource| resource.into_http_response());
}
}
"calendar" => {
@@ -487,7 +493,7 @@ impl ParseHttp for Server {
});
}
}
"autodiscover" | "Autodiscover" => {
"autodiscover" | "Autodiscover" | "AutoDiscover" => {
if req.method() == Method::POST
&& path
.next()
@@ -502,7 +508,27 @@ impl ParseHttp for Server {
.handle_autodiscover_request(
fetch_body(&mut req, 8192, session.session_id).await,
)
.await;
.await
.map(|resource| resource.into_http_response());
} else if req.method() == Method::POST
&& path
.next()
.unwrap_or_default()
.eq_ignore_ascii_case("autodiscover.json")
{
// Limit anonymous requests
self.is_http_anonymous_request_allowed(session.remote_ip)
.await?;
return self
.handle_autodiscover_v2_request(req.uri().query())
.await
.map(|result| match result {
Ok(resource) => resource.into_http_response(),
Err(err) => HttpResponse::new(StatusCode::BAD_REQUEST)
.with_content_type("application/json; charset=utf-8")
.with_text_body(err),
});
}
}
"robots.txt" => {