mobilizon/lib/web/email/user.ex
Thomas Citharel b2492a3870
fix(backend): handle email not being sent when resending registration instructions
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2023-08-18 13:41:46 +02:00

223 lines
6.2 KiB
Elixir

defmodule Mobilizon.Web.Email.User do
@moduledoc """
Handles emails sent to users.
"""
use Phoenix.Swoosh, view: Mobilizon.Web.EmailView
import Mobilizon.Web.Gettext, only: [gettext: 2]
alias Mobilizon.{Config, Crypto, Users}
alias Mobilizon.Storage.Repo
alias Mobilizon.Users.User
alias Mobilizon.Web.Email
require Logger
@spec confirmation_email(User.t(), String.t()) :: Swoosh.Email.t()
def confirmation_email(
%User{email: email, confirmation_token: confirmation_token},
locale \\ "en"
) do
Gettext.put_locale(locale)
subject =
gettext(
"Instructions to confirm your Mobilizon account on %{instance}",
instance: Config.instance_name()
)
[to: email, subject: subject]
|> Email.base_email()
|> render_body(:registration_confirmation, %{
locale: locale,
token: confirmation_token,
subject: subject,
offer_unsupscription: false
})
end
@spec reset_password_email(User.t(), String.t()) :: Swoosh.Email.t()
def reset_password_email(
%User{email: email, reset_password_token: reset_password_token},
locale \\ "en"
) do
Gettext.put_locale(locale)
subject =
gettext(
"Instructions to reset your password on %{instance}",
instance: Config.instance_name()
)
[to: email, subject: subject]
|> Email.base_email()
|> render_body(:password_reset, %{
locale: locale,
token: reset_password_token,
subject: subject,
offer_unsupscription: false
})
end
@spec check_confirmation_token(String.t()) ::
{:ok, User.t()} | {:error, :invalid_token | Ecto.Changeset.t()}
def check_confirmation_token(token) when is_binary(token) do
case Users.get_user_by_activation_token(token) do
%User{} = user ->
case Users.update_user(user, %{
confirmed_at: DateTime.utc_now() |> DateTime.truncate(:second),
confirmation_sent_at: nil,
confirmation_token: nil,
email: user.unconfirmed_email || user.email
}) do
{:ok, %User{} = user} ->
Logger.info("User #{user.email} has been confirmed")
{:ok, user}
{:error, %Ecto.Changeset{} = err} ->
{:error, err}
end
nil ->
{:error, :invalid_token}
end
end
def resend_confirmation_email(%User{} = user, locale \\ "en") do
with :ok <- we_can_send_email(user, :confirmation_sent_at),
{:ok, user} <-
Users.update_user(user, %{
"confirmation_sent_at" => DateTime.utc_now() |> DateTime.truncate(:second)
}) do
case send_confirmation_email(user, locale) do
{:ok, _} ->
Logger.info("Sent confirmation email again to #{user.email}")
{:ok, user.email}
{:error, err} ->
Logger.error("Failed sending email to #{user.email}. #{inspect(err)}")
{:error, :failed_sending_mail}
end
end
end
@spec send_confirmation_email(User.t(), String.t()) :: {:ok, term} | {:error, term}
def send_confirmation_email(%User{} = user, locale \\ "en") do
user
|> Email.User.confirmation_email(locale)
|> Email.Mailer.send_email()
end
@doc """
Check that the provided token is correct and update provided password
"""
@spec check_reset_password_token(String.t(), String.t()) ::
{:ok, User.t()} | {:error, :user_not_found | Ecto.Changeset.t()}
def check_reset_password_token(password, token) do
case Users.get_user_by_reset_password_token(token) do
%User{} = user ->
user
|> User.password_reset_changeset(%{
"password" => password,
"reset_password_sent_at" => nil,
"reset_password_token" => nil
})
|> Repo.update()
nil ->
{:error, :user_not_found}
end
end
@doc """
Send the email reset password, if it's not too soon since the last send
"""
@spec send_password_reset_email(User.t(), String.t()) :: tuple
def send_password_reset_email(%User{} = user, locale \\ "en") do
with :ok <- we_can_send_email(user, :reset_password_sent_at),
{:ok, %User{} = user_updated} <-
Repo.update(
User.send_password_reset_changeset(user, %{
"reset_password_token" => Crypto.random_string(30),
"reset_password_sent_at" => DateTime.utc_now() |> DateTime.truncate(:second)
})
) do
user_updated
|> Email.User.reset_password_email(locale)
|> Email.Mailer.send_email()
{:ok, user.email}
else
{:error, reason} -> {:error, reason}
end
end
def send_email_reset_old_email(%User{
locale: user_locale,
email: email,
unconfirmed_email: unconfirmed_email
}) do
Gettext.put_locale(user_locale)
subject =
gettext(
"Mobilizon on %{instance}: email changed",
instance: Config.instance_name()
)
[to: email, subject: subject]
|> Email.base_email()
|> render_body(:email_changed_old, %{
locale: user_locale,
new_email: unconfirmed_email,
subject: subject,
offer_unsupscription: false
})
end
def send_email_reset_new_email(%User{
locale: user_locale,
unconfirmed_email: unconfirmed_email,
confirmation_token: confirmation_token
}) do
Gettext.put_locale(user_locale)
subject =
gettext(
"Mobilizon on %{instance}: confirm your email address",
instance: Config.instance_name()
)
[to: unconfirmed_email, subject: subject]
|> Email.base_email()
|> render_body(:email_changed_new, %{
locale: user_locale,
token: confirmation_token,
subject: subject,
offer_unsupscription: false
})
end
@spec we_can_send_email(User.t(), atom) :: :ok | {:error, :email_too_soon}
defp we_can_send_email(%User{} = user, key) do
case Map.get(user, key) do
nil ->
:ok
_ ->
case DateTime.compare(
DateTime.add(Map.get(user, key), 100),
DateTime.utc_now() |> DateTime.truncate(:second)
) do
:lt ->
:ok
_ ->
{:error, :email_too_soon}
end
end
end
end