diff --git a/conduwuit-example.toml b/conduwuit-example.toml index 64c46b2ec..e71d384a9 100644 --- a/conduwuit-example.toml +++ b/conduwuit-example.toml @@ -2069,10 +2069,12 @@ # #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 -# 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 diff --git a/src/admin/user/commands.rs b/src/admin/user/commands.rs index 7b854e587..b7f129fd2 100644 --- a/src/admin/user/commands.rs +++ b/src/admin/user/commands.rs @@ -1121,7 +1121,7 @@ pub(super) async fn get_user_by_email(&self, email: String) -> Result { self.bail_restricted()?; 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 { @@ -1147,7 +1147,7 @@ pub(super) async fn change_email(&self, user_id: String, email: Option) let user_id = parse_local_user_id(self.services, &user_id)?; 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() { diff --git a/src/api/client/account/mod.rs b/src/api/client/account/mod.rs index 8fe5d2eeb..3e450a46a 100644 --- a/src/api/client/account/mod.rs +++ b/src/api/client/account/mod.rs @@ -13,9 +13,8 @@ use ruma::{ api::client::{ account::{ ThirdPartyIdRemovalStatus, change_password, check_registration_token_validity, - deactivate, get_3pids, get_username_availability, - request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn, - request_password_change_token_via_email, whoami, + deactivate, get_username_availability, request_password_change_token_via_email, + whoami, }, uiaa::{AuthFlow, AuthType}, }, @@ -33,6 +32,7 @@ use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH, join_room_by_id_helper}; use crate::Ruma; pub(crate) mod register; +pub(crate) mod threepid; /// # `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` /// /// 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, body: Ruma, ) -> Result { 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 { @@ -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, -) -> Result { - 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, -) -> Result { - 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, -) -> Result { - Err!(Request(ThreepidDenied("Third party identifiers are not implemented"))) -} - /// # `GET /_matrix/client/v1/register/m.login.registration_token/validity` /// /// Checks if the provided registration token is valid at the time of checking. diff --git a/src/api/client/account/register.rs b/src/api/client/account/register.rs index 1e69f81df..6aa034018 100644 --- a/src/api/client/account/register.rs +++ b/src/api/client/account/register.rs @@ -569,13 +569,13 @@ async fn determine_registration_user_id( /// # `POST /_matrix/client/v3/register/email/requestToken` /// -/// Requests a validation email for the purpose of registering a new account -pub(crate) async fn register_request_token_route( +/// Requests a validation email for the purpose of registering a new account. +pub(crate) async fn request_registration_token_via_email_route( State(services): State, body: Ruma, ) -> Result { 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 @@ -584,7 +584,7 @@ pub(crate) async fn register_request_token_route( .await .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 diff --git a/src/api/client/account/threepid.rs b/src/api/client/account/threepid.rs new file mode 100644 index 000000000..60b305493 --- /dev/null +++ b/src/api/client/account/threepid.rs @@ -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, + body: Ruma, +) -> Result { + 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, + body: Ruma, +) -> Result { + 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, +) -> Result { + 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, + body: Ruma, +) -> Result { + 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, + body: Ruma, +) -> Result { + 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, + }) +} diff --git a/src/api/client/capabilities.rs b/src/api/client/capabilities.rs index afa61498e..405d11a0e 100644 --- a/src/api/client/capabilities.rs +++ b/src/api/client/capabilities.rs @@ -30,8 +30,10 @@ pub(crate) async fn get_capabilities_route( default: services.server.config.default_room_version.clone(), }; - // we do not implement 3PID stuff - capabilities.thirdparty_id_changes = ThirdPartyIdChangesCapability { enabled: false }; + // Only allow 3pid changes if SMTP is configured + capabilities.thirdparty_id_changes = ThirdPartyIdChangesCapability { + enabled: services.mailer.mailer().is_some(), + }; capabilities.get_login_token = GetLoginTokenCapability { enabled: services.server.config.login_via_existing_session, @@ -51,7 +53,7 @@ pub(crate) async fn get_capabilities_route( .await { // Advertise suspension API - capabilities.set("uk.timedout.msc4323", json!({"suspend":true, "lock": false}))?; + capabilities.set("uk.timedout.msc4323", json!({"suspend": true, "lock": false}))?; } Ok(get_capabilities::v3::Response { capabilities }) diff --git a/src/api/router.rs b/src/api/router.rs index 66951bd83..c8d5c8029 100644 --- a/src/api/router.rs +++ b/src/api/router.rs @@ -29,7 +29,7 @@ pub fn build(router: Router, server: &Server) -> Router { .ruma_route(&client::get_supported_versions_route) .ruma_route(&client::get_register_available_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::login_route) .ruma_route(&client::login_token_route) @@ -37,11 +37,13 @@ pub fn build(router: Router, server: &Server) -> Router { .ruma_route(&client::logout_route) .ruma_route(&client::logout_all_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::third_party_route) - .ruma_route(&client::request_3pid_management_token_via_email_route) - .ruma_route(&client::request_3pid_management_token_via_msisdn_route) + .ruma_route(&client::threepid::third_party_route) + .ruma_route(&client::threepid::request_3pid_management_token_via_email_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::get_capabilities_route) .ruma_route(&client::get_pushrules_all_route) diff --git a/src/database/de.rs b/src/database/de.rs index 21b9311ec..efb117b52 100644 --- a/src/database/de.rs +++ b/src/database/de.rs @@ -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)) )] fn deserialize_any>(self, visitor: V) -> Result { - debug_assert_eq!( - conduwuit::debug::type_name::(), - "serde_json::value::de::::deserialize::ValueVisitor", - "deserialize_any: type not expected" - ); - match self.record_peek_byte() { | Some(b'{') => self.deserialize_map(visitor), | Some(b'[') => serde_json::Deserializer::from_slice(self.record_next()) diff --git a/src/service/mailer/messages.rs b/src/service/mailer/messages.rs index 441c38490..ad22ed94b 100644 --- a/src/service/mailer/messages.rs +++ b/src/service/mailer/messages.rs @@ -8,7 +8,8 @@ pub trait MessageTemplate: Template { #[derive(Template)] #[template(path = "mail/change_email.txt")] pub struct ChangeEmail<'a> { - pub user_id: &'a UserId, + pub server_name: &'a str, + pub user_id: Option<&'a UserId>, pub verification_link: String, } diff --git a/src/service/templates/mail/change_email.txt b/src/service/templates/mail/change_email.txt index 1b744cdc8..7e82d1535 100644 --- a/src/service/templates/mail/change_email.txt +++ b/src/service/templates/mail/change_email.txt @@ -2,9 +2,12 @@ {% block content -%} Hello! - +{% if let Some(user_id) = 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 }} Otherwise, you can ignore this email. The above link will expire in one hour. {%- endblock %} diff --git a/src/service/threepid/mod.rs b/src/service/threepid/mod.rs index 8db752ad7..2e1375f80 100644 --- a/src/service/threepid/mod.rs +++ b/src/service/threepid/mod.rs @@ -227,15 +227,15 @@ impl Service { /// /// [`Self::get_localpart_for_email`] may be used if only the email is /// known. - pub async fn disassociate_localpart_email(&self, localpart: &str) { - let email = self - .get_email_for_localpart(localpart) - .await - .expect("localpart has no email associated"); + pub async fn disassociate_localpart_email(&self, localpart: &str) -> Option
{ + let email = self.get_email_for_localpart(localpart).await?; + self.db.localpart_email.remove(localpart); self.db .email_localpart .remove(
>::as_ref(&email)); + + Some(email) } /// Get the email associated with a localpart, if one exists.