1use std::{future::Future, sync::Arc};
5
6use topcoat::{
7 context::Cx,
8 mail::{Mail, Mailbox},
9 router::{request::uri, response::Response},
10};
11
12use crate::auth::realm::{Realm, current_realm};
13
14pub struct AuthMail {
39 from: Mailbox,
40 base_url: String,
41}
42
43impl AuthMail {
44 #[must_use]
48 pub fn new(from: Mailbox, base_url: impl Into<String>) -> Self {
49 Self {
50 from,
51 base_url: base_url.into().trim_end_matches('/').to_string(),
52 }
53 }
54
55 pub(crate) fn base_url_is_absolute(&self) -> bool {
57 self.base_url.parse::<http::Uri>().is_ok_and(|uri| {
58 matches!(uri.scheme_str(), Some("http" | "https"))
59 && uri.authority().is_some()
60 && uri.query().is_none()
61 }) && !self.base_url.contains('#')
62 }
63
64 pub(crate) fn base_url(&self) -> &str {
65 &self.base_url
66 }
67
68 pub(super) fn link(&self, path: &str, token: &topcoat::session::Token) -> String {
70 let query = form_urlencoded::Serializer::new(String::new())
71 .append_pair(TOKEN_FIELD, &token.encode())
72 .finish();
73 format!("{}{path}?{query}", self.base_url)
74 }
75
76 pub(super) fn compose(
78 &self,
79 address: &str,
80 subject: String,
81 text: String,
82 ) -> topcoat::Result<Mail> {
83 Ok(Mail::builder()
84 .from(self.from.clone())
85 .to([Mailbox::new(address)?])
86 .subject(subject)
87 .text(text)
88 .build())
89 }
90}
91
92impl std::fmt::Debug for AuthMail {
93 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94 f.debug_struct("AuthMail")
95 .field("from", &self.from.address())
96 .field("base_url", &self.base_url)
97 .finish()
98 }
99}
100
101pub(super) const TOKEN_FIELD: &str = "token";
103
104pub(super) fn token_from_query(cx: &Cx) -> String {
106 uri(cx)
107 .query()
108 .and_then(|query| {
109 form_urlencoded::parse(query.as_bytes())
110 .find(|(key, _)| key == TOKEN_FIELD)
111 .map(|(_, value)| value.into_owned())
112 })
113 .unwrap_or_default()
114}
115
116pub(super) fn after_response<F, Fut>(cx: &Cx, what: &'static str, send: F)
120where
121 F: FnOnce(Cx, Arc<Realm>) -> Fut + Send + 'static,
122 Fut: Future<Output = topcoat::Result<()>> + Send + 'static,
123{
124 let Some(panel) = current_realm(cx).map(Arc::clone) else {
125 return;
126 };
127 let cx = cx.clone();
128 tokio::spawn(async move {
129 let prefix = panel.prefix.clone();
130 if let Err(error) = send(cx, panel).await {
131 tracing::error!(error = %error, panel = %prefix, "{what} not sent");
132 }
133 });
134}
135
136pub(super) fn private(mut response: Response) -> topcoat::Result<Response> {
139 response.headers_mut().insert(
140 http::header::REFERRER_POLICY,
141 http::HeaderValue::from_static("no-referrer"),
142 );
143 Ok(response)
144}
145
146pub(super) fn declaration_errors(
150 auth: &super::AuthConfig,
151 has_mail: bool,
152) -> Vec<crate::DeclarationErrorKind> {
153 use crate::{AuthMailFault, DeclarationErrorKind};
154
155 let mailing = auth.mailing();
156 if auth.is_disabled() || mailing.is_empty() {
157 return Vec::new();
158 }
159 let Some(mail) = auth.mail.as_ref() else {
160 return mailing
161 .into_iter()
162 .map(|feature| DeclarationErrorKind::AuthMail(AuthMailFault::Unset { feature }))
163 .collect();
164 };
165 let mut faults = Vec::new();
166 if !mail.base_url_is_absolute() {
167 faults.push(AuthMailFault::BaseUrl(mail.base_url().to_string()));
168 }
169 if !has_mail {
170 faults.push(AuthMailFault::MissingMail);
171 }
172 faults
173 .into_iter()
174 .map(DeclarationErrorKind::AuthMail)
175 .collect()
176}