mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2025-12-13 07:16:23 +08:00
325 lines
12 KiB
Rust
325 lines
12 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
|
*/
|
|
|
|
use common::{Server, manager::webadmin::Resource};
|
|
use directory::QueryParams;
|
|
use http_proto::*;
|
|
use quick_xml::Reader;
|
|
use quick_xml::events::Event;
|
|
use std::fmt::Write;
|
|
use std::future::Future;
|
|
use trc::AddContext;
|
|
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;
|
|
fn autoconfig_parameters<'x>(
|
|
&'x self,
|
|
emailaddress: &'x str,
|
|
fail_if_invalid: bool,
|
|
) -> impl Future<Output = trc::Result<(String, String, &'x str)>> + 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 (account_name, server_name, domain) =
|
|
self.autoconfig_parameters(&emailaddress, false).await?;
|
|
let services = self.core.storage.config.get_services().await?;
|
|
|
|
// 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 (protocol, port, is_tls) in services {
|
|
let tag = match protocol.as_str() {
|
|
"imap" | "pop3" => "incomingServer",
|
|
"smtp" if port != 25 => "outgoingServer",
|
|
_ => 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 is_tls { "SSL" } else { "STARTTLS" }
|
|
);
|
|
let _ = writeln!(&mut config, "\t\t\t<username>{account_name}</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");
|
|
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 (account_name, server_name, _) =
|
|
self.autoconfig_parameters(&emailaddress, true).await?;
|
|
let services = self.core.storage.config.get_services().await?;
|
|
|
|
// 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 (protocol, port, is_tls) in services {
|
|
match protocol.as_str() {
|
|
"imap" | "pop3" => (),
|
|
"smtp" if port != 25 => (),
|
|
_ => continue,
|
|
}
|
|
|
|
let _ = writeln!(&mut config, "\t\t\t<Protocol>");
|
|
let _ = writeln!(
|
|
&mut config,
|
|
"\t\t\t\t<Type>{}</Type>",
|
|
protocol.to_uppercase()
|
|
);
|
|
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>{account_name}</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 is_tls { "on" } else { "off" }
|
|
);
|
|
if is_tls {
|
|
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(),
|
|
)
|
|
}
|
|
|
|
async fn autoconfig_parameters<'x>(
|
|
&'x self,
|
|
emailaddress: &'x str,
|
|
fail_if_invalid: bool,
|
|
) -> trc::Result<(String, String, &'x str)> {
|
|
// Return EMAILADDRESS
|
|
let Some((_, domain)) = emailaddress.rsplit_once('@') else {
|
|
return if !fail_if_invalid {
|
|
Ok((
|
|
"%EMAILADDRESS%".to_string(),
|
|
self.core.network.server_name.clone(),
|
|
&self.core.network.report_domain,
|
|
))
|
|
} else {
|
|
Err(trc::ResourceEvent::BadParameters
|
|
.into_err()
|
|
.details("Missing domain in email address"))
|
|
};
|
|
};
|
|
|
|
// Find the account name by e-mail address
|
|
let mut account_name = emailaddress.into();
|
|
if let Some(id) = self
|
|
.core
|
|
.storage
|
|
.directory
|
|
.email_to_id(emailaddress)
|
|
.await
|
|
.caused_by(trc::location!())?
|
|
&& let Ok(Some(principal)) = self
|
|
.core
|
|
.storage
|
|
.directory
|
|
.query(QueryParams::id(id).with_return_member_of(false))
|
|
.await
|
|
&& principal
|
|
.primary_email()
|
|
.is_some_and(|email| email.eq_ignore_ascii_case(emailaddress))
|
|
{
|
|
account_name = principal.name;
|
|
}
|
|
|
|
Ok((account_name, self.core.network.server_name.clone(), domain))
|
|
}
|
|
}
|
|
|
|
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"
|
|
);
|
|
}
|
|
}
|