diff --git a/Cargo.lock b/Cargo.lock
index c1225800..c056e31d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
-version = 3
+version = 4
[[package]]
name = "RustyXML"
diff --git a/README.md b/README.md
index b6843544..7b2f0dff 100644
--- a/README.md
+++ b/README.md
@@ -25,7 +25,7 @@
-
+
## Features
diff --git a/crates/common/src/auth/oauth/mod.rs b/crates/common/src/auth/oauth/mod.rs
index 575b7591..0f255c01 100644
--- a/crates/common/src/auth/oauth/mod.rs
+++ b/crates/common/src/auth/oauth/mod.rs
@@ -24,6 +24,7 @@ pub enum GrantType {
RefreshToken,
LiveTracing,
LiveMetrics,
+ Troubleshoot,
}
impl GrantType {
@@ -33,6 +34,7 @@ impl GrantType {
GrantType::RefreshToken => "refresh_token",
GrantType::LiveTracing => "live_tracing",
GrantType::LiveMetrics => "live_metrics",
+ GrantType::Troubleshoot => "troubleshoot",
}
}
@@ -42,6 +44,7 @@ impl GrantType {
GrantType::RefreshToken => 1,
GrantType::LiveTracing => 2,
GrantType::LiveMetrics => 3,
+ GrantType::Troubleshoot => 4,
}
}
@@ -51,6 +54,7 @@ impl GrantType {
1 => Some(GrantType::RefreshToken),
2 => Some(GrantType::LiveTracing),
3 => Some(GrantType::LiveMetrics),
+ 4 => Some(GrantType::Troubleshoot),
_ => None,
}
}
diff --git a/crates/common/src/config/smtp/resolver.rs b/crates/common/src/config/smtp/resolver.rs
index b6e6802f..7b7c39ab 100644
--- a/crates/common/src/config/smtp/resolver.rs
+++ b/crates/common/src/config/smtp/resolver.rs
@@ -22,6 +22,7 @@ use mail_auth::{
Resolver,
};
use parking_lot::Mutex;
+use serde::{Deserialize, Serialize};
use utils::config::{utils::ParseValue, Config};
use crate::Server;
@@ -42,7 +43,7 @@ pub struct DnsRecordCache {
pub mta_sts: LruCache>,
}
-#[derive(Debug, Hash, PartialEq, Eq)]
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct TlsaEntry {
pub is_end_entity: bool,
pub is_sha256: bool,
@@ -50,14 +51,15 @@ pub struct TlsaEntry {
pub data: Vec,
}
-#[derive(Debug, Hash, PartialEq, Eq)]
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct Tlsa {
pub entries: Vec,
pub has_end_entities: bool,
pub has_intermediates: bool,
}
-#[derive(Debug, PartialEq, Eq, Hash, Default, Clone, Copy)]
+#[derive(Debug, PartialEq, Eq, Hash, Default, Clone, Copy, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
pub enum Mode {
Enforce,
Testing,
@@ -65,13 +67,14 @@ pub enum Mode {
None,
}
-#[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Clone)]
+#[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
pub enum MxPattern {
Equals(String),
StartsWith(String),
}
-#[derive(Debug, PartialEq, Eq, Hash, Clone)]
+#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
pub struct Policy {
pub id: String,
pub mode: Mode,
diff --git a/crates/directory/src/core/mod.rs b/crates/directory/src/core/mod.rs
index 89428ded..859ea631 100644
--- a/crates/directory/src/core/mod.rs
+++ b/crates/directory/src/core/mod.rs
@@ -198,6 +198,7 @@ impl Permission {
Permission::OauthClientUpdate => "Modify OAuth clients",
Permission::OauthClientDelete => "Remove OAuth clients",
Permission::AiModelInteract => "Interact with AI models",
+ Permission::Troubleshoot => "Perform troubleshooting",
}
}
}
diff --git a/crates/directory/src/lib.rs b/crates/directory/src/lib.rs
index 69f5109f..cdfda42e 100644
--- a/crates/directory/src/lib.rs
+++ b/crates/directory/src/lib.rs
@@ -264,7 +264,7 @@ pub enum Permission {
OauthClientOverride,
AiModelInteract,
- // WARNING: add new ids at the end (TODO: use static ids)
+ Troubleshoot, // WARNING: add new ids at the end (TODO: use static ids)
}
pub type Permissions = Bitset<{ Permission::COUNT.div_ceil(std::mem::size_of::()) }>;
diff --git a/crates/jmap/src/api/http.rs b/crates/jmap/src/api/http.rs
index 50e2851b..5c5ce0eb 100644
--- a/crates/jmap/src/api/http.rs
+++ b/crates/jmap/src/api/http.rs
@@ -33,6 +33,7 @@ use jmap_proto::{
};
use std::future::Future;
use trc::SecurityEvent;
+use utils::url_params::UrlParams;
#[cfg(feature = "enterprise")]
use crate::api::management::enterprise::telemetry::TelemetryApi;
@@ -54,7 +55,7 @@ use super::{
autoconfig::Autoconfig,
event_source::EventSourceHandler,
form::FormHandler,
- management::{ManagementApi, ManagementApiError},
+ management::{troubleshoot::TroubleshootApi, ManagementApi, ManagementApiError},
request::RequestHandler,
session::SessionHandler,
HtmlResponse, HttpRequest, HttpResponse, HttpResponseBody, JmapSessionManager, JsonResponse,
@@ -335,45 +336,67 @@ impl ParseHttp for Server {
.await;
}
Err(err) => {
- #[cfg(feature = "enterprise")]
- {
- // SPDX-SnippetBegin
- // SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd
- // SPDX-License-Identifier: LicenseRef-SEL
+ if err.matches(trc::EventType::Auth(trc::AuthEvent::Failed)) {
+ let params = UrlParams::new(req.uri().query());
+ let path = req.uri().path().split('/').skip(2).collect::>();
- // Eventsource does not support authentication, validate the token instead
- if err.matches(trc::EventType::Auth(trc::AuthEvent::Failed))
- && self.core.is_enterprise_edition()
- {
- if let Some((live_path, grant_type, token)) = req
- .uri()
- .path()
- .strip_prefix("/api/telemetry/")
- .and_then(|p| {
- p.strip_prefix("traces/live/")
- .map(|t| ("traces", GrantType::LiveTracing, t))
- .or_else(|| {
- p.strip_prefix("metrics/live/")
- .map(|t| ("metrics", GrantType::LiveMetrics, t))
- })
- })
+ let (grant_type, token) = match (
+ path.first().copied(),
+ path.get(1).copied(),
+ params.get("token"),
+ ) {
+ // SPDX-SnippetBegin
+ // SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd
+ // SPDX-License-Identifier: LicenseRef-SEL
+ #[cfg(feature = "enterprise")]
+ (Some("telemetry"), Some("traces"), Some(token))
+ if self.core.is_enterprise_edition() =>
{
- let token_info = self
- .validate_access_token(grant_type.into(), token)
- .await?;
-
- return self
- .handle_telemetry_api_request(
- &req,
- vec!["", live_path, "live"],
- &AccessToken::from_id(token_info.account_id)
- .with_permission(Permission::MetricsLive)
- .with_permission(Permission::TracingLive),
- )
- .await;
+ (GrantType::LiveTracing, token)
}
- }
- // SPDX-SnippetEnd
+ #[cfg(feature = "enterprise")]
+ (Some("telemetry"), Some("metrics"), Some(token))
+ if self.core.is_enterprise_edition() =>
+ {
+ (GrantType::LiveMetrics, token)
+ }
+ // SPDX-SnippetEnd
+ (Some("troubleshoot"), _, Some(token)) => {
+ (GrantType::Troubleshoot, token)
+ }
+ _ => return Err(err),
+ };
+ let token_info =
+ self.validate_access_token(grant_type.into(), token).await?;
+
+ return match grant_type {
+ // SPDX-SnippetBegin
+ // SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd
+ // SPDX-License-Identifier: LicenseRef-SEL
+ #[cfg(feature = "enterprise")]
+ GrantType::LiveTracing | GrantType::LiveMetrics => {
+ self.handle_telemetry_api_request(
+ &req,
+ path,
+ &AccessToken::from_id(token_info.account_id)
+ .with_permission(Permission::MetricsLive)
+ .with_permission(Permission::TracingLive),
+ )
+ .await
+ }
+ // SPDX-SnippetEnd
+ GrantType::Troubleshoot => {
+ self.handle_troubleshoot_api_request(
+ &req,
+ path,
+ &AccessToken::from_id(token_info.account_id)
+ .with_permission(Permission::Troubleshoot),
+ None,
+ )
+ .await
+ }
+ _ => unreachable!(),
+ };
}
return Err(err);
diff --git a/crates/jmap/src/api/management/enterprise/telemetry.rs b/crates/jmap/src/api/management/enterprise/telemetry.rs
index 0ae46fe9..78a6f3ac 100644
--- a/crates/jmap/src/api/management/enterprise/telemetry.rs
+++ b/crates/jmap/src/api/management/enterprise/telemetry.rs
@@ -353,7 +353,6 @@ impl TelemetryApi for Server {
access_token.assert_has_permission(Permission::TracingLive)?;
// Issue a live telemetry token valid for 60 seconds
-
Ok(JsonResponse::new(json!({
"data": self.encode_access_token(GrantType::LiveTracing, account_id, "web", 60).await?,
}))
@@ -364,7 +363,6 @@ impl TelemetryApi for Server {
access_token.assert_has_permission(Permission::MetricsLive)?;
// Issue a live telemetry token valid for 60 seconds
-
Ok(JsonResponse::new(json!({
"data": self.encode_access_token(GrantType::LiveMetrics, account_id, "web", 60).await?,
}))
diff --git a/crates/jmap/src/api/management/mod.rs b/crates/jmap/src/api/management/mod.rs
index 776c723d..b3c96284 100644
--- a/crates/jmap/src/api/management/mod.rs
+++ b/crates/jmap/src/api/management/mod.rs
@@ -16,6 +16,7 @@ pub mod report;
pub mod settings;
pub mod sieve;
pub mod stores;
+pub mod troubleshoot;
use std::{borrow::Cow, str::FromStr, sync::Arc};
@@ -37,6 +38,7 @@ use settings::ManageSettings;
use sieve::SieveHandler;
use store::write::now;
use stores::ManageStore;
+use troubleshoot::TroubleshootApi;
use crate::{auth::oauth::auth::OAuthApiHandler, email::crypto::CryptoHandler};
@@ -155,6 +157,13 @@ impl ManagementApi for Server {
}
_ => Err(trc::ResourceEvent::NotFound.into_err()),
},
+ "troubleshoot" => {
+ // Validate the access token
+ access_token.assert_has_permission(Permission::Troubleshoot)?;
+
+ self.handle_troubleshoot_api_request(req, path, &access_token, body)
+ .await
+ }
// SPDX-SnippetBegin
// SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd
// SPDX-License-Identifier: LicenseRef-SEL
diff --git a/crates/jmap/src/api/management/troubleshoot.rs b/crates/jmap/src/api/management/troubleshoot.rs
new file mode 100644
index 00000000..4a176f2a
--- /dev/null
+++ b/crates/jmap/src/api/management/troubleshoot.rs
@@ -0,0 +1,1061 @@
+/*
+ * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
+ */
+
+use std::{
+ future::Future,
+ net::{IpAddr, SocketAddr},
+ time::{Duration, Instant},
+};
+
+use common::{
+ auth::{oauth::GrantType, AccessToken},
+ config::smtp::resolver::{Policy, Tlsa},
+ psl, Server,
+};
+use directory::backend::internal::manage;
+use http_body_util::{combinators::BoxBody, StreamBody};
+use hyper::{
+ body::{Bytes, Frame},
+ Method, StatusCode,
+};
+use mail_auth::{
+ dmarc::{self},
+ mta_sts::TlsRpt,
+ AuthenticatedMessage, DkimResult, DmarcResult, IpLookupStrategy, IprevOutput, IprevResult,
+ SpfOutput, SpfResult,
+};
+use serde::{Deserialize, Serialize};
+use serde_json::json;
+use smtp::outbound::{
+ client::{SmtpClient, StartTlsResult},
+ dane::{dnssec::TlsaLookup, verify::TlsaVerify},
+ lookup::{DnsLookup, ToNextHop},
+ mta_sts::{lookup::MtaStsLookup, verify::VerifyPolicy},
+};
+use tokio::{io::AsyncWriteExt, sync::mpsc};
+use utils::url_params::UrlParams;
+
+use crate::api::{
+ http::ToHttpResponse, management::decode_path_element, HttpRequest, HttpResponse,
+ HttpResponseBody, JsonResponse,
+};
+
+pub trait TroubleshootApi: Sync + Send {
+ fn handle_troubleshoot_api_request(
+ &self,
+ req: &HttpRequest,
+ path: Vec<&str>,
+ access_token: &AccessToken,
+ body: Option>,
+ ) -> impl Future