mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2024-12-29 19:51:10 +08:00
Autoconfig and MS Autodiscover services (closes #336)
This commit is contained in:
parent
baef85e55b
commit
1473d8cdcf
18 changed files with 411 additions and 73 deletions
23
Cargo.lock
generated
23
Cargo.lock
generated
|
@ -986,7 +986,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "common"
|
||||
version = "0.7.3"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"ahash 0.8.11",
|
||||
"arc-swap",
|
||||
|
@ -1498,7 +1498,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "directory"
|
||||
version = "0.7.3"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"ahash 0.8.11",
|
||||
"argon2",
|
||||
|
@ -2702,7 +2702,7 @@ checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284"
|
|||
|
||||
[[package]]
|
||||
name = "imap"
|
||||
version = "0.7.3"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"ahash 0.8.11",
|
||||
"common",
|
||||
|
@ -2898,7 +2898,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "jmap"
|
||||
version = "0.7.3"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
|
@ -2930,6 +2930,7 @@ dependencies = [
|
|||
"nlp",
|
||||
"p256",
|
||||
"pkcs8",
|
||||
"quick-xml 0.31.0",
|
||||
"rand",
|
||||
"rasn",
|
||||
"rasn-cms",
|
||||
|
@ -3313,7 +3314,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "mail-server"
|
||||
version = "0.7.3"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"common",
|
||||
"directory",
|
||||
|
@ -3331,7 +3332,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "managesieve"
|
||||
version = "0.7.3"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"ahash 0.8.11",
|
||||
"bincode",
|
||||
|
@ -3608,7 +3609,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "nlp"
|
||||
version = "0.7.3"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"ahash 0.8.11",
|
||||
"bincode",
|
||||
|
@ -5646,7 +5647,7 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
|||
|
||||
[[package]]
|
||||
name = "smtp"
|
||||
version = "0.7.3"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"ahash 0.8.11",
|
||||
"bincode",
|
||||
|
@ -5762,7 +5763,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "stalwart-cli"
|
||||
version = "0.7.3"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"console",
|
||||
|
@ -5793,7 +5794,7 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
|||
|
||||
[[package]]
|
||||
name = "store"
|
||||
version = "0.7.3"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"ahash 0.8.11",
|
||||
"arc-swap",
|
||||
|
@ -6636,7 +6637,7 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
|||
|
||||
[[package]]
|
||||
name = "utils"
|
||||
version = "0.7.3"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"ahash 0.8.11",
|
||||
"base64 0.22.0",
|
||||
|
|
|
@ -5,7 +5,7 @@ authors = ["Stalwart Labs Ltd. <hello@stalw.art>"]
|
|||
license = "AGPL-3.0-only"
|
||||
repository = "https://github.com/stalwartlabs/cli"
|
||||
homepage = "https://github.com/stalwartlabs/cli"
|
||||
version = "0.7.3"
|
||||
version = "0.8.0"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
resolver = "2"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "common"
|
||||
version = "0.7.3"
|
||||
version = "0.8.0"
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
|
|
|
@ -448,7 +448,12 @@ impl ConfigManager {
|
|||
}
|
||||
}
|
||||
|
||||
result.sort_unstable();
|
||||
// Sort by name, then tls and finally port
|
||||
result.sort_unstable_by(|a, b| {
|
||||
a.0.cmp(&b.0)
|
||||
.then_with(|| b.2.cmp(&a.2))
|
||||
.then_with(|| a.1.cmp(&b.1))
|
||||
});
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "directory"
|
||||
version = "0.7.3"
|
||||
version = "0.8.0"
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "imap"
|
||||
version = "0.7.3"
|
||||
version = "0.8.0"
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "jmap"
|
||||
version = "0.7.3"
|
||||
version = "0.8.0"
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
|
@ -56,6 +56,7 @@ async-trait = "0.1.68"
|
|||
lz4_flex = { version = "0.11", default-features = false }
|
||||
rev_lines = "0.3.0"
|
||||
x509-parser = "0.16.0"
|
||||
quick-xml = "0.31"
|
||||
|
||||
[dev-dependencies]
|
||||
ece = "2.2"
|
||||
|
|
317
crates/jmap/src/api/autoconfig.rs
Normal file
317
crates/jmap/src/api/autoconfig.rs
Normal file
|
@ -0,0 +1,317 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of Stalwart Mail Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use std::fmt::Write;
|
||||
|
||||
use common::manager::webadmin::Resource;
|
||||
use directory::QueryBy;
|
||||
use jmap_proto::error::request::RequestError;
|
||||
use quick_xml::events::Event;
|
||||
use quick_xml::Reader;
|
||||
use utils::url_params::UrlParams;
|
||||
|
||||
use crate::{api::http::ToHttpResponse, JMAP};
|
||||
|
||||
use super::{HttpRequest, HttpResponse};
|
||||
|
||||
impl JMAP {
|
||||
pub async fn handle_autoconfig_request(&self, req: &HttpRequest) -> 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) =
|
||||
match self.autoconfig_parameters(&emailaddress).await {
|
||||
Ok(result) => result,
|
||||
Err(err) => return err.into_http_response(),
|
||||
};
|
||||
let services = match self.core.storage.config.get_services().await {
|
||||
Ok(services) => services,
|
||||
Err(err) => return err.into_http_response(),
|
||||
};
|
||||
|
||||
// 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<authentication>password-cleartext</authentication>"
|
||||
);
|
||||
let _ = writeln!(&mut config, "\t\t\t<username>{account_name}</username>");
|
||||
let _ = writeln!(&mut config, "\t\t</{tag}>");
|
||||
}
|
||||
|
||||
config.push_str("\t</emailProvider>\n");
|
||||
let _ = writeln!(
|
||||
&mut config,
|
||||
"\t<clientConfigUpdate url=\"https://autoconfig.{domain}/.well-known/mail-v1.xml\"/>"
|
||||
);
|
||||
config.push_str("</clientConfig>\n");
|
||||
|
||||
Resource {
|
||||
content_type: "text/xml+autoconfig; charset=utf-8",
|
||||
contents: config.into_bytes(),
|
||||
}
|
||||
.into_http_response()
|
||||
}
|
||||
|
||||
pub async fn handle_autodiscover_request(&self, body: Option<Vec<u8>>) -> HttpResponse {
|
||||
// Obtain parameters
|
||||
let emailaddress = match parse_autodiscover_request(body.as_deref().unwrap_or_default()) {
|
||||
Ok(emailaddress) => emailaddress,
|
||||
Err(err) => {
|
||||
return RequestError::blank(400, "Failed to parse autodiscover request", err)
|
||||
.into_http_response()
|
||||
}
|
||||
};
|
||||
let (account_name, server_name, _) = match self.autoconfig_parameters(&emailaddress).await {
|
||||
Ok(result) => result,
|
||||
Err(err) => return err.into_http_response(),
|
||||
};
|
||||
let services = match self.core.storage.config.get_services().await {
|
||||
Ok(services) => services,
|
||||
Err(err) => return err.into_http_response(),
|
||||
};
|
||||
|
||||
// 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>");
|
||||
|
||||
Resource {
|
||||
content_type: "text/xml; charset=utf-8",
|
||||
contents: config.into_bytes(),
|
||||
}
|
||||
.into_http_response()
|
||||
}
|
||||
|
||||
async fn autoconfig_parameters<'x>(
|
||||
&self,
|
||||
emailaddress: &'x str,
|
||||
) -> Result<(String, String, &'x str), RequestError> {
|
||||
let domain = if let Some((_, domain)) = emailaddress.rsplit_once('@') {
|
||||
domain
|
||||
} else {
|
||||
return Err(RequestError::invalid_parameters());
|
||||
};
|
||||
|
||||
// Obtain server name
|
||||
let server_name = if let Ok(Some(server_name)) = self
|
||||
.core
|
||||
.storage
|
||||
.config
|
||||
.get("lookup.default.hostname")
|
||||
.await
|
||||
{
|
||||
server_name
|
||||
} else {
|
||||
tracing::error!("Autoconfig request failed: Server name not configured");
|
||||
return Err(RequestError::internal_server_error());
|
||||
};
|
||||
|
||||
// Find the account name by e-mail address
|
||||
let mut account_name = emailaddress.to_string();
|
||||
for id in self
|
||||
.core
|
||||
.storage
|
||||
.directory
|
||||
.email_to_ids(emailaddress)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
{
|
||||
if let Ok(Some(principal)) = self
|
||||
.core
|
||||
.storage
|
||||
.directory
|
||||
.query(QueryBy::Id(id), false)
|
||||
.await
|
||||
{
|
||||
account_name = principal.name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok((account_name, server_name, 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.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()
|
||||
));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(format!(
|
||||
"Error at position {}: {:?}",
|
||||
reader.buffer_position(),
|
||||
e
|
||||
))
|
||||
}
|
||||
_ => {
|
||||
return Err(format!(
|
||||
"Expected tag {}, found unexpected EOF at position {}.",
|
||||
tag_name,
|
||||
reader.buffer_position()
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(Event::Text(text)) = reader.read_event_into(&mut buf) {
|
||||
if let Ok(text) = text.unescape() {
|
||||
if text.contains('@') {
|
||||
return Ok(text.trim().to_lowercase());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(format!(
|
||||
"Expected email address, found unexpected value at position {}.",
|
||||
reader.buffer_position()
|
||||
))
|
||||
}
|
|
@ -46,7 +46,7 @@ use jmap_proto::{
|
|||
};
|
||||
|
||||
use crate::{
|
||||
auth::{oauth::OAuthMetadata, AccessToken},
|
||||
auth::oauth::OAuthMetadata,
|
||||
blob::{DownloadResponse, UploadResponse},
|
||||
services::state,
|
||||
JMAP,
|
||||
|
@ -72,7 +72,7 @@ impl JMAP {
|
|||
let mut path = req.uri().path().split('/');
|
||||
path.next();
|
||||
|
||||
match path.next().unwrap_or("") {
|
||||
match path.next().unwrap_or_default() {
|
||||
"jmap" => {
|
||||
// Authenticate request
|
||||
let (_in_flight, access_token) =
|
||||
|
@ -88,12 +88,15 @@ impl JMAP {
|
|||
Err(err) => return err.into_http_response(),
|
||||
};
|
||||
|
||||
match (path.next().unwrap_or(""), req.method()) {
|
||||
match (path.next().unwrap_or_default(), req.method()) {
|
||||
("", &Method::POST) => {
|
||||
return match fetch_body(
|
||||
&mut req,
|
||||
self.core.jmap.request_max_size,
|
||||
&access_token,
|
||||
if !access_token.is_super_user() {
|
||||
self.core.jmap.upload_max_size
|
||||
} else {
|
||||
0
|
||||
},
|
||||
)
|
||||
.await
|
||||
.ok_or_else(|| RequestError::limit(RequestLimitError::SizeRequest))
|
||||
|
@ -159,8 +162,11 @@ impl JMAP {
|
|||
{
|
||||
return match fetch_body(
|
||||
&mut req,
|
||||
self.core.jmap.upload_max_size,
|
||||
&access_token,
|
||||
if !access_token.is_super_user() {
|
||||
self.core.jmap.upload_max_size
|
||||
} else {
|
||||
0
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
@ -204,7 +210,7 @@ impl JMAP {
|
|||
_ => (),
|
||||
}
|
||||
}
|
||||
".well-known" => match (path.next().unwrap_or(""), req.method()) {
|
||||
".well-known" => match (path.next().unwrap_or_default(), req.method()) {
|
||||
("jmap", &Method::GET) => {
|
||||
// Authenticate request
|
||||
let (_in_flight, access_token) =
|
||||
|
@ -265,12 +271,22 @@ impl JMAP {
|
|||
return RequestError::not_found().into_http_response();
|
||||
}
|
||||
}
|
||||
("mail-v1.xml", &Method::GET) => {
|
||||
return self.handle_autoconfig_request(&req).await;
|
||||
}
|
||||
("autoconfig", &Method::GET) => {
|
||||
if path.next().unwrap_or_default() == "mail"
|
||||
&& path.next().unwrap_or_default() == "config-v1.1.xml"
|
||||
{
|
||||
return self.handle_autoconfig_request(&req).await;
|
||||
}
|
||||
}
|
||||
(_, &Method::OPTIONS) => {
|
||||
return ().into_http_response();
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
"auth" => match (path.next().unwrap_or(""), req.method()) {
|
||||
"auth" => match (path.next().unwrap_or_default(), req.method()) {
|
||||
("device", &Method::POST) => {
|
||||
return match self.is_anonymous_allowed(&session.remote_ip).await {
|
||||
Ok(_) => {
|
||||
|
@ -300,7 +316,7 @@ impl JMAP {
|
|||
// Authenticate user
|
||||
return match self.authenticate_headers(&req, session.remote_ip).await {
|
||||
Ok(Some((_, access_token))) => {
|
||||
let body = fetch_body(&mut req, 8192, &access_token).await;
|
||||
let body = fetch_body(&mut req, 1024 * 1024).await;
|
||||
self.handle_api_manage_request(&req, body, access_token)
|
||||
.await
|
||||
}
|
||||
|
@ -308,6 +324,22 @@ impl JMAP {
|
|||
Err(err) => err.into_http_response(),
|
||||
};
|
||||
}
|
||||
"mail" => {
|
||||
if req.method() == Method::GET
|
||||
&& path.next().unwrap_or_default() == "config-v1.1.xml"
|
||||
{
|
||||
return self.handle_autoconfig_request(&req).await;
|
||||
}
|
||||
}
|
||||
"autodiscover" => {
|
||||
if req.method() == Method::POST
|
||||
&& path.next().unwrap_or_default() == "autodiscover.xml"
|
||||
{
|
||||
return self
|
||||
.handle_autodiscover_request(fetch_body(&mut req, 8192).await)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let path = req.uri().path();
|
||||
return match self
|
||||
|
@ -456,16 +488,11 @@ impl HttpSessionData {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn fetch_body(
|
||||
req: &mut HttpRequest,
|
||||
max_size: usize,
|
||||
access_token: &AccessToken,
|
||||
) -> Option<Vec<u8>> {
|
||||
pub async fn fetch_body(req: &mut HttpRequest, max_size: usize) -> Option<Vec<u8>> {
|
||||
let mut bytes = Vec::with_capacity(1024);
|
||||
while let Some(Ok(frame)) = req.frame().await {
|
||||
if let Some(data) = frame.data_ref() {
|
||||
if bytes.len() + data.len() <= max_size || max_size == 0 || access_token.is_super_user()
|
||||
{
|
||||
if bytes.len() + data.len() <= max_size || max_size == 0 {
|
||||
bytes.extend_from_slice(data);
|
||||
} else {
|
||||
return None;
|
||||
|
|
|
@ -217,19 +217,12 @@ impl JMAP {
|
|||
content: "v=spf1 a ra=postmaster -all".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
records.push(DnsRecord {
|
||||
typ: "TXT".to_string(),
|
||||
name: format!("{domain_name}."),
|
||||
content: "v=spf1 mx ra=postmaster -all".to_string(),
|
||||
});
|
||||
|
||||
records.push(DnsRecord {
|
||||
typ: "CNAME".to_string(),
|
||||
name: format!("autoconfig.{domain_name}."),
|
||||
content: format!("{server_name}."),
|
||||
});
|
||||
|
||||
let mut has_https = false;
|
||||
for (protocol, port, is_tls) in self
|
||||
.core
|
||||
|
@ -260,22 +253,27 @@ impl JMAP {
|
|||
content: format!("0 1 {port} {server_name}."),
|
||||
});
|
||||
}
|
||||
("http", port @ 1..=u16::MAX) => {
|
||||
if is_tls {
|
||||
has_https = true;
|
||||
records.push(DnsRecord {
|
||||
typ: "SRV".to_string(),
|
||||
name: format!("_autodiscover._tcp.{domain_name}."),
|
||||
content: format!("0 1 {port} {server_name}."),
|
||||
});
|
||||
}
|
||||
("http", _) if is_tls => {
|
||||
has_https = true;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
// Add MTA-STS record
|
||||
if has_https {
|
||||
// Add autoconfig and autodiscover records
|
||||
records.push(DnsRecord {
|
||||
typ: "CNAME".to_string(),
|
||||
name: format!("autoconfig.{domain_name}."),
|
||||
content: format!("{server_name}."),
|
||||
});
|
||||
records.push(DnsRecord {
|
||||
typ: "CNAME".to_string(),
|
||||
name: format!("autodiscover.{domain_name}."),
|
||||
content: format!("{server_name}."),
|
||||
});
|
||||
|
||||
// Add MTA-STS records
|
||||
if let Some(policy) = self.core.build_mta_sts_policy() {
|
||||
records.push(DnsRecord {
|
||||
typ: "CNAME".to_string(),
|
||||
|
|
|
@ -28,6 +28,7 @@ use utils::map::vec_map::VecMap;
|
|||
|
||||
use crate::JmapInstance;
|
||||
|
||||
pub mod autoconfig;
|
||||
pub mod event_source;
|
||||
pub mod http;
|
||||
pub mod management;
|
||||
|
|
|
@ -23,11 +23,13 @@
|
|||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use http_body_util::BodyExt;
|
||||
use hyper::{header::CONTENT_TYPE, StatusCode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::api::{http::ToHttpResponse, HtmlResponse, HttpRequest, HttpResponse};
|
||||
use crate::api::{
|
||||
http::{fetch_body, ToHttpResponse},
|
||||
HtmlResponse, HttpRequest, HttpResponse,
|
||||
};
|
||||
|
||||
pub mod auth;
|
||||
pub mod token;
|
||||
|
@ -272,17 +274,3 @@ impl FormData {
|
|||
self.fields.remove(key)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fetch_body(req: &mut HttpRequest, max_len: usize) -> Option<Vec<u8>> {
|
||||
let mut bytes = Vec::with_capacity(1024);
|
||||
while let Some(Ok(frame)) = req.frame().await {
|
||||
if let Some(data) = frame.data_ref() {
|
||||
if bytes.len() + data.len() <= max_len {
|
||||
bytes.extend_from_slice(data);
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
bytes.into()
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ homepage = "https://stalw.art"
|
|||
keywords = ["imap", "jmap", "smtp", "email", "mail", "server"]
|
||||
categories = ["email"]
|
||||
license = "AGPL-3.0-only"
|
||||
version = "0.7.3"
|
||||
version = "0.8.0"
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "managesieve"
|
||||
version = "0.7.3"
|
||||
version = "0.8.0"
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "nlp"
|
||||
version = "0.7.3"
|
||||
version = "0.8.0"
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ homepage = "https://stalw.art/smtp"
|
|||
keywords = ["smtp", "email", "mail", "server"]
|
||||
categories = ["email"]
|
||||
license = "AGPL-3.0-only"
|
||||
version = "0.7.3"
|
||||
version = "0.8.0"
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "store"
|
||||
version = "0.7.3"
|
||||
version = "0.8.0"
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "utils"
|
||||
version = "0.7.3"
|
||||
version = "0.8.0"
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
|
|
Loading…
Reference in a new issue