feat: Add admin commands for managing users' email addresses

This commit is contained in:
Ginger
2026-03-31 02:20:59 +00:00
committed by Ellis Git
parent 7e79a544cf
commit 955da3a74f
16 changed files with 281 additions and 48 deletions
Generated
+2
View File
@@ -982,6 +982,7 @@ dependencies = [
"conduwuit_service", "conduwuit_service",
"const-str", "const-str",
"futures", "futures",
"lettre",
"log", "log",
"ruma", "ruma",
"serde-saphyr", "serde-saphyr",
@@ -1011,6 +1012,7 @@ dependencies = [
"hyper", "hyper",
"ipaddress", "ipaddress",
"itertools 0.14.0", "itertools 0.14.0",
"lettre",
"log", "log",
"rand 0.10.0", "rand 0.10.0",
"reqwest", "reqwest",
+2
View File
@@ -2050,6 +2050,8 @@
# #
# For most modern mail servers, format the URI like this: # For most modern mail servers, format the URI like this:
# `smtps://username:password@hostname:port` # `smtps://username:password@hostname:port`
# Note that you will need to URL-encode the username and password. If your username _is_
# your email address, you will need to replace the `@` with `%40`.
# #
# For a guide on the accepted URI syntax, consult Lettre's documentation: # For a guide on the accepted URI syntax, consult Lettre's documentation:
# https://docs.rs/lettre/latest/lettre/transport/smtp/struct.AsyncSmtpTransport.html#method.from_url # https://docs.rs/lettre/latest/lettre/transport/smtp/struct.AsyncSmtpTransport.html#method.from_url
+1
View File
@@ -80,6 +80,7 @@ conduwuit-macros.workspace = true
conduwuit-service.workspace = true conduwuit-service.workspace = true
const-str.workspace = true const-str.workspace = true
futures.workspace = true futures.workspace = true
lettre.workspace = true
log.workspace = true log.workspace = true
ruma.workspace = true ruma.workspace = true
serde_json.workspace = true serde_json.workspace = true
+29
View File
@@ -19,6 +19,7 @@ use conduwuit::{
warn, warn,
}; };
use futures::{FutureExt, StreamExt, TryStreamExt}; use futures::{FutureExt, StreamExt, TryStreamExt};
use lettre::message::Mailbox;
use ruma::{ use ruma::{
CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, OwnedRoomId, CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, OwnedRoomId,
OwnedRoomOrAliasId, OwnedServerName, RoomId, RoomVersionId, OwnedRoomOrAliasId, OwnedServerName, RoomId, RoomVersionId,
@@ -876,3 +877,31 @@ pub(super) async fn trim_memory(&self) -> Result {
writeln!(self, "done").await writeln!(self, "done").await
} }
#[admin_command]
pub(super) async fn send_test_email(&self) -> Result {
self.bail_restricted()?;
let mailer = self.services.mailer.expect_mailer()?;
let Some(sender) = self.sender else {
return Err!("No sender user provided in context");
};
let Some(email) = self
.services
.threepid
.get_email_for_localpart(sender.localpart())
.await
else {
return Err!("{} has no associated email address", sender);
};
mailer
.send(Mailbox::new(None, email.clone()), service::mailer::messages::Test)
.await?;
self.write_str(&format!("Test email successfully sent to {email}"))
.await?;
Ok(())
}
+3
View File
@@ -225,6 +225,9 @@ pub enum DebugCommand {
level: Option<i32>, level: Option<i32>,
}, },
/// Send a test email to the invoking admin's email address
SendTestEmail,
/// Developer test stubs /// Developer test stubs
#[command(subcommand)] #[command(subcommand)]
#[allow(non_snake_case)] #[allow(non_snake_case)]
+104
View File
@@ -11,6 +11,7 @@ use conduwuit::{
warn, warn,
}; };
use futures::{FutureExt, StreamExt}; use futures::{FutureExt, StreamExt};
use lettre::Address;
use ruma::{ use ruma::{
OwnedEventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, UserId, OwnedEventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, UserId,
events::{ events::{
@@ -1094,3 +1095,106 @@ pub(super) async fn enable_login(&self, user_id: String) -> Result {
self.write_str(&format!("{user_id} can now log in.")).await self.write_str(&format!("{user_id} can now log in.")).await
} }
#[admin_command]
pub(super) async fn get_email(&self, user_id: String) -> Result {
self.bail_restricted()?;
let user_id = parse_local_user_id(self.services, &user_id)?;
match self
.services
.threepid
.get_email_for_localpart(user_id.localpart())
.await
{
| Some(email) =>
self.write_str(&format!("{user_id} has the associated email address {email}."))
.await,
| None =>
self.write_str(&format!("{user_id} has no associated email address."))
.await,
}
}
#[admin_command]
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");
};
match self.services.threepid.get_localpart_for_email(&email).await {
| Some(localpart) => {
let user_id = OwnedUserId::parse(format!(
"@{localpart}:{}",
self.services.globals.server_name()
))
.unwrap();
self.write_str(&format!("{email} belongs to {user_id}."))
.await
},
| None =>
self.write_str(&format!("No user has {email} as their email address."))
.await,
}
}
#[admin_command]
pub(super) async fn change_email(&self, user_id: String, email: Option<String>) -> Result {
self.bail_restricted()?;
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");
};
if self.services.mailer.mailer().is_none() {
warn!("SMTP has not been configured on this server, emails cannot be sent.");
}
let current_email = self
.services
.threepid
.get_email_for_localpart(user_id.localpart())
.await;
match (current_email, new_email) {
| (None, None) =>
self.write_str(&format!(
"{user_id} already had no associated email. No changes have been made."
))
.await,
| (current_email, Some(new_email)) => {
self.services
.threepid
.associate_localpart_email(user_id.localpart(), &new_email)
.await?;
if let Some(current_email) = current_email {
self.write_str(&format!(
"The associated email of {user_id} has been changed from {current_email} to \
{new_email}."
))
.await
} else {
self.write_str(&format!(
"{user_id} has been associated with the email {new_email}."
))
.await
}
},
| (Some(current_email), None) => {
self.services
.threepid
.disassociate_localpart_email(user_id.localpart())
.await;
self.write_str(&format!(
"The associated email of {user_id} has been removed (it was {current_email})."
))
.await
},
}
}
+18
View File
@@ -35,6 +35,24 @@ pub enum UserCommand {
username: String, username: String,
}, },
/// Get a user's associated email address.
GetEmail {
user_id: String,
},
/// Get the user with the given email address.
GetUserByEmail {
email: String,
},
/// Update or remove a user's email address.
///
/// If `email` is not supplied, the user's existing address wil be removed.
ChangeEmail {
user_id: String,
email: Option<String>,
},
/// Deactivate a user /// Deactivate a user
/// ///
/// User will be removed from all rooms by default. /// User will be removed from all rooms by default.
+11 -5
View File
@@ -168,19 +168,25 @@ pub(crate) async fn register_route(
let password = if is_guest { None } else { body.password.as_deref() }; let password = if is_guest { None } else { body.password.as_deref() };
// Create user // If the user registered with an email, associate it with their account.
services.users.create(&user_id, password, None).await?; // Do this _before_ creating the user to make sure that, if their email is
// already in use, we don't make them an account.
// If the user registered with an email, associate it with their account //
// Note that this should only rarely cause a bailout because email uniqueness is
// also checked by /requestToken.
#[allow(clippy::collapsible_if)] #[allow(clippy::collapsible_if)]
if let Some(identity) = identity { if let Some(identity) = identity {
if let Some(email) = identity.email { if let Some(email) = identity.email {
services services
.threepid .threepid
.associate_localpart_email(user_id.localpart(), email); .associate_localpart_email(user_id.localpart(), &email)
.await?;
} }
} }
// Create user
services.users.create(&user_id, password, None).await?;
// Set an initial display name // Set an initial display name
let mut displayname = user_id.localpart().to_owned(); let mut displayname = user_id.localpart().to_owned();
+3
View File
@@ -2461,6 +2461,9 @@ pub struct SmtpConfig {
/// ///
/// For most modern mail servers, format the URI like this: /// For most modern mail servers, format the URI like this:
/// `smtps://username:password@hostname:port` /// `smtps://username:password@hostname:port`
/// Note that you will need to URL-encode the username and password. If your
/// username _is_ your email address, you will need to replace the `@` with
/// `%40`.
/// ///
/// For a guide on the accepted URI syntax, consult Lettre's documentation: /// For a guide on the accepted URI syntax, consult Lettre's documentation:
/// https://docs.rs/lettre/latest/lettre/transport/smtp/struct.AsyncSmtpTransport.html#method.from_url /// https://docs.rs/lettre/latest/lettre/transport/smtp/struct.AsyncSmtpTransport.html#method.from_url
+20
View File
@@ -49,10 +49,18 @@ pub(super) static MAPS: &[Descriptor] = &[
name: "bannedroomids", name: "bannedroomids",
..descriptor::RANDOM_SMALL ..descriptor::RANDOM_SMALL
}, },
Descriptor {
name: "clientsecret_validationsessionid",
..descriptor::RANDOM_SMALL
},
Descriptor { Descriptor {
name: "disabledroomids", name: "disabledroomids",
..descriptor::RANDOM_SMALL ..descriptor::RANDOM_SMALL
}, },
Descriptor {
name: "email_localpart",
..descriptor::RANDOM_SMALL
},
Descriptor { Descriptor {
name: "eventid_outlierpdu", name: "eventid_outlierpdu",
cache_disp: CacheDisp::SharedWith("pduid_pdu"), cache_disp: CacheDisp::SharedWith("pduid_pdu"),
@@ -100,6 +108,10 @@ pub(super) static MAPS: &[Descriptor] = &[
name: "lazyloadedids", name: "lazyloadedids",
..descriptor::RANDOM_SMALL ..descriptor::RANDOM_SMALL
}, },
Descriptor {
name: "localpart_email",
..descriptor::RANDOM_SMALL
},
Descriptor { Descriptor {
name: "mediaid_file", name: "mediaid_file",
..descriptor::RANDOM_SMALL ..descriptor::RANDOM_SMALL
@@ -458,4 +470,12 @@ pub(super) static MAPS: &[Descriptor] = &[
name: "userroomid_invitesender", name: "userroomid_invitesender",
..descriptor::RANDOM_SMALL ..descriptor::RANDOM_SMALL
}, },
Descriptor {
name: "validationsessionid_session",
..descriptor::RANDOM_SMALL
},
Descriptor {
name: "validationsessionid_token",
..descriptor::RANDOM_SMALL
},
]; ];
+7 -7
View File
@@ -8,8 +8,8 @@ pub trait MessageTemplate: Template {
#[derive(Template)] #[derive(Template)]
#[template(path = "mail/change_email.txt.j2")] #[template(path = "mail/change_email.txt.j2")]
pub struct ChangeEmail<'a> { pub struct ChangeEmail<'a> {
user_id: &'a UserId, pub user_id: &'a UserId,
verification_link: &'a str, pub verification_link: String,
} }
impl MessageTemplate for ChangeEmail<'_> { impl MessageTemplate for ChangeEmail<'_> {
@@ -19,8 +19,8 @@ impl MessageTemplate for ChangeEmail<'_> {
#[derive(Template)] #[derive(Template)]
#[template(path = "mail/new_account.txt.j2")] #[template(path = "mail/new_account.txt.j2")]
pub struct NewAccount<'a> { pub struct NewAccount<'a> {
server_name: &'a str, pub server_name: &'a str,
verification_link: &'a str, pub verification_link: String,
} }
impl MessageTemplate for NewAccount<'_> { impl MessageTemplate for NewAccount<'_> {
@@ -30,9 +30,9 @@ impl MessageTemplate for NewAccount<'_> {
#[derive(Template)] #[derive(Template)]
#[template(path = "mail/password_reset.txt.j2")] #[template(path = "mail/password_reset.txt.j2")]
pub struct PasswordReset<'a> { pub struct PasswordReset<'a> {
display_name: &'a str, pub display_name: Option<&'a str>,
user_id: &'a UserId, pub user_id: &'a UserId,
verification_link: &'a str, pub verification_link: String,
} }
impl MessageTemplate for PasswordReset<'_> { impl MessageTemplate for PasswordReset<'_> {
+7 -1
View File
@@ -3,7 +3,7 @@ use std::sync::Arc;
use conduwuit::{Err, Result, err, info}; use conduwuit::{Err, Result, err, info};
use lettre::{ use lettre::{
AsyncSmtpTransport, AsyncTransport, Tokio1Executor, AsyncSmtpTransport, AsyncTransport, Tokio1Executor,
message::{Mailbox, MessageBuilder}, message::{Mailbox, MessageBuilder, header::ContentType},
}; };
use crate::{Args, mailer::messages::MessageTemplate}; use crate::{Args, mailer::messages::MessageTemplate};
@@ -65,6 +65,11 @@ impl Service {
.as_ref() .as_ref()
.map(|(sender, transport)| Mailer { sender, transport }) .map(|(sender, transport)| Mailer { sender, transport })
} }
pub fn expect_mailer(&self) -> Result<Mailer<'_>> {
self.mailer()
.ok_or_else(|| err!("SMTP is not configured on this server"))
}
} }
pub struct Mailer<'a> { pub struct Mailer<'a> {
@@ -89,6 +94,7 @@ impl Mailer<'_> {
.to(recipient) .to(recipient)
.subject(subject) .subject(subject)
.date_now() .date_now()
.header(ContentType::TEXT_PLAIN)
.body(body) .body(body)
.expect("should have been able to construct message"); .expect("should have been able to construct message");
@@ -1,7 +1,11 @@
{% extends "_base.txt.j2" %} {% extends "_base.txt.j2" %}
{% block content -%} {% block content -%}
{%- if let Some(display_name) = display_name -%}
Hello {{ display_name }} ({{ user_id }}), Hello {{ display_name }} ({{ user_id }}),
{%- else -%}
Hello {{ user_id }},
{%- endif %}
Somebody, probably you, tried to reset your Matrix account's password. Somebody, probably you, tried to reset your Matrix account's password.
If you requested for your password to be reset, click this link to proceed: If you requested for your password to be reset, click this link to proceed:
+9 -8
View File
@@ -6,6 +6,7 @@ use std::{
use conduwuit::utils; use conduwuit::utils;
use database::{Database, Deserialized, Map}; use database::{Database, Deserialized, Map};
use lettre::Address; use lettre::Address;
use ruma::{ClientSecret, OwnedClientSecret, OwnedSessionId};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
pub(super) struct Data { pub(super) struct Data {
@@ -20,11 +21,11 @@ pub(super) struct Data {
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub(crate) struct ValidationSession { pub(crate) struct ValidationSession {
/// The session's ID /// The session's ID
pub session_id: String, pub session_id: OwnedSessionId,
/// The email address which is being validated /// The email address which is being validated
pub email: Address, pub email: Address,
/// The client's supplied client secret /// The client's supplied client secret
pub client_secret: String, pub client_secret: OwnedClientSecret,
/// Whether the email address has been validated successfully yet /// Whether the email address has been validated successfully yet
pub(super) has_been_validated: bool, pub(super) has_been_validated: bool,
} }
@@ -62,9 +63,9 @@ impl PartialEq<str> for ValidationToken {
impl Data { impl Data {
pub(super) fn new(db: &Arc<Database>) -> Self { pub(super) fn new(db: &Arc<Database>) -> Self {
Self { Self {
clientsecret_sessionid: db["clientsecret_sessionid"].clone(), clientsecret_sessionid: db["clientsecret_validationsessionid"].clone(),
sessionid_session: db["sessionid_session"].clone(), sessionid_session: db["validationsessionid_session"].clone(),
sessionid_token: db["sessionid_token"].clone(), sessionid_token: db["validationsessionid_token"].clone(),
localpart_email: db["localpart_email"].clone(), localpart_email: db["localpart_email"].clone(),
email_localpart: db["email_localpart"].clone(), email_localpart: db["email_localpart"].clone(),
} }
@@ -74,8 +75,8 @@ impl Data {
pub(super) fn create_session( pub(super) fn create_session(
&self, &self,
email: Address, email: Address,
session_id: String, session_id: OwnedSessionId,
client_secret: String, client_secret: OwnedClientSecret,
token: ValidationToken, token: ValidationToken,
) { ) {
let session = ValidationSession { let session = ValidationSession {
@@ -103,7 +104,7 @@ impl Data {
/// Get a validation session by client secret. /// Get a validation session by client secret.
pub(super) async fn get_session_by_secret( pub(super) async fn get_session_by_secret(
&self, &self,
client_secret: &str, client_secret: &ClientSecret,
) -> Option<ValidationSession> { ) -> Option<ValidationSession> {
let session_id: String = self let session_id: String = self
.clientsecret_sessionid .clientsecret_sessionid
+60 -26
View File
@@ -10,15 +10,16 @@ use crate::{
}; };
mod data; mod data;
use conduwuit::{Err, Result, utils}; use conduwuit::{Err, Result, result::FlatOk, utils};
use data::{Data, ValidationToken}; use data::{Data, ValidationToken};
use database::Deserialized; use database::Deserialized;
use lettre::{Address, message::Mailbox}; use lettre::{Address, message::Mailbox};
use ruma::{ClientSecret, OwnedClientSecret, OwnedSessionId};
pub struct Service { pub struct Service {
db: Data, db: Data,
services: Services, services: Services,
send_attempts: Mutex<HashMap<(String, Address), usize>>, send_attempts: Mutex<HashMap<(OwnedClientSecret, Address), usize>>,
} }
struct Services { struct Services {
@@ -45,20 +46,23 @@ impl Service {
const RANDOM_SID_LENGTH: usize = 16; const RANDOM_SID_LENGTH: usize = 16;
const VALIDATION_URL_PATH: &str = "/_continuwuity/3pid/email/validate"; const VALIDATION_URL_PATH: &str = "/_continuwuity/3pid/email/validate";
#[must_use]
pub fn generate_session_id() -> OwnedSessionId {
OwnedSessionId::parse(utils::random_string(Self::RANDOM_SID_LENGTH)).unwrap()
}
/// Send a validation message to an email address. /// Send a validation message to an email address.
/// ///
/// Returns the validation session ID on success. /// Returns the validation session ID on success.
#[allow(clippy::impl_trait_in_params)] #[allow(clippy::impl_trait_in_params)]
pub async fn send_validation_email<Template: MessageTemplate>( pub async fn send_validation_email<Template: MessageTemplate>(
&self, &self,
recipient: Address, recipient: Mailbox,
prepare_body: impl FnOnce(String) -> Template, prepare_body: impl FnOnce(String) -> Template,
client_secret: &str, client_secret: &ClientSecret,
send_attempt: usize, send_attempt: usize,
) -> Result<String> { ) -> Result<OwnedSessionId> {
let Some(mailer) = self.services.mailer.mailer() else { let mailer = self.services.mailer.expect_mailer()?;
return Err!("SMTP is not configured");
};
let (session_id, ValidationToken { token, .. }) = let (session_id, ValidationToken { token, .. }) =
match self.db.get_session_by_secret(client_secret).await { match self.db.get_session_by_secret(client_secret).await {
@@ -102,11 +106,11 @@ impl Service {
}, },
// If no session exists, create a new one. // If no session exists, create a new one.
| None => { | None => {
let session_id = utils::random_string(Self::RANDOM_SID_LENGTH); let session_id = Self::generate_session_id();
let token = ValidationToken::new_random(); let token = ValidationToken::new_random();
self.db.create_session( self.db.create_session(
recipient.clone(), recipient.email.clone(),
session_id.clone(), session_id.clone(),
client_secret.to_owned(), client_secret.to_owned(),
token.clone(), token.clone(),
@@ -125,11 +129,9 @@ impl Service {
validation_url validation_url
.query_pairs_mut() .query_pairs_mut()
.append_pair("session_id", &session_id) .append_pair("session_id", session_id.as_ref())
.append_pair("client_secret", client_secret)
.append_pair("token", &token); .append_pair("token", &token);
let recipient = Mailbox::new(None, recipient);
let message = prepare_body(validation_url.to_string()); let message = prepare_body(validation_url.to_string());
mailer.send(recipient, message).await?; mailer.send(recipient, message).await?;
@@ -137,10 +139,10 @@ impl Service {
Ok(session_id) Ok(session_id)
} }
/// Attempt to mark a validation session as valid using a validation token.
pub async fn try_validate_session( pub async fn try_validate_session(
&self, &self,
session_id: &str, session_id: &str,
client_secret: &str,
supplied_token: &str, supplied_token: &str,
) -> Result<(), Cow<'static, str>> { ) -> Result<(), Cow<'static, str>> {
let Some(session) = self.db.get_session(session_id).await else { let Some(session) = self.db.get_session(session_id).await else {
@@ -151,15 +153,12 @@ impl Service {
return Ok(()); return Ok(());
} }
if session.client_secret != client_secret {
return Err("Invalid client secret for session".into());
}
let token = self let token = self
.db .db
.get_session_validation_token(&session) .get_session_validation_token(&session)
.await .await
.expect("valid session should have a token"); .expect("valid session should have a token");
if token != *supplied_token || !token.is_valid() { if token != *supplied_token || !token.is_valid() {
return Err("Validation token is invalid or expired, please request a new one".into()); return Err("Validation token is invalid or expired, please request a new one".into());
} }
@@ -169,10 +168,12 @@ impl Service {
Ok(()) Ok(())
} }
/// Consume a validated validation session, removing it from the database
/// and returning the newly validated email address.
pub async fn consume_valid_session( pub async fn consume_valid_session(
&self, &self,
session_id: &str, session_id: &str,
client_secret: &str, client_secret: &ClientSecret,
) -> Result<Address, Cow<'static, str>> { ) -> Result<Address, Cow<'static, str>> {
let Some(session) = self.db.get_session(session_id).await else { let Some(session) = self.db.get_session(session_id).await else {
return Err("Validation session does not exist".into()); return Err("Validation session does not exist".into());
@@ -183,14 +184,38 @@ impl Service {
self.db.remove_session(session).await; self.db.remove_session(session).await;
Ok(email) Ok(email)
} else { } else {
Err("Validation failed. Did you use the link that was sent to you?".into()) Err("This email address has not been validated. Did you use the link that was sent \
to you?"
.into())
} }
} }
/// Associate a localpart with an email address. /// Associate a localpart with an email address.
pub fn associate_localpart_email(&self, localpart: &str, email: Address) { pub async fn associate_localpart_email(
self.db.localpart_email.raw_put(localpart, &email); &self,
self.db.email_localpart.put_raw(email, localpart); localpart: &str,
email: &Address,
) -> Result<()> {
match self.get_localpart_for_email(email).await {
| Some(existing_localpart) if existing_localpart != localpart => {
// Another account is already using the supplied email
Err!(Request(ThreepidInUse("This email address is already in use.")))
},
| Some(_) => {
// The supplied localpart is already associated with the supplied email,
// no changes are necessary
Ok(())
},
| None => {
// The supplied email is not already in use
let email: &str = email.as_ref();
self.db.localpart_email.insert(localpart, email);
self.db.email_localpart.insert(email, localpart);
Ok(())
},
}
} }
/// Given a localpart, remove its corresponding email address. /// Given a localpart, remove its corresponding email address.
@@ -203,7 +228,9 @@ impl Service {
.await .await
.expect("localpart has no email associated"); .expect("localpart has no email associated");
self.db.localpart_email.remove(localpart); self.db.localpart_email.remove(localpart);
self.db.email_localpart.del(&email); self.db
.email_localpart
.remove(<Address as AsRef<str>>::as_ref(&email));
} }
/// Get the email associated with a localpart, if one exists. /// Get the email associated with a localpart, if one exists.
@@ -212,12 +239,19 @@ impl Service {
.localpart_email .localpart_email
.get(localpart) .get(localpart)
.await .await
.deserialized() .deserialized::<String>()
.ok() .ok()
.map(TryInto::try_into)
.flat_ok()
} }
/// Get the localpart associated with an email, if one exists. /// Get the localpart associated with an email, if one exists.
pub async fn get_localpart_for_email(&self, email: &Address) -> Option<String> { pub async fn get_localpart_for_email(&self, email: &Address) -> Option<String> {
self.db.email_localpart.qry(email).await.deserialized().ok() self.db
.email_localpart
.get(<Address as AsRef<str>>::as_ref(email))
.await
.deserialized()
.ok()
} }
} }
+1 -1
View File
@@ -301,7 +301,7 @@ impl Service {
match self match self
.services .services
.threepid .threepid
.consume_valid_session(sid.as_str(), client_secret.as_str()) .consume_valid_session(sid.as_str(), client_secret)
.await .await
{ {
| Ok(email) => { | Ok(email) => {