You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-08-06 06:02:40 +03:00
Actually send emails for recovery
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -3534,6 +3534,7 @@ dependencies = [
|
|||||||
"mas-email",
|
"mas-email",
|
||||||
"mas-i18n",
|
"mas-i18n",
|
||||||
"mas-matrix",
|
"mas-matrix",
|
||||||
|
"mas-router",
|
||||||
"mas-storage",
|
"mas-storage",
|
||||||
"mas-storage-pg",
|
"mas-storage-pg",
|
||||||
"mas-templates",
|
"mas-templates",
|
||||||
|
@@ -170,8 +170,13 @@ impl Options {
|
|||||||
let worker_name = Alphanumeric.sample_string(&mut rng, 10);
|
let worker_name = Alphanumeric.sample_string(&mut rng, 10);
|
||||||
|
|
||||||
info!(worker_name, "Starting task worker");
|
info!(worker_name, "Starting task worker");
|
||||||
let monitor =
|
let monitor = mas_tasks::init(
|
||||||
mas_tasks::init(&worker_name, &pool, &mailer, homeserver_connection.clone())
|
&worker_name,
|
||||||
|
&pool,
|
||||||
|
&mailer,
|
||||||
|
homeserver_connection.clone(),
|
||||||
|
url_builder.clone(),
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
// TODO: grab the handle
|
// TODO: grab the handle
|
||||||
tokio::spawn(monitor.run());
|
tokio::spawn(monitor.run());
|
||||||
|
@@ -77,7 +77,7 @@ impl Options {
|
|||||||
let worker_name = Alphanumeric.sample_string(&mut rng, 10);
|
let worker_name = Alphanumeric.sample_string(&mut rng, 10);
|
||||||
|
|
||||||
info!(worker_name, "Starting task scheduler");
|
info!(worker_name, "Starting task scheduler");
|
||||||
let monitor = mas_tasks::init(&worker_name, &pool, &mailer, conn).await?;
|
let monitor = mas_tasks::init(&worker_name, &pool, &mailer, conn, url_builder).await?;
|
||||||
|
|
||||||
span.exit();
|
span.exit();
|
||||||
|
|
||||||
|
@@ -18,7 +18,7 @@ use lettre::{
|
|||||||
message::{Mailbox, MessageBuilder, MultiPart},
|
message::{Mailbox, MessageBuilder, MultiPart},
|
||||||
AsyncTransport, Message,
|
AsyncTransport, Message,
|
||||||
};
|
};
|
||||||
use mas_templates::{EmailVerificationContext, Templates, WithLanguage};
|
use mas_templates::{EmailRecoveryContext, EmailVerificationContext, Templates, WithLanguage};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::MailTransport;
|
use crate::MailTransport;
|
||||||
@@ -85,6 +85,28 @@ impl Mailer {
|
|||||||
Ok(message)
|
Ok(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn prepare_recovery_email(
|
||||||
|
&self,
|
||||||
|
to: Mailbox,
|
||||||
|
context: &WithLanguage<EmailRecoveryContext>,
|
||||||
|
) -> Result<Message, Error> {
|
||||||
|
let plain = self.templates.render_email_recovery_txt(context)?;
|
||||||
|
|
||||||
|
let html = self.templates.render_email_recovery_html(context)?;
|
||||||
|
|
||||||
|
let multipart = MultiPart::alternative_plain_html(plain, html);
|
||||||
|
|
||||||
|
let subject = self.templates.render_email_recovery_subject(context)?;
|
||||||
|
|
||||||
|
let message = self
|
||||||
|
.base_message()
|
||||||
|
.subject(subject.trim())
|
||||||
|
.to(to)
|
||||||
|
.multipart(multipart)?;
|
||||||
|
|
||||||
|
Ok(message)
|
||||||
|
}
|
||||||
|
|
||||||
/// Send the verification email to a user
|
/// Send the verification email to a user
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
@@ -112,6 +134,32 @@ impl Mailer {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send the recovery email to a user
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Will return `Err` if the email failed rendering or failed sending
|
||||||
|
#[tracing::instrument(
|
||||||
|
name = "email.recovery.send",
|
||||||
|
skip_all,
|
||||||
|
fields(
|
||||||
|
email.to = %to,
|
||||||
|
email.language = %context.language(),
|
||||||
|
user.id = %context.user().id,
|
||||||
|
user_recovery_session.id = %context.session().id,
|
||||||
|
),
|
||||||
|
err,
|
||||||
|
)]
|
||||||
|
pub async fn send_recovery_email(
|
||||||
|
&self,
|
||||||
|
to: Mailbox,
|
||||||
|
context: &WithLanguage<EmailRecoveryContext>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let message = self.prepare_recovery_email(to, context)?;
|
||||||
|
self.transport.send(message).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Test the connetion to the mail server
|
/// Test the connetion to the mail server
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
|
@@ -808,6 +808,31 @@ impl Route for AccountRecoveryProgress {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `GET|POST /recover/complete?ticket=:ticket`
|
||||||
|
#[derive(Default, Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct AccountRecoveryFinish {
|
||||||
|
ticket: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AccountRecoveryFinish {
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(ticket: String) -> Self {
|
||||||
|
Self { ticket }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Route for AccountRecoveryFinish {
|
||||||
|
type Query = AccountRecoveryFinish;
|
||||||
|
|
||||||
|
fn route() -> &'static str {
|
||||||
|
"/recover/complete"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn query(&self) -> Option<&Self::Query> {
|
||||||
|
Some(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// `GET /assets`
|
/// `GET /assets`
|
||||||
pub struct StaticAsset {
|
pub struct StaticAsset {
|
||||||
path: String,
|
path: String,
|
||||||
|
@@ -237,6 +237,12 @@ impl UrlBuilder {
|
|||||||
pub fn account_management_uri(&self) -> Url {
|
pub fn account_management_uri(&self) -> Url {
|
||||||
self.absolute_url_for(&crate::endpoints::Account::default())
|
self.absolute_url_for(&crate::endpoints::Account::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Account recovery link
|
||||||
|
#[must_use]
|
||||||
|
pub fn account_recovery_link(&self, ticket: String) -> Url {
|
||||||
|
self.absolute_url_for(&crate::endpoints::AccountRecoveryFinish::new(ticket))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@@ -446,6 +446,7 @@ mod jobs {
|
|||||||
///
|
///
|
||||||
/// * `user_recovery_session` - The user recovery session to send the
|
/// * `user_recovery_session` - The user recovery session to send the
|
||||||
/// email for
|
/// email for
|
||||||
|
/// * `language` - The locale to send the email in
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new(user_recovery_session: &UserRecoverySession) -> Self {
|
pub fn new(user_recovery_session: &UserRecoverySession) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
@@ -38,6 +38,7 @@ mas-data-model.workspace = true
|
|||||||
mas-email.workspace = true
|
mas-email.workspace = true
|
||||||
mas-i18n.workspace = true
|
mas-i18n.workspace = true
|
||||||
mas-matrix.workspace = true
|
mas-matrix.workspace = true
|
||||||
|
mas-router.workspace = true
|
||||||
mas-storage.workspace = true
|
mas-storage.workspace = true
|
||||||
mas-storage-pg.workspace = true
|
mas-storage-pg.workspace = true
|
||||||
mas-templates.workspace = true
|
mas-templates.workspace = true
|
||||||
|
@@ -17,6 +17,7 @@ use std::sync::Arc;
|
|||||||
use apalis_core::{executor::TokioExecutor, layers::extensions::Extension, monitor::Monitor};
|
use apalis_core::{executor::TokioExecutor, layers::extensions::Extension, monitor::Monitor};
|
||||||
use mas_email::Mailer;
|
use mas_email::Mailer;
|
||||||
use mas_matrix::HomeserverConnection;
|
use mas_matrix::HomeserverConnection;
|
||||||
|
use mas_router::UrlBuilder;
|
||||||
use mas_storage::{BoxClock, BoxRepository, Repository, SystemClock};
|
use mas_storage::{BoxClock, BoxRepository, Repository, SystemClock};
|
||||||
use mas_storage_pg::{DatabaseError, PgRepository};
|
use mas_storage_pg::{DatabaseError, PgRepository};
|
||||||
use rand::SeedableRng;
|
use rand::SeedableRng;
|
||||||
@@ -39,6 +40,7 @@ struct State {
|
|||||||
mailer: Mailer,
|
mailer: Mailer,
|
||||||
clock: SystemClock,
|
clock: SystemClock,
|
||||||
homeserver: Arc<dyn HomeserverConnection<Error = anyhow::Error>>,
|
homeserver: Arc<dyn HomeserverConnection<Error = anyhow::Error>>,
|
||||||
|
url_builder: UrlBuilder,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
@@ -47,12 +49,14 @@ impl State {
|
|||||||
clock: SystemClock,
|
clock: SystemClock,
|
||||||
mailer: Mailer,
|
mailer: Mailer,
|
||||||
homeserver: impl HomeserverConnection<Error = anyhow::Error> + 'static,
|
homeserver: impl HomeserverConnection<Error = anyhow::Error> + 'static,
|
||||||
|
url_builder: UrlBuilder,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
pool,
|
pool,
|
||||||
mailer,
|
mailer,
|
||||||
clock,
|
clock,
|
||||||
homeserver: Arc::new(homeserver),
|
homeserver: Arc::new(homeserver),
|
||||||
|
url_builder,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,6 +94,10 @@ impl State {
|
|||||||
pub fn matrix_connection(&self) -> &dyn HomeserverConnection<Error = anyhow::Error> {
|
pub fn matrix_connection(&self) -> &dyn HomeserverConnection<Error = anyhow::Error> {
|
||||||
self.homeserver.as_ref()
|
self.homeserver.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn url_builder(&self) -> &UrlBuilder {
|
||||||
|
&self.url_builder
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trait JobContextExt {
|
trait JobContextExt {
|
||||||
@@ -140,12 +148,14 @@ pub async fn init(
|
|||||||
pool: &Pool<Postgres>,
|
pool: &Pool<Postgres>,
|
||||||
mailer: &Mailer,
|
mailer: &Mailer,
|
||||||
homeserver: impl HomeserverConnection<Error = anyhow::Error> + 'static,
|
homeserver: impl HomeserverConnection<Error = anyhow::Error> + 'static,
|
||||||
|
url_builder: UrlBuilder,
|
||||||
) -> Result<Monitor<TokioExecutor>, sqlx::Error> {
|
) -> Result<Monitor<TokioExecutor>, sqlx::Error> {
|
||||||
let state = State::new(
|
let state = State::new(
|
||||||
pool.clone(),
|
pool.clone(),
|
||||||
SystemClock::default(),
|
SystemClock::default(),
|
||||||
mailer.clone(),
|
mailer.clone(),
|
||||||
homeserver,
|
homeserver,
|
||||||
|
url_builder,
|
||||||
);
|
);
|
||||||
let factory = PostgresStorageFactory::new(pool.clone());
|
let factory = PostgresStorageFactory::new(pool.clone());
|
||||||
let monitor = Monitor::new().executor(TokioExecutor::new());
|
let monitor = Monitor::new().executor(TokioExecutor::new());
|
||||||
|
@@ -14,13 +14,16 @@
|
|||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use apalis_core::{context::JobContext, executor::TokioExecutor, monitor::Monitor};
|
use apalis_core::{context::JobContext, executor::TokioExecutor, monitor::Monitor};
|
||||||
|
use mas_email::{Address, Mailbox};
|
||||||
|
use mas_i18n::DataLocale;
|
||||||
use mas_storage::{
|
use mas_storage::{
|
||||||
job::{JobWithSpanContext, SendAccountRecoveryEmailsJob},
|
job::{JobWithSpanContext, SendAccountRecoveryEmailsJob},
|
||||||
user::{UserEmailFilter, UserRecoveryRepository},
|
user::{UserEmailFilter, UserRecoveryRepository},
|
||||||
Pagination, RepositoryAccess,
|
Pagination, RepositoryAccess,
|
||||||
};
|
};
|
||||||
|
use mas_templates::{EmailRecoveryContext, TemplateContext};
|
||||||
use rand::distributions::{Alphanumeric, DistString};
|
use rand::distributions::{Alphanumeric, DistString};
|
||||||
use tracing::info;
|
use tracing::{error, info};
|
||||||
|
|
||||||
use crate::{storage::PostgresStorageFactory, JobContextExt, State};
|
use crate::{storage::PostgresStorageFactory, JobContextExt, State};
|
||||||
|
|
||||||
@@ -40,6 +43,8 @@ async fn send_account_recovery_email_job(
|
|||||||
) -> Result<(), anyhow::Error> {
|
) -> Result<(), anyhow::Error> {
|
||||||
let state = ctx.state();
|
let state = ctx.state();
|
||||||
let clock = state.clock();
|
let clock = state.clock();
|
||||||
|
let mailer = state.mailer();
|
||||||
|
let url_builder = state.url_builder();
|
||||||
let mut rng = state.rng();
|
let mut rng = state.rng();
|
||||||
let mut repo = state.repository().await?;
|
let mut repo = state.repository().await?;
|
||||||
|
|
||||||
@@ -58,6 +63,11 @@ async fn send_account_recovery_email_job(
|
|||||||
|
|
||||||
let mut cursor = Pagination::first(50);
|
let mut cursor = Pagination::first(50);
|
||||||
|
|
||||||
|
let lang: DataLocale = session
|
||||||
|
.locale
|
||||||
|
.parse()
|
||||||
|
.context("Invalid locale in database on recovery session")?;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let page = repo
|
let page = repo
|
||||||
.user_email()
|
.user_email()
|
||||||
@@ -72,13 +82,39 @@ async fn send_account_recovery_email_job(
|
|||||||
for email in page.edges {
|
for email in page.edges {
|
||||||
let ticket = Alphanumeric.sample_string(&mut rng, 32);
|
let ticket = Alphanumeric.sample_string(&mut rng, 32);
|
||||||
|
|
||||||
let _ticket = repo
|
let ticket = repo
|
||||||
.user_recovery()
|
.user_recovery()
|
||||||
.add_ticket(&mut rng, &clock, &session, &email, ticket)
|
.add_ticket(&mut rng, &clock, &session, &email, ticket)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
info!("Sending recovery email to {}", email.email);
|
let user_email = repo
|
||||||
// TODO
|
.user_email()
|
||||||
|
.lookup(email.id)
|
||||||
|
.await?
|
||||||
|
.context("User email not found")?;
|
||||||
|
|
||||||
|
let user = repo
|
||||||
|
.user()
|
||||||
|
.lookup(user_email.user_id)
|
||||||
|
.await?
|
||||||
|
.context("User not found")?;
|
||||||
|
|
||||||
|
let url = url_builder.account_recovery_link(ticket.ticket);
|
||||||
|
|
||||||
|
let address: Address = user_email.email.parse()?;
|
||||||
|
let mailbox = Mailbox::new(Some(user.username.clone()), address);
|
||||||
|
|
||||||
|
info!("Sending recovery email to {}", mailbox);
|
||||||
|
let context =
|
||||||
|
EmailRecoveryContext::new(user, session.clone(), url).with_language(lang.clone());
|
||||||
|
|
||||||
|
// XXX: we only log if the email fails to send, to avoid stopping the loop
|
||||||
|
if let Err(e) = mailer.send_recovery_email(mailbox, &context).await {
|
||||||
|
error!(
|
||||||
|
error = &e as &dyn std::error::Error,
|
||||||
|
"Failed to send recovery email"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
cursor = cursor.after(email.id);
|
cursor = cursor.after(email.id);
|
||||||
}
|
}
|
||||||
|
@@ -29,7 +29,7 @@ use http::{Method, Uri, Version};
|
|||||||
use mas_data_model::{
|
use mas_data_model::{
|
||||||
AuthorizationGrant, BrowserSession, Client, CompatSsoLogin, CompatSsoLoginState,
|
AuthorizationGrant, BrowserSession, Client, CompatSsoLogin, CompatSsoLoginState,
|
||||||
DeviceCodeGrant, UpstreamOAuthLink, UpstreamOAuthProvider, User, UserAgent, UserEmail,
|
DeviceCodeGrant, UpstreamOAuthLink, UpstreamOAuthProvider, User, UserAgent, UserEmail,
|
||||||
UserEmailVerification,
|
UserEmailVerification, UserRecoverySession,
|
||||||
};
|
};
|
||||||
use mas_i18n::DataLocale;
|
use mas_i18n::DataLocale;
|
||||||
use mas_router::{Account, GraphQL, PostAuthAction, UrlBuilder};
|
use mas_router::{Account, GraphQL, PostAuthAction, UrlBuilder};
|
||||||
@@ -751,6 +751,61 @@ where {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Context used by the `emails/recovery.{txt,html,subject}` templates
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct EmailRecoveryContext {
|
||||||
|
user: User,
|
||||||
|
session: UserRecoverySession,
|
||||||
|
recovery_link: Url,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EmailRecoveryContext {
|
||||||
|
/// Constructs a context for the recovery email
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(user: User, session: UserRecoverySession, recovery_link: Url) -> Self {
|
||||||
|
Self {
|
||||||
|
user,
|
||||||
|
session,
|
||||||
|
recovery_link,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the user associated with the recovery email
|
||||||
|
#[must_use]
|
||||||
|
pub fn user(&self) -> &User {
|
||||||
|
&self.user
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the recovery session associated with the recovery email
|
||||||
|
#[must_use]
|
||||||
|
pub fn session(&self) -> &UserRecoverySession {
|
||||||
|
&self.session
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TemplateContext for EmailRecoveryContext {
|
||||||
|
fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
User::samples(now, rng).into_iter().map(|user| {
|
||||||
|
let session = UserRecoverySession {
|
||||||
|
id: Ulid::from_datetime_with_source(now.into(), rng),
|
||||||
|
email: "hello@example.com".to_owned(),
|
||||||
|
user_agent: UserAgent::parse("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_4) AppleWebKit/536.30.1 (KHTML, like Gecko) Version/6.0.5 Safari/536.30.1".to_owned()),
|
||||||
|
ip_address: Some(IpAddr::from([192_u8, 0, 2, 1])),
|
||||||
|
locale: "en".to_owned(),
|
||||||
|
created_at: now,
|
||||||
|
consumed_at: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let link = "https://example.com/recovery/complete?ticket=abcdefghijklmnopqrstuvwxyz0123456789".parse().unwrap();
|
||||||
|
|
||||||
|
Self::new(user, session, link)
|
||||||
|
}).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Context used by the `emails/verification.{txt,html,subject}` templates
|
/// Context used by the `emails/verification.{txt,html,subject}` templates
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct EmailVerificationContext {
|
pub struct EmailVerificationContext {
|
||||||
|
@@ -56,7 +56,7 @@ pub struct WithCaptcha<T> {
|
|||||||
|
|
||||||
impl<T> WithCaptcha<T> {
|
impl<T> WithCaptcha<T> {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new(captcha: Option<mas_data_model::CaptchaConfig>, inner: T) -> Self {
|
pub(crate) fn new(captcha: Option<mas_data_model::CaptchaConfig>, inner: T) -> Self {
|
||||||
Self {
|
Self {
|
||||||
captcha: captcha.map(|captcha| Value::from_object(CaptchaConfig(captcha))),
|
captcha: captcha.map(|captcha| Value::from_object(CaptchaConfig(captcha))),
|
||||||
inner,
|
inner,
|
||||||
|
@@ -22,7 +22,6 @@ use std::{collections::HashSet, sync::Arc};
|
|||||||
use anyhow::Context as _;
|
use anyhow::Context as _;
|
||||||
use arc_swap::ArcSwap;
|
use arc_swap::ArcSwap;
|
||||||
use camino::{Utf8Path, Utf8PathBuf};
|
use camino::{Utf8Path, Utf8PathBuf};
|
||||||
use context::WithCaptcha;
|
|
||||||
use mas_i18n::Translator;
|
use mas_i18n::Translator;
|
||||||
use mas_router::UrlBuilder;
|
use mas_router::UrlBuilder;
|
||||||
use mas_spa::ViteManifest;
|
use mas_spa::ViteManifest;
|
||||||
@@ -44,13 +43,13 @@ mod macros;
|
|||||||
pub use self::{
|
pub use self::{
|
||||||
context::{
|
context::{
|
||||||
AppContext, CompatSsoContext, ConsentContext, DeviceConsentContext, DeviceLinkContext,
|
AppContext, CompatSsoContext, ConsentContext, DeviceConsentContext, DeviceLinkContext,
|
||||||
DeviceLinkFormField, EmailAddContext, EmailVerificationContext,
|
DeviceLinkFormField, EmailAddContext, EmailRecoveryContext, EmailVerificationContext,
|
||||||
EmailVerificationPageContext, EmptyContext, ErrorContext, FormPostContext, IndexContext,
|
EmailVerificationPageContext, EmptyContext, ErrorContext, FormPostContext, IndexContext,
|
||||||
LoginContext, LoginFormField, NotFoundContext, PolicyViolationContext, PostAuthContext,
|
LoginContext, LoginFormField, NotFoundContext, PolicyViolationContext, PostAuthContext,
|
||||||
PostAuthContextInner, ReauthContext, ReauthFormField, RecoveryStartContext,
|
PostAuthContextInner, ReauthContext, ReauthFormField, RecoveryStartContext,
|
||||||
RecoveryStartFormField, RegisterContext, RegisterFormField, SiteBranding, SiteConfigExt,
|
RecoveryStartFormField, RegisterContext, RegisterFormField, SiteBranding, SiteConfigExt,
|
||||||
SiteFeatures, TemplateContext, UpstreamExistingLinkContext, UpstreamRegister,
|
SiteFeatures, TemplateContext, UpstreamExistingLinkContext, UpstreamRegister,
|
||||||
UpstreamRegisterFormField, UpstreamSuggestLink, WithCsrf, WithLanguage,
|
UpstreamRegisterFormField, UpstreamSuggestLink, WithCaptcha, WithCsrf, WithLanguage,
|
||||||
WithOptionalSession, WithSession,
|
WithOptionalSession, WithSession,
|
||||||
},
|
},
|
||||||
forms::{FieldError, FormError, FormField, FormState, ToFormState},
|
forms::{FieldError, FormError, FormField, FormState, ToFormState},
|
||||||
@@ -360,6 +359,15 @@ register_templates! {
|
|||||||
/// Render the HTML error page
|
/// Render the HTML error page
|
||||||
pub fn render_error(ErrorContext) { "pages/error.html" }
|
pub fn render_error(ErrorContext) { "pages/error.html" }
|
||||||
|
|
||||||
|
/// Render the email recovery email (plain text variant)
|
||||||
|
pub fn render_email_recovery_txt(WithLanguage<EmailRecoveryContext>) { "emails/recovery.txt" }
|
||||||
|
|
||||||
|
/// Render the email recovery email (HTML text variant)
|
||||||
|
pub fn render_email_recovery_html(WithLanguage<EmailRecoveryContext>) { "emails/recovery.html" }
|
||||||
|
|
||||||
|
/// Render the email recovery subject
|
||||||
|
pub fn render_email_recovery_subject(WithLanguage<EmailRecoveryContext>) { "emails/recovery.subject" }
|
||||||
|
|
||||||
/// Render the email verification email (plain text variant)
|
/// Render the email verification email (plain text variant)
|
||||||
pub fn render_email_verification_txt(WithLanguage<EmailVerificationContext>) { "emails/verification.txt" }
|
pub fn render_email_verification_txt(WithLanguage<EmailVerificationContext>) { "emails/verification.txt" }
|
||||||
|
|
||||||
|
55
templates/emails/recovery.html
Normal file
55
templates/emails/recovery.html
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
{#
|
||||||
|
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-#}
|
||||||
|
|
||||||
|
{%- set _ = translator(lang) -%}
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" lang="{{ lang }}">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||||
|
<style type="text/css">
|
||||||
|
a#button:hover { background-color: #3C4045!important; }
|
||||||
|
a#button:active { background-color: #4C5158!important; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body style="
|
||||||
|
color: black;
|
||||||
|
background-color: white;
|
||||||
|
font-family: Inter, system-ui, ui-sans-serif, sans-serif;
|
||||||
|
">
|
||||||
|
{{ _("mas.emails.recovery.headline", server_name=branding.server_name) }}<br />
|
||||||
|
<br />
|
||||||
|
{{ _("mas.emails.recovery.click_button") }}<br />
|
||||||
|
<br />
|
||||||
|
<a id="button" href="{{ recovery_link }}" target="_blank" style="
|
||||||
|
display: inline-block;
|
||||||
|
transition: background-color 0.1s ease;
|
||||||
|
font-size: 18px;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #FFF;
|
||||||
|
background-color: #1B1D22;
|
||||||
|
padding: 16px 32px;
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
border-radius: 32px;
|
||||||
|
border-radius: 2rem;
|
||||||
|
text-decoration: none;
|
||||||
|
">{{ _("mas.emails.recovery.create_new_password") }}</a><br />
|
||||||
|
<br />
|
||||||
|
{{ _("mas.emails.recovery.you_can_ignore") }}
|
||||||
|
</body>
|
||||||
|
</html>
|
22
templates/emails/recovery.subject
Normal file
22
templates/emails/recovery.subject
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{#
|
||||||
|
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-#}
|
||||||
|
|
||||||
|
{%- set _ = translator(lang) -%}
|
||||||
|
{%- set mxid -%}
|
||||||
|
@{{ user.username }}:{{ branding.server_name }}
|
||||||
|
{%- endset -%}
|
||||||
|
|
||||||
|
{{ _("mas.emails.recovery.subject", mxid=mxid) }}
|
24
templates/emails/recovery.txt
Normal file
24
templates/emails/recovery.txt
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{#
|
||||||
|
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-#}
|
||||||
|
|
||||||
|
{%- set _ = translator(lang) -%}
|
||||||
|
{{ _("mas.emails.recovery.headline", server_name=branding.server_name) }}
|
||||||
|
|
||||||
|
{{ _("mas.emails.recovery.copy_link") }}
|
||||||
|
|
||||||
|
{{ recovery_link }}
|
||||||
|
|
||||||
|
{{ _("mas.emails.recovery.you_can_ignore") }}
|
@@ -221,6 +221,32 @@
|
|||||||
"context": "emails/verification.html:19:3-51, emails/verification.txt:19:3-51",
|
"context": "emails/verification.html:19:3-51, emails/verification.txt:19:3-51",
|
||||||
"description": "Greeting at the top of emails sent to the user"
|
"description": "Greeting at the top of emails sent to the user"
|
||||||
},
|
},
|
||||||
|
"recovery": {
|
||||||
|
"click_button": "Click on the button below to create a new password:",
|
||||||
|
"@click_button": {
|
||||||
|
"context": "emails/recovery.html:36:7-44"
|
||||||
|
},
|
||||||
|
"copy_link": "Copy the following link and paste it into a browser to create a new password:",
|
||||||
|
"@copy_link": {
|
||||||
|
"context": "emails/recovery.txt:20:3-37"
|
||||||
|
},
|
||||||
|
"create_new_password": "Create new password",
|
||||||
|
"@create_new_password": {
|
||||||
|
"context": "emails/recovery.html:51:9-53"
|
||||||
|
},
|
||||||
|
"headline": "You requested a password reset for your %(server_name)s account.",
|
||||||
|
"@headline": {
|
||||||
|
"context": "emails/recovery.html:34:7-74, emails/recovery.txt:18:3-70"
|
||||||
|
},
|
||||||
|
"subject": "Reset your account password (%(mxid)s)",
|
||||||
|
"@subject": {
|
||||||
|
"context": "emails/recovery.subject:22:3-46"
|
||||||
|
},
|
||||||
|
"you_can_ignore": "If you didn't ask for a new password, you can ignore this email. Your current password will continue to work.",
|
||||||
|
"@you_can_ignore": {
|
||||||
|
"context": "emails/recovery.html:53:7-46, emails/recovery.txt:24:3-42"
|
||||||
|
}
|
||||||
|
},
|
||||||
"verify": {
|
"verify": {
|
||||||
"body_html": "Your verification code to confirm this email address is: <strong>%(code)s</strong>",
|
"body_html": "Your verification code to confirm this email address is: <strong>%(code)s</strong>",
|
||||||
"@body_html": {
|
"@body_html": {
|
||||||
|
Reference in New Issue
Block a user