mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-05-26 20:49:55 +00:00
feat: Add admin commands for managing users' email addresses
This commit is contained in:
Generated
+2
@@ -973,6 +973,7 @@ dependencies = [
|
|||||||
"conduwuit_service",
|
"conduwuit_service",
|
||||||
"const-str",
|
"const-str",
|
||||||
"futures",
|
"futures",
|
||||||
|
"lettre",
|
||||||
"log",
|
"log",
|
||||||
"ruma",
|
"ruma",
|
||||||
"serde-saphyr",
|
"serde-saphyr",
|
||||||
@@ -1002,6 +1003,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",
|
||||||
|
|||||||
@@ -2046,6 +2046,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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -2457,6 +2457,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
|
||||||
|
|||||||
@@ -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
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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<'_> {
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user