Skip to main content

tuwunel_api/client/account/3pid/
email_token.rs

1use std::{net::IpAddr, time::Duration};
2
3use ruma::{OwnedSessionId, thirdparty::Medium};
4use tuwunel_core::{Result, err, utils::html::escape as html_escape};
5use tuwunel_service::Services;
6use url::form_urlencoded;
7
8/// Lifetime of a pending email verification before it self-reaps.
9const PENDING_TTL: Duration = Duration::from_hours(1);
10
11/// Shared requestToken spine for the email medium: throttle both axes, open or
12/// reuse a pending verification, and send the magic-link message when a token
13/// is freshly minted. The caller supplies the already-canonicalized address and
14/// performs its own directional in-use / not-found check beforehand.
15pub(super) async fn send_email_token(
16	services: &Services,
17	client: IpAddr,
18	client_secret: &str,
19	email_canon: &str,
20	send_attempt: u64,
21) -> Result<OwnedSessionId> {
22	services.sendmail.check_address(email_canon)?;
23	services.threepid.check_ip_rate_limit(client)?;
24	services
25		.threepid
26		.check_address_rate_limit(email_canon)?;
27
28	let outcome = services
29		.threepid
30		.create_or_reuse_pending(
31			client_secret,
32			Medium::Email,
33			email_canon,
34			send_attempt,
35			PENDING_TTL,
36		)
37		.await?;
38
39	if let Some(token) = outcome.freshly_minted_token {
40		let link = validate_link(services, &outcome.sid, client_secret, &token)?;
41		let body = verification_html(&link);
42
43		services
44			.sendmail
45			.send_to(email_canon, "Verify your email address", body)
46			.await?;
47	}
48
49	outcome
50		.sid
51		.parse()
52		.map_err(|_| err!("Generated an invalid session id"))
53}
54
55fn validate_link(
56	services: &Services,
57	sid: &str,
58	client_secret: &str,
59	token: &str,
60) -> Result<String> {
61	let base = services
62		.config
63		.well_known
64		.client
65		.as_ref()
66		.map(ToString::to_string)
67		.ok_or_else(|| {
68			err!(Config(
69				"well_known.client",
70				"A public client base URL must be set to send email"
71			))
72		})?;
73
74	let base = base.trim_end_matches('/');
75	let query = form_urlencoded::Serializer::new(String::new())
76		.append_pair("sid", sid)
77		.append_pair("client_secret", client_secret)
78		.append_pair("token", token)
79		.finish();
80
81	Ok(format!("{base}/_tuwunel/3pid/email/validate?{query}"))
82}
83
84fn verification_html(link: &str) -> String {
85	let link = html_escape(link);
86
87	format!(
88		"<!DOCTYPE html>
89<html lang=\"en\">
90  <body>
91    <h1>Verify your email address</h1>
92    <p>Open the link below to confirm this address.</p>
93    <p><a href=\"{link}\">{link}</a></p>
94    <p>If you did not request this, you can ignore this message.</p>
95  </body>
96</html>"
97	)
98}