Skip to content

Commit ac71d2b

Browse files
committed
Delivery and DMARC troubleshooting (closes stalwartlabs/mail-server#420)
1 parent c21b1a2 commit ac71d2b

File tree

11 files changed

+1427
-45
lines changed

11 files changed

+1427
-45
lines changed

src/components/icon.rs

+16
Original file line numberDiff line numberDiff line change
@@ -783,3 +783,19 @@ pub fn IconHandRaised(
783783
</SvgWrapper>
784784
}
785785
}
786+
787+
#[component]
788+
pub fn IconBeaker(
789+
#[prop(optional)] size: Option<usize>,
790+
#[prop(attrs)] attrs: Vec<(&'static str, Attribute)>,
791+
) -> impl IntoView {
792+
view! {
793+
<SvgWrapper size attrs>
794+
<path
795+
stroke-linecap="round"
796+
stroke-linejoin="round"
797+
d="M9.75 3.104v5.714a2.25 2.25 0 0 1-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 0 1 4.5 0m0 0v5.714c0 .597.237 1.17.659 1.591L19.8 15.3M14.25 3.104c.251.023.501.05.75.082M19.8 15.3l-1.57.393A9.065 9.065 0 0 1 12 15a9.065 9.065 0 0 0-6.23-.693L5 14.5m14.8.8 1.402 1.402c1.232 1.232.65 3.318-1.067 3.611A48.309 48.309 0 0 1 12 21c-2.773 0-5.491-.235-8.135-.687-1.718-.293-2.3-2.379-1.067-3.61L5 14.5"
798+
></path>
799+
</SvgWrapper>
800+
}
801+
}

src/core/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ pub enum Permission {
106106
TracingLive,
107107
MetricsList,
108108
MetricsLive,
109+
Troubleshoot,
109110

110111
// Account Management
111112
ManageEncryption,

src/main.rs

+39-4
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ use std::{sync::Arc, time::Duration};
1010

1111
use components::{
1212
icon::{
13-
IconAdjustmentsHorizontal, IconChartBarSquare, IconClock, IconDocumentChartBar, IconKey,
14-
IconLockClosed, IconQueueList, IconShieldCheck, IconSignal, IconSquare2x2, IconUserGroup,
15-
IconWrench,
13+
IconAdjustmentsHorizontal, IconBeaker, IconChartBarSquare, IconClock, IconDocumentChartBar,
14+
IconKey, IconLockClosed, IconQueueList, IconShieldCheck, IconSignal, IconSquare2x2,
15+
IconUserGroup, IconWrench,
1616
},
1717
layout::MenuItem,
1818
};
@@ -33,7 +33,10 @@ use pages::{
3333
tracing::{display::SpanDisplay, list::SpanList, live::LiveTracing},
3434
undelete::UndeleteList,
3535
},
36-
manage::spam::{SpamTest, SpamTrain},
36+
manage::{
37+
spam::{SpamTest, SpamTrain},
38+
troubleshoot::{TroubleshootDelivery, TroubleshootDmarc},
39+
},
3740
};
3841

3942
pub static VERSION_NAME: &str = concat!("Stalwart Management UI v", env!("CARGO_PKG_VERSION"),);
@@ -419,6 +422,28 @@ pub fn App() -> impl IntoView {
419422
}
420423
/>
421424

425+
<ProtectedRoute
426+
path="/troubleshoot/delivery"
427+
view=TroubleshootDelivery
428+
redirect_path="/login"
429+
condition=move || {
430+
permissions
431+
.get()
432+
.map_or(false, |p| { p.has_access(Permission::Troubleshoot) })
433+
}
434+
/>
435+
436+
<ProtectedRoute
437+
path="/troubleshoot/dmarc"
438+
view=TroubleshootDmarc
439+
redirect_path="/login"
440+
condition=move || {
441+
permissions
442+
.get()
443+
.map_or(false, |p| { p.has_access(Permission::Troubleshoot) })
444+
}
445+
/>
446+
422447
</ProtectedRoute>
423448
<ProtectedRoute
424449
path="/settings"
@@ -653,6 +678,15 @@ impl LayoutBuilder {
653678
.route("/spam/test")
654679
.insert(true)
655680
.insert(permissions.has_access(Permission::SieveRun))
681+
.create("Troubleshoot")
682+
.icon(view! { <IconBeaker/> })
683+
.create("E-mail Delivery")
684+
.route("/troubleshoot/delivery")
685+
.insert(true)
686+
.create("DMARC")
687+
.route("/troubleshoot/dmarc")
688+
.insert(true)
689+
.insert(permissions.has_access(Permission::Troubleshoot))
656690
.create("Settings")
657691
.icon(view! { <IconAdjustmentsHorizontal/> })
658692
.raw_route(DEFAULT_SETTINGS_URL)
@@ -717,6 +751,7 @@ pub fn build_schemas() -> Arc<Schemas> {
717751
.build_mfa()
718752
.build_app_passwords()
719753
.build_live_tracing()
754+
.build_troubleshoot()
720755
.build()
721756
.into()
722757
}

src/pages/authorize.rs

+23-33
Original file line numberDiff line numberDiff line change
@@ -49,19 +49,10 @@ pub fn Authorize() -> impl IntoView {
4949
let state = query.get().get("state").cloned();
5050

5151
async move {
52-
5352
match &request {
54-
OAuthCodeRequest::Code {
55-
redirect_uri,
56-
..
57-
} => {
58-
match oauth_user_authentication(
59-
BASE_URL,
60-
&username,
61-
&password,
62-
&request,
63-
)
64-
.await
53+
OAuthCodeRequest::Code { redirect_uri, .. } => {
54+
match oauth_user_authentication(BASE_URL, &username, &password, &request)
55+
.await
6556
{
6657
AuthenticationResult::Success(response) => {
6758
let url = if let Some(state) = state {
@@ -92,27 +83,26 @@ pub fn Authorize() -> impl IntoView {
9283
}
9384
}
9485
OAuthCodeRequest::Device { .. } => {
95-
let message =
96-
match oauth_device_authentication(BASE_URL, &username, &password, &request)
97-
.await
98-
{
99-
AuthenticationResult::Success(true) => {
100-
Alert::success("Device authenticated")
101-
.with_details(
102-
"You have successfully authenticated your device",
103-
)
104-
.without_timeout()
105-
}
106-
AuthenticationResult::Success(false) => Alert::warning(
107-
"Device authentication failed",
108-
)
109-
.with_details("The code you entered is invalid or has expired"),
110-
AuthenticationResult::TotpRequired => {
111-
show_totp.set(true);
112-
return;
113-
}
114-
AuthenticationResult::Error(err) => err,
115-
};
86+
let message = match oauth_device_authentication(
87+
BASE_URL, &username, &password, &request,
88+
)
89+
.await
90+
{
91+
AuthenticationResult::Success(true) => {
92+
Alert::success("Device authenticated")
93+
.with_details("You have successfully authenticated your device")
94+
.without_timeout()
95+
}
96+
AuthenticationResult::Success(false) => {
97+
Alert::warning("Device authentication failed")
98+
.with_details("The code you entered is invalid or has expired")
99+
}
100+
AuthenticationResult::TotpRequired => {
101+
show_totp.set(true);
102+
return;
103+
}
104+
AuthenticationResult::Error(err) => err,
105+
};
116106

117107
alert.set(message);
118108
}

src/pages/directory/edit.rs

+5-2
Original file line numberDiff line numberDiff line change
@@ -1118,7 +1118,10 @@ impl FormData {
11181118
principal.disabled_permissions.as_string_list(),
11191119
),
11201120
("urls", principal.urls.as_string_list()),
1121-
("external-members", principal.external_members.as_string_list()),
1121+
(
1122+
"external-members",
1123+
principal.external_members.as_string_list(),
1124+
),
11221125
] {
11231126
self.array_set(key, list.iter());
11241127
}
@@ -1181,7 +1184,7 @@ impl FormData {
11811184
("enabled-permissions", &mut principal.enabled_permissions),
11821185
("disabled-permissions", &mut principal.disabled_permissions),
11831186
("urls", &mut principal.urls),
1184-
("external-members", &mut principal.external_members)
1187+
("external-members", &mut principal.external_members),
11851188
] {
11861189
*list = PrincipalValue::StringList(
11871190
self.array_value(key).map(|m| m.to_string()).collect(),

src/pages/directory/list.rs

+33-1
Original file line numberDiff line numberDiff line change
@@ -674,7 +674,39 @@ fn PrincipalItem(principal: Principal, params: Parameters) -> impl IntoView {
674674
}
675675
>
676676

677-
DNS records
677+
View DNS records
678+
</a>
679+
<a
680+
class="flex items-center gap-x-3 py-2 px-3 rounded-lg text-sm text-gray-800 hover:bg-gray-100 focus:ring-2 focus:ring-blue-500 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:hover:text-neutral-300"
681+
href=move || {
682+
format!(
683+
"/manage/troubleshoot/delivery?target={}",
684+
principal.get_untracked().name().unwrap_or_default(),
685+
)
686+
}
687+
688+
class:hidden=move || {
689+
!matches!(selected_type, PrincipalType::Domain)
690+
}
691+
>
692+
693+
Test Email Delivery
694+
</a>
695+
<a
696+
class="flex items-center gap-x-3 py-2 px-3 rounded-lg text-sm text-gray-800 hover:bg-gray-100 focus:ring-2 focus:ring-blue-500 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:hover:text-neutral-300"
697+
href=move || {
698+
format!(
699+
"/manage/troubleshoot/dmarc?target={}",
700+
principal.get_untracked().name().unwrap_or_default(),
701+
)
702+
}
703+
704+
class:hidden=move || {
705+
!matches!(selected_type, PrincipalType::Domain)
706+
}
707+
>
708+
709+
Test DMARC
678710
</a>
679711
<a
680712
class="flex items-center gap-x-3 py-2 px-3 rounded-lg text-sm text-gray-800 hover:bg-gray-100 focus:ring-2 focus:ring-blue-500 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:hover:text-neutral-300"

src/pages/directory/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -624,7 +624,7 @@ impl<'de> serde::Deserialize<'de> for StringOrU64 {
624624
{
625625
struct StringOrU64Visitor;
626626

627-
impl<'de> Visitor<'de> for StringOrU64Visitor {
627+
impl Visitor<'_> for StringOrU64Visitor {
628628
type Value = StringOrU64;
629629

630630
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {

src/pages/enterprise/dashboard.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -175,10 +175,10 @@ pub fn Dashboard() -> impl IntoView {
175175
create_effect(move |_| match start_live_telemetry.get() {
176176
Some(Ok(auth_token)) => {
177177
let url_builer = UrlBuilder::new(format!(
178-
"{}/api/telemetry/metrics/live/{}",
178+
"{}/api/telemetry/metrics/live",
179179
auth.get_untracked().base_url,
180-
auth_token
181180
))
181+
.with_parameter("token", auth_token)
182182
.with_parameter("interval", "30")
183183
.with_parameter(
184184
"metrics",

src/pages/enterprise/tracing/live.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,9 @@ pub fn LiveTracing() -> impl IntoView {
7171
let auth = auth.get_untracked();
7272
let filter = data.get().value::<String>("filter").unwrap_or_default();
7373
let mut url_builer = UrlBuilder::new(
74-
format!("{}/api/telemetry/traces/live/{}", auth.base_url, auth_token),
75-
);
74+
format!("{}/api/telemetry/traces/live", auth.base_url),
75+
)
76+
.with_parameter("token", auth_token);
7677
if !filter.is_empty() {
7778
for keyword in filter.split_ascii_whitespace() {
7879
if let Some((key, value)) = keyword.split_once(':') {

src/pages/manage/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@
77
pub mod logs;
88
pub mod maintenance;
99
pub mod spam;
10+
pub mod troubleshoot;

0 commit comments

Comments
 (0)