/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL * * Modified by Coffey Labs in 2026 for INBUXA. */ use crate::{Server, manager::application::Resource, network::legacy::is_legacy_service}; use quick_xml::Reader; use quick_xml::XmlVersion; use quick_xml::events::Event; use registry::schema::enums::ServiceProtocol; use std::fmt::Write; impl Server { pub 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 default_host = &self.core.network.server_name; // 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"); // inbuxa: legacy-protocols LP-7, LP-14a let legacy_off = match emailaddress.rsplit_once('@') { Some((_, domain)) => self.legacy_protocols_off_for(domain).await?, None => self.legacy_protocols_off_for("").await?, }; for (protocol, service) in &self.core.network.info.services { if legacy_off && is_legacy_service(protocol) { continue; } let (protocol, ports) = match protocol { ServiceProtocol::Imap => ("IMAP", [143, 993]), ServiceProtocol::Pop3 => ("POP3", [110, 995]), ServiceProtocol::Smtp => ("SMTP", [587, 465]), _ => continue, }; for (is_tls, port) in ports.into_iter().enumerate() { if is_tls == 1 || service.cleartext { let server_name = service.hostname.as_deref().unwrap_or(default_host); let _ = writeln!(&mut config, "\t\t\t"); let _ = writeln!(&mut config, "\t\t\t\t{protocol}",); 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{emailaddress}"); 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 == 1 { "on" } else { "off" } ); if is_tls == 1 { 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(), )) } } 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(XmlVersion::Implicit1_0) && 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" ); } }