/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * 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> + Send; fn handle_autodiscover_request( &self, body: Option>, ) -> impl Future> + Send; fn autoconfig_parameters<'x>( &'x self, emailaddress: &'x str, fail_if_invalid: bool, ) -> impl Future> + Send; } impl Autoconfig for Server { async fn handle_autoconfig_request(&self, req: &HttpRequest) -> trc::Result { // 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("\n"); config.push_str("\n"); let _ = writeln!(&mut config, "\t"); let _ = writeln!(&mut config, "\t\t{domain}"); let _ = writeln!(&mut config, "\t\t{emailaddress}"); let _ = writeln!( &mut config, "\t\t{domain}" ); 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{server_name}"); let _ = writeln!(&mut config, "\t\t\t{port}"); let _ = writeln!( &mut config, "\t\t\t{}", if is_tls { "SSL" } else { "STARTTLS" } ); let _ = writeln!(&mut config, "\t\t\t{account_name}"); let _ = writeln!( &mut config, "\t\t\tpassword-cleartext" ); let _ = writeln!(&mut config, "\t\t"); } config.push_str("\t\n"); let _ = writeln!( &mut config, "\t" ); config.push_str("\n"); Ok( Resource::new("application/xml; charset=utf-8", config.into_bytes()) .into_http_response(), ) } async fn handle_autodiscover_request( &self, body: Option>, ) -> trc::Result { // 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, ""); let _ = writeln!( &mut config, "" ); let _ = writeln!( &mut config, "\t" ); let _ = writeln!(&mut config, "\t\t"); let _ = writeln!( &mut config, "\t\t\t{emailaddress}" ); let _ = writeln!( &mut config, "\t\t\t{emailaddress}" ); // 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\t644560b8-a1ce-429c-8ace-23395843f701" ); let _ = writeln!(&mut config, "\t\t"); let _ = writeln!(&mut config, "\t\t"); let _ = writeln!(&mut config, "\t\t\temail"); let _ = writeln!(&mut config, "\t\t\tsettings"); 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"); let _ = writeln!( &mut config, "\t\t\t\t{}", protocol.to_uppercase() ); let _ = writeln!(&mut config, "\t\t\t\t{server_name}"); let _ = writeln!(&mut config, "\t\t\t\t{port}"); let _ = writeln!(&mut config, "\t\t\t\t{account_name}"); let _ = writeln!(&mut config, "\t\t\t\ton"); let _ = writeln!(&mut config, "\t\t\t\t0"); let _ = writeln!(&mut config, "\t\t\t\t0"); let _ = writeln!( &mut config, "\t\t\t\t{}", if is_tls { "on" } else { "off" } ); if is_tls { let _ = writeln!(&mut config, "\t\t\t\tTLS"); } let _ = writeln!(&mut config, "\t\t\t\toff"); let _ = writeln!(&mut config, "\t\t\t"); } let _ = writeln!(&mut config, "\t\t"); let _ = writeln!(&mut config, "\t"); let _ = writeln!(&mut config, ""); 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 { 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#" email@example.com http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a "#; assert_eq!( super::parse_autodiscover_request(r.as_bytes()).unwrap(), "email@example.com" ); } }