feat: Add support for 3pid management

This commit is contained in:
Ginger
2026-03-31 02:20:59 +00:00
committed by Ellis Git
parent ef7ad6082c
commit 3885e43b5d
11 changed files with 192 additions and 75 deletions
+4 -2
View File
@@ -2069,10 +2069,12 @@
# #
#sender = #sender =
# Whether to require that users provide an email address when they register. # Whether to require that users provide an email address when they
# register.
# #
#require_email_for_registration = false #require_email_for_registration = false
# Whether to require that users who register with a registration token provide an email address. # Whether to require that users who register with a registration token
# provide an email address.
# #
#require_email_for_token_registration = false #require_email_for_token_registration = false
+2 -2
View File
@@ -1121,7 +1121,7 @@ pub(super) async fn get_user_by_email(&self, email: String) -> Result {
self.bail_restricted()?; self.bail_restricted()?;
let Ok(email) = Address::try_from(email) else { let Ok(email) = Address::try_from(email) else {
return Err!("Invalid email address"); return Err!("Invalid email address.");
}; };
match self.services.threepid.get_localpart_for_email(&email).await { match self.services.threepid.get_localpart_for_email(&email).await {
@@ -1147,7 +1147,7 @@ pub(super) async fn change_email(&self, user_id: String, email: Option<String>)
let user_id = parse_local_user_id(self.services, &user_id)?; let user_id = parse_local_user_id(self.services, &user_id)?;
let Ok(new_email) = email.map(Address::try_from).transpose() else { let Ok(new_email) = email.map(Address::try_from).transpose() else {
return Err!("Invalid email address"); return Err!("Invalid email address.");
}; };
if self.services.mailer.mailer().is_none() { if self.services.mailer.mailer().is_none() {
+5 -44
View File
@@ -13,9 +13,8 @@ use ruma::{
api::client::{ api::client::{
account::{ account::{
ThirdPartyIdRemovalStatus, change_password, check_registration_token_validity, ThirdPartyIdRemovalStatus, change_password, check_registration_token_validity,
deactivate, get_3pids, get_username_availability, deactivate, get_username_availability, request_password_change_token_via_email,
request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn, whoami,
request_password_change_token_via_email, whoami,
}, },
uiaa::{AuthFlow, AuthType}, uiaa::{AuthFlow, AuthType},
}, },
@@ -33,6 +32,7 @@ use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH, join_room_by_id_helper};
use crate::Ruma; use crate::Ruma;
pub(crate) mod register; pub(crate) mod register;
pub(crate) mod threepid;
/// # `GET /_matrix/client/v3/register/available` /// # `GET /_matrix/client/v3/register/available`
/// ///
@@ -226,12 +226,12 @@ pub(crate) async fn change_password_route(
/// # `POST /_matrix/client/v3/account/password/email/requestToken` /// # `POST /_matrix/client/v3/account/password/email/requestToken`
/// ///
/// Requests a validation email for the purpose of resetting a user's password. /// Requests a validation email for the purpose of resetting a user's password.
pub(crate) async fn password_request_token_route( pub(crate) async fn request_password_change_token_via_email_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<request_password_change_token_via_email::v3::Request>, body: Ruma<request_password_change_token_via_email::v3::Request>,
) -> Result<request_password_change_token_via_email::v3::Response> { ) -> Result<request_password_change_token_via_email::v3::Response> {
let Ok(email) = Address::try_from(body.email.clone()) else { let Ok(email) = Address::try_from(body.email.clone()) else {
return Err!(Request(InvalidParam("Invalid email address"))); return Err!(Request(InvalidParam("Invalid email address.")));
}; };
let Some(localpart) = services.threepid.get_localpart_for_email(&email).await else { let Some(localpart) = services.threepid.get_localpart_for_email(&email).await else {
@@ -341,45 +341,6 @@ pub(crate) async fn deactivate_route(
}) })
} }
/// # `GET _matrix/client/v3/account/3pid`
///
/// Get a list of third party identifiers associated with this account.
///
/// - Currently always returns empty list
pub(crate) async fn third_party_route(
body: Ruma<get_3pids::v3::Request>,
) -> Result<get_3pids::v3::Response> {
let _sender_user = body.sender_user.as_ref().expect("user is authenticated");
Ok(get_3pids::v3::Response::new(Vec::new()))
}
/// # `POST /_matrix/client/v3/account/3pid/email/requestToken`
///
/// "This API should be used to request validation tokens when adding an email
/// address to an account"
///
/// - 403 signals that The homeserver does not allow the third party identifier
/// as a contact option.
pub(crate) async fn request_3pid_management_token_via_email_route(
_body: Ruma<request_3pid_management_token_via_email::v3::Request>,
) -> Result<request_3pid_management_token_via_email::v3::Response> {
Err!(Request(ThreepidDenied("Third party identifiers are not implemented")))
}
/// # `POST /_matrix/client/v3/account/3pid/msisdn/requestToken`
///
/// "This API should be used to request validation tokens when adding an phone
/// number to an account"
///
/// - 403 signals that The homeserver does not allow the third party identifier
/// as a contact option.
pub(crate) async fn request_3pid_management_token_via_msisdn_route(
_body: Ruma<request_3pid_management_token_via_msisdn::v3::Request>,
) -> Result<request_3pid_management_token_via_msisdn::v3::Response> {
Err!(Request(ThreepidDenied("Third party identifiers are not implemented")))
}
/// # `GET /_matrix/client/v1/register/m.login.registration_token/validity` /// # `GET /_matrix/client/v1/register/m.login.registration_token/validity`
/// ///
/// Checks if the provided registration token is valid at the time of checking. /// Checks if the provided registration token is valid at the time of checking.
+4 -4
View File
@@ -569,13 +569,13 @@ async fn determine_registration_user_id(
/// # `POST /_matrix/client/v3/register/email/requestToken` /// # `POST /_matrix/client/v3/register/email/requestToken`
/// ///
/// Requests a validation email for the purpose of registering a new account /// Requests a validation email for the purpose of registering a new account.
pub(crate) async fn register_request_token_route( pub(crate) async fn request_registration_token_via_email_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<request_registration_token_via_email::v3::Request>, body: Ruma<request_registration_token_via_email::v3::Request>,
) -> Result<request_registration_token_via_email::v3::Response> { ) -> Result<request_registration_token_via_email::v3::Response> {
let Ok(email) = Address::try_from(body.email.clone()) else { let Ok(email) = Address::try_from(body.email.clone()) else {
return Err!(Request(InvalidParam("Invalid email address"))); return Err!(Request(InvalidParam("Invalid email address.")));
}; };
if services if services
@@ -584,7 +584,7 @@ pub(crate) async fn register_request_token_route(
.await .await
.is_some() .is_some()
{ {
return Err!(Request(ThreepidInUse("This email address is already in use"))); return Err!(Request(ThreepidInUse("This email address is already in use.")));
} }
let session = services let session = services
+153
View File
@@ -0,0 +1,153 @@
use std::time::SystemTime;
use axum::extract::State;
use conduwuit::{Err, Result, err};
use lettre::{Address, message::Mailbox};
use ruma::{
MilliSecondsSinceUnixEpoch,
api::client::account::{
ThirdPartyIdRemovalStatus, add_3pid, delete_3pid, get_3pids,
request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn,
},
thirdparty::{Medium, ThirdPartyIdentifierInit},
};
use service::{mailer::messages, uiaa::Identity};
use crate::Ruma;
/// # `GET _matrix/client/v3/account/3pid`
///
/// Get a list of third party identifiers associated with this account.
pub(crate) async fn third_party_route(
State(services): State<crate::State>,
body: Ruma<get_3pids::v3::Request>,
) -> Result<get_3pids::v3::Response> {
let sender_user = body.sender_user();
let mut threepids = vec![];
if let Some(email) = services
.threepid
.get_email_for_localpart(sender_user.localpart())
.await
{
threepids.push(
ThirdPartyIdentifierInit {
address: email.to_string(),
medium: Medium::Email,
// We don't currently track these, and they aren't used for much
validated_at: MilliSecondsSinceUnixEpoch::now(),
added_at: MilliSecondsSinceUnixEpoch::from_system_time(SystemTime::UNIX_EPOCH)
.unwrap(),
}
.into(),
);
}
Ok(get_3pids::v3::Response::new(threepids))
}
/// # `POST /_matrix/client/v3/account/3pid/email/requestToken`
///
/// Requests a validation email for the purpose of changing an account's email.
pub(crate) async fn request_3pid_management_token_via_email_route(
State(services): State<crate::State>,
body: Ruma<request_3pid_management_token_via_email::v3::Request>,
) -> Result<request_3pid_management_token_via_email::v3::Response> {
let Ok(email) = Address::try_from(body.email.clone()) else {
return Err!(Request(InvalidParam("Invalid email address.")));
};
if services
.threepid
.get_localpart_for_email(&email)
.await
.is_some()
{
return Err!(Request(ThreepidInUse("This email address is already in use.")));
}
let session = services
.threepid
.send_validation_email(
Mailbox::new(None, email),
|verification_link| messages::ChangeEmail {
server_name: services.config.server_name.as_str(),
user_id: body.sender_user.as_deref(),
verification_link,
},
&body.client_secret,
body.send_attempt.try_into().unwrap(),
)
.await?;
Ok(request_3pid_management_token_via_email::v3::Response::new(session))
}
/// # `POST /_matrix/client/v3/account/3pid/msisdn/requestToken`
///
/// "This API should be used to request validation tokens when adding an email
/// address to an account"
///
/// - 403 signals that The homeserver does not allow the third party identifier
/// as a contact option.
pub(crate) async fn request_3pid_management_token_via_msisdn_route(
_body: Ruma<request_3pid_management_token_via_msisdn::v3::Request>,
) -> Result<request_3pid_management_token_via_msisdn::v3::Response> {
Err!(Request(ThreepidMediumNotSupported(
"MSISDN third-party identifiers are not supported."
)))
}
/// # `POST /_matrix/client/v3/account/3pid/add`
pub(crate) async fn add_3pid_route(
State(services): State<crate::State>,
body: Ruma<add_3pid::v3::Request>,
) -> Result<add_3pid::v3::Response> {
let sender_user = body.sender_user();
// Require password auth to add an email
services
.uiaa
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
.await?;
let email = services
.threepid
.consume_valid_session(&body.sid, &body.client_secret)
.await
.map_err(|message| err!(Request(ThreepidAuthFailed("{message}"))))?;
services
.threepid
.associate_localpart_email(sender_user.localpart(), &email)
.await?;
Ok(add_3pid::v3::Response::new())
}
/// # `POST /_matrix/client/v3/account/3pid/delete`
pub(crate) async fn delete_3pid_route(
State(services): State<crate::State>,
body: Ruma<delete_3pid::v3::Request>,
) -> Result<delete_3pid::v3::Response> {
let sender_user = body.sender_user();
if body.medium != Medium::Email {
return Ok(delete_3pid::v3::Response {
id_server_unbind_result: ThirdPartyIdRemovalStatus::NoSupport,
});
}
if services
.threepid
.disassociate_localpart_email(sender_user.localpart())
.await
.is_none()
{
return Err!(Request(ThreepidNotFound("Your account has no associated email.")));
}
Ok(delete_3pid::v3::Response {
id_server_unbind_result: ThirdPartyIdRemovalStatus::Success,
})
}
+4 -2
View File
@@ -30,8 +30,10 @@ pub(crate) async fn get_capabilities_route(
default: services.server.config.default_room_version.clone(), default: services.server.config.default_room_version.clone(),
}; };
// we do not implement 3PID stuff // Only allow 3pid changes if SMTP is configured
capabilities.thirdparty_id_changes = ThirdPartyIdChangesCapability { enabled: false }; capabilities.thirdparty_id_changes = ThirdPartyIdChangesCapability {
enabled: services.mailer.mailer().is_some(),
};
capabilities.get_login_token = GetLoginTokenCapability { capabilities.get_login_token = GetLoginTokenCapability {
enabled: services.server.config.login_via_existing_session, enabled: services.server.config.login_via_existing_session,
+7 -5
View File
@@ -29,7 +29,7 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
.ruma_route(&client::get_supported_versions_route) .ruma_route(&client::get_supported_versions_route)
.ruma_route(&client::get_register_available_route) .ruma_route(&client::get_register_available_route)
.ruma_route(&client::register::register_route) .ruma_route(&client::register::register_route)
.ruma_route(&client::register::register_request_token_route) .ruma_route(&client::register::request_registration_token_via_email_route)
.ruma_route(&client::get_login_types_route) .ruma_route(&client::get_login_types_route)
.ruma_route(&client::login_route) .ruma_route(&client::login_route)
.ruma_route(&client::login_token_route) .ruma_route(&client::login_token_route)
@@ -37,11 +37,13 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
.ruma_route(&client::logout_route) .ruma_route(&client::logout_route)
.ruma_route(&client::logout_all_route) .ruma_route(&client::logout_all_route)
.ruma_route(&client::change_password_route) .ruma_route(&client::change_password_route)
.ruma_route(&client::password_request_token_route) .ruma_route(&client::request_password_change_token_via_email_route)
.ruma_route(&client::deactivate_route) .ruma_route(&client::deactivate_route)
.ruma_route(&client::third_party_route) .ruma_route(&client::threepid::third_party_route)
.ruma_route(&client::request_3pid_management_token_via_email_route) .ruma_route(&client::threepid::request_3pid_management_token_via_email_route)
.ruma_route(&client::request_3pid_management_token_via_msisdn_route) .ruma_route(&client::threepid::request_3pid_management_token_via_msisdn_route)
.ruma_route(&client::threepid::add_3pid_route)
.ruma_route(&client::threepid::delete_3pid_route)
.ruma_route(&client::check_registration_token_validity) .ruma_route(&client::check_registration_token_validity)
.ruma_route(&client::get_capabilities_route) .ruma_route(&client::get_capabilities_route)
.ruma_route(&client::get_pushrules_all_route) .ruma_route(&client::get_pushrules_all_route)
-7
View File
@@ -415,13 +415,6 @@ impl<'a, 'de: 'a> de::Deserializer<'de> for &'a mut Deserializer<'de> {
tracing::instrument(level = "trace", skip_all, fields(?self.buf)) tracing::instrument(level = "trace", skip_all, fields(?self.buf))
)] )]
fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> { fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
debug_assert_eq!(
conduwuit::debug::type_name::<V>(),
"serde_json::value::de::<impl serde_core::de::Deserialize for \
serde_json::value::Value>::deserialize::ValueVisitor",
"deserialize_any: type not expected"
);
match self.record_peek_byte() { match self.record_peek_byte() {
| Some(b'{') => self.deserialize_map(visitor), | Some(b'{') => self.deserialize_map(visitor),
| Some(b'[') => serde_json::Deserializer::from_slice(self.record_next()) | Some(b'[') => serde_json::Deserializer::from_slice(self.record_next())
+2 -1
View File
@@ -8,7 +8,8 @@ pub trait MessageTemplate: Template {
#[derive(Template)] #[derive(Template)]
#[template(path = "mail/change_email.txt")] #[template(path = "mail/change_email.txt")]
pub struct ChangeEmail<'a> { pub struct ChangeEmail<'a> {
pub user_id: &'a UserId, pub server_name: &'a str,
pub user_id: Option<&'a UserId>,
pub verification_link: String, pub verification_link: String,
} }
+5 -2
View File
@@ -2,9 +2,12 @@
{% block content -%} {% block content -%}
Hello! Hello!
{% if let Some(user_id) = user_id -%}
Somebody, probably you, tried to associate this email address with the Matrix account {{ user_id }}. Somebody, probably you, tried to associate this email address with the Matrix account {{ user_id }}.
If that's your account, and this is your email address, click this link to proceed: {%- else -%}
Somebody, probably you, tried to associate this email address with a Matrix account on {{ server_name }}.
{%- endif %}
If that was you, and this is your email address, click this link to proceed:
{{ verification_link }} {{ verification_link }}
Otherwise, you can ignore this email. The above link will expire in one hour. Otherwise, you can ignore this email. The above link will expire in one hour.
{%- endblock %} {%- endblock %}
+5 -5
View File
@@ -227,15 +227,15 @@ impl Service {
/// ///
/// [`Self::get_localpart_for_email`] may be used if only the email is /// [`Self::get_localpart_for_email`] may be used if only the email is
/// known. /// known.
pub async fn disassociate_localpart_email(&self, localpart: &str) { pub async fn disassociate_localpart_email(&self, localpart: &str) -> Option<Address> {
let email = self let email = self.get_email_for_localpart(localpart).await?;
.get_email_for_localpart(localpart)
.await
.expect("localpart has no email associated");
self.db.localpart_email.remove(localpart); self.db.localpart_email.remove(localpart);
self.db self.db
.email_localpart .email_localpart
.remove(<Address as AsRef<str>>::as_ref(&email)); .remove(<Address as AsRef<str>>::as_ref(&email));
Some(email)
} }
/// Get the email associated with a localpart, if one exists. /// Get the email associated with a localpart, if one exists.