Compare commits

...

3 Commits

Author SHA1 Message Date
Ginger 19204b097d chore: News fragments 2026-04-23 13:19:58 -04:00
Ginger 567d809efe fix: Forbid removing emails if they're required to register 2026-04-23 13:17:48 -04:00
Ginger 8171e3d614 fix: Remove a user's existing email before adding a new one 2026-04-23 13:05:21 -04:00
7 changed files with 54 additions and 1 deletions
+1
View File
@@ -0,0 +1 @@
Users will now be prevented from removing their email if the server is configured to require an email when registering an account.
+1
View File
@@ -0,0 +1 @@
Fixed a situation where multiple email addresses could be associated with one user when that user changes their email address.
+3
View File
@@ -2104,6 +2104,9 @@
# Whether to require that users provide an email address when they
# register.
#
# If either this option or `require_email_for_token_registration` are set,
# users will not be allowed to remove their email address.
#
#require_email_for_registration = false
# Whether to require that users who register with a registration token
+12
View File
@@ -53,6 +53,10 @@ 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> {
if !services.threepid.email_requirement().may_change() {
return Err!(Request(Forbidden("You may not change your email address.")));
}
let Ok(email) = Address::try_from(body.email.clone()) else {
return Err!(Request(InvalidParam("Invalid email address.")));
};
@@ -105,6 +109,10 @@ pub(crate) async fn add_3pid_route(
) -> Result<add_3pid::v3::Response> {
let sender_user = body.sender_user();
if !services.threepid.email_requirement().may_change() {
return Err!(Request(Forbidden("You may not change your email address.")));
}
// Require password auth to add an email
let _ = services
.uiaa
@@ -138,6 +146,10 @@ pub(crate) async fn delete_3pid_route(
});
}
if !services.threepid.email_requirement().may_remove() {
return Err!(Request(Forbidden("You may not remove your email address.")));
}
if services
.threepid
.disassociate_localpart_email(sender_user.localpart())
+1 -1
View File
@@ -32,7 +32,7 @@ pub(crate) async fn get_capabilities_route(
// Only allow 3pid changes if SMTP is configured
capabilities.thirdparty_id_changes = ThirdPartyIdChangesCapability {
enabled: services.mailer.mailer().is_some(),
enabled: services.threepid.email_requirement().may_change(),
};
capabilities.get_login_token = GetLoginTokenCapability {
+3
View File
@@ -2516,6 +2516,9 @@ pub struct SmtpConfig {
/// Whether to require that users provide an email address when they
/// register.
///
/// If either this option or `require_email_for_token_registration` are set,
/// users will not be allowed to remove their email address.
///
/// default: false
#[serde(default)]
pub require_email_for_registration: bool,
+33
View File
@@ -25,6 +25,23 @@ pub struct Service {
ratelimiter: DefaultKeyedRateLimiter<Address>,
}
pub enum EmailRequirement {
/// Users may change their email, but cannot remove it entirely.
Required,
/// Users may change or remove their email.
Optional,
/// Users may not change their email at all.
Unavailable,
}
impl EmailRequirement {
#[must_use]
pub fn may_change(&self) -> bool { matches!(self, Self::Required | Self::Optional) }
#[must_use]
pub fn may_remove(&self) -> bool { matches!(self, Self::Optional) }
}
struct Data {
localpart_email: Arc<Map>,
email_localpart: Arc<Map>,
@@ -64,6 +81,19 @@ impl Service {
Quota::per_minute(nonzero!(10_u32)).allow_burst(nonzero!(2_u32));
const VALIDATION_URL_PATH: &str = "/_continuwuity/3pid/email/validate";
/// Check if users are required to have an email address.
pub fn email_requirement(&self) -> EmailRequirement {
if let Some(smtp) = &self.services.config.smtp {
if smtp.require_email_for_registration || smtp.require_email_for_token_registration {
EmailRequirement::Required
} else {
EmailRequirement::Optional
}
} else {
EmailRequirement::Unavailable
}
}
/// Send a validation message to an email address.
///
/// Returns the validation session ID on success.
@@ -234,6 +264,9 @@ pub async fn associate_localpart_email(
| None => {
// The supplied email is not already in use.
// Remove the user's existing email first.
let _ = self.disassociate_localpart_email(localpart).await;
let email: &str = email.as_ref();
self.db.localpart_email.insert(localpart, email);
self.db.email_localpart.insert(email, localpart);