diff --git a/config/config.md b/config/config.md
index 6b45c01da..165f5d9f8 100644
--- a/config/config.md
+++ b/config/config.md
@@ -30,6 +30,31 @@ This filter replaces the filename (not the path) of an upload. For complete obfu
* `text`: Text to replace filenames in links. If empty, `{random}.extension` will be used.
+## Pleroma.Mailer
+* `adapter`: one of the mail adapters listed in [Swoosh readme](https://github.com/swoosh/swoosh#adapters), or `Swoosh.Adapters.Local` for in-memory mailbox.
+* `api_key` / `password` and / or other adapter-specific settings, per the above documentation.
+
+An example for Sendgrid adapter:
+
+```
+config :pleroma, Pleroma.Mailer,
+ adapter: Swoosh.Adapters.Sendgrid,
+ api_key: "YOUR_API_KEY"
+```
+
+An example for SMTP adapter:
+```
+config :pleroma, Pleroma.Mailer,
+ adapter: Swoosh.Adapters.SMTP,
+ relay: "smtp.gmail.com",
+ username: "YOUR_USERNAME@gmail.com",
+ password: "YOUR_SMTP_PASSWORD",
+ port: 465,
+ ssl: true,
+ tls: :always,
+ auth: :always
+```
+
## :uri_schemes
* `valid_schemes`: List of the scheme part that is considered valid to be an URL
diff --git a/config/dev.exs b/config/dev.exs
index 166be721a..080a2f8db 100644
--- a/config/dev.exs
+++ b/config/dev.exs
@@ -17,6 +17,8 @@
check_origin: false,
watchers: []
+config :pleroma, Pleroma.Mailer, adapter: Swoosh.Adapters.Local
+
# ## SSL Support
#
# In order to use HTTPS in development, a self-signed
diff --git a/config/test.exs b/config/test.exs
index ca10a616c..5c6acfead 100644
--- a/config/test.exs
+++ b/config/test.exs
@@ -11,6 +11,8 @@
config :pleroma, Pleroma.Uploaders.Local, uploads: "test/uploads"
+config :pleroma, Pleroma.Mailer, adapter: Swoosh.Adapters.Test
+
# Configure your database
config :pleroma, Pleroma.Repo,
adapter: Ecto.Adapters.Postgres,
diff --git a/lib/pleroma/emails/mailer.ex b/lib/pleroma/emails/mailer.ex
new file mode 100644
index 000000000..14ed32ea8
--- /dev/null
+++ b/lib/pleroma/emails/mailer.ex
@@ -0,0 +1,3 @@
+defmodule Pleroma.Mailer do
+ use Swoosh.Mailer, otp_app: :pleroma
+end
diff --git a/lib/pleroma/emails/user_email.ex b/lib/pleroma/emails/user_email.ex
new file mode 100644
index 000000000..9cdf002f3
--- /dev/null
+++ b/lib/pleroma/emails/user_email.ex
@@ -0,0 +1,40 @@
+defmodule Pleroma.UserEmail do
+ @moduledoc "User emails"
+
+ import Swoosh.Email
+
+ alias Pleroma.Web.{Endpoint, Router}
+
+ defp instance_config, do: Pleroma.Config.get(:instance)
+
+ defp instance_name, do: instance_config()[:name]
+
+ defp sender do
+ {instance_name(), instance_config()[:email]}
+ end
+
+ defp recipient(email, nil), do: email
+ defp recipient(email, name), do: {name, email}
+
+ def password_reset_email(user, password_reset_token) when is_binary(password_reset_token) do
+ password_reset_url =
+ Router.Helpers.util_url(
+ Endpoint,
+ :show_password_reset,
+ password_reset_token
+ )
+
+ html_body = """
+
Reset your password at #{instance_name()}
+ Someone has requested password change for your account at #{instance_name()}.
+ If it was you, visit the following link to proceed: reset password.
+ If it was someone else, nothing to worry about: your data is secure and your password has not been changed.
+ """
+
+ new()
+ |> to(recipient(user.email, user.name))
+ |> from(sender())
+ |> subject("Password reset")
+ |> html_body(html_body)
+ end
+end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 9c06fac4f..6253a28db 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -85,6 +85,15 @@ defmodule Pleroma.Web.Router do
plug(:accepts, ["html", "json"])
end
+ pipeline :mailbox_preview do
+ plug(:accepts, ["html"])
+
+ plug(:put_secure_browser_headers, %{
+ "content-security-policy" =>
+ "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' 'unsafe-eval'"
+ })
+ end
+
scope "/api/pleroma", Pleroma.Web.TwitterAPI do
pipe_through(:pleroma_api)
get("/password_reset/:token", UtilController, :show_password_reset)
@@ -268,6 +277,7 @@ defmodule Pleroma.Web.Router do
get("/statusnet/conversation/:id", TwitterAPI.Controller, :fetch_conversation)
post("/account/register", TwitterAPI.Controller, :register)
+ post("/account/password_reset", TwitterAPI.Controller, :password_reset)
get("/search", TwitterAPI.Controller, :search)
get("/statusnet/tags/timeline/:tag", TwitterAPI.Controller, :public_and_external_timeline)
@@ -424,6 +434,14 @@ defmodule Pleroma.Web.Router do
get("/:sig/:url/:filename", MediaProxyController, :remote)
end
+ if Mix.env() == :dev do
+ scope "/dev" do
+ pipe_through([:mailbox_preview])
+
+ forward("/mailbox", Plug.Swoosh.MailboxPreview, base_path: "/dev/mailbox")
+ end
+ end
+
scope "/", Fallback do
get("/registration/:token", RedirectController, :registration_page)
get("/*path", RedirectController, :redirector)
diff --git a/lib/pleroma/web/templates/twitter_api/util/password_reset_failed.html.eex b/lib/pleroma/web/templates/twitter_api/util/password_reset_failed.html.eex
index 58a3736fd..df037c01e 100644
--- a/lib/pleroma/web/templates/twitter_api/util/password_reset_failed.html.eex
+++ b/lib/pleroma/web/templates/twitter_api/util/password_reset_failed.html.eex
@@ -1 +1,2 @@
Password reset failed
+
diff --git a/lib/pleroma/web/templates/twitter_api/util/password_reset_success.html.eex b/lib/pleroma/web/templates/twitter_api/util/password_reset_success.html.eex
index c7dfcb6dd..f30ba3274 100644
--- a/lib/pleroma/web/templates/twitter_api/util/password_reset_success.html.eex
+++ b/lib/pleroma/web/templates/twitter_api/util/password_reset_success.html.eex
@@ -1 +1,2 @@
Password changed!
+
diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex
index 79ea48d86..1e764f24a 100644
--- a/lib/pleroma/web/twitter_api/twitter_api.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api.ex
@@ -167,6 +167,25 @@ def register_user(params) do
end
end
+ def password_reset(nickname_or_email) do
+ with true <- is_binary(nickname_or_email),
+ %User{local: true} = user <- User.get_by_nickname_or_email(nickname_or_email),
+ {:ok, token_record} <- Pleroma.PasswordResetToken.create_token(user) do
+ user
+ |> Pleroma.UserEmail.password_reset_email(token_record.token)
+ |> Pleroma.Mailer.deliver()
+ else
+ false ->
+ {:error, "bad user identifier"}
+
+ %User{local: false} ->
+ {:error, "remote user"}
+
+ nil ->
+ {:error, "unknown user"}
+ end
+ end
+
def get_by_id_or_nickname(id_or_nickname) do
if !is_integer(id_or_nickname) && :error == Integer.parse(id_or_nickname) do
Repo.get_by(User, nickname: id_or_nickname)
diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
index 786849aa3..38eff8191 100644
--- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
@@ -1,5 +1,8 @@
defmodule Pleroma.Web.TwitterAPI.Controller do
use Pleroma.Web, :controller
+
+ import Pleroma.Web.ControllerHelper, only: [json_response: 3]
+
alias Pleroma.Web.TwitterAPI.{TwitterAPI, UserView, ActivityView, NotificationView}
alias Pleroma.Web.CommonAPI
alias Pleroma.{Repo, Activity, Object, User, Notification}
@@ -322,6 +325,14 @@ def register(conn, params) do
end
end
+ def password_reset(conn, params) do
+ nickname_or_email = params["email"] || params["nickname"]
+
+ with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
+ json_response(conn, :no_content, "")
+ end
+ end
+
def update_avatar(%{assigns: %{user: user}} = conn, params) do
{:ok, object} = ActivityPub.upload(params, type: :avatar)
change = Changeset.change(user, %{avatar: object.data})
diff --git a/mix.exs b/mix.exs
index b5bcd027f..0fb40e07b 100644
--- a/mix.exs
+++ b/mix.exs
@@ -75,7 +75,9 @@ defp deps do
git: "https://github.com/msantos/crypt", ref: "1f2b58927ab57e72910191a7ebaeff984382a1d3"},
{:cors_plug, "~> 1.5"},
{:ex_doc, "> 0.18.3 and < 0.20.0", only: :dev, runtime: false},
- {:web_push_encryption, "~> 0.2.1"}
+ {:web_push_encryption, "~> 0.2.1"},
+ {:swoosh, "~> 0.20"},
+ {:gen_smtp, "~> 0.13"}
]
end
diff --git a/mix.lock b/mix.lock
index ff8e9fdca..d9ae9a83b 100644
--- a/mix.lock
+++ b/mix.lock
@@ -20,6 +20,7 @@
"ex_aws_s3": {:hex, :ex_aws_s3, "2.0.1", "9e09366e77f25d3d88c5393824e613344631be8db0d1839faca49686e99b6704", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm"},
"ex_doc": {:hex, :ex_doc, "0.19.1", "519bb9c19526ca51d326c060cb1778d4a9056b190086a8c6c115828eaccea6cf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
"ex_machina": {:hex, :ex_machina, "2.2.0", "fec496331e04fc2db2a1a24fe317c12c0c4a50d2beb8ebb3531ed1f0d84be0ed", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"},
+ "gen_smtp": {:hex, :gen_smtp, "0.13.0", "11f08504c4bdd831dc520b8f84a1dce5ce624474a797394e7aafd3c29f5dcd25", [:rebar3], [], "hexpm"},
"gettext": {:hex, :gettext, "0.15.0", "40a2b8ce33a80ced7727e36768499fc9286881c43ebafccae6bab731e2b2b8ce", [:mix], [], "hexpm"},
"hackney": {:hex, :hackney, "1.13.0", "24edc8cd2b28e1c652593833862435c80661834f6c9344e84b6a2255e7aeef03", [:rebar3], [{:certifi, "2.3.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.2", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
"html_sanitize_ex": {:hex, :html_sanitize_ex, "1.3.0", "f005ad692b717691203f940c686208aa3d8ffd9dd4bb3699240096a51fa9564e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"},
@@ -49,6 +50,7 @@
"postgrex": {:hex, :postgrex, "0.13.5", "3d931aba29363e1443da167a4b12f06dcd171103c424de15e5f3fc2ba3e6d9c5", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"},
"ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"},
+ "swoosh": {:hex, :swoosh, "0.20.0", "9a6c13822c9815993c03b6f8fccc370fcffb3c158d9754f67b1fdee6b3a5d928", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.12", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm"},
"tesla": {:hex, :tesla, "1.2.1", "864783cc27f71dd8c8969163704752476cec0f3a51eb3b06393b3971dc9733ff", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"},
"trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"tzdata": {:hex, :tzdata, "0.5.17", "50793e3d85af49736701da1a040c415c97dc1caf6464112fd9bd18f425d3053b", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
diff --git a/test/web/twitter_api/twitter_api_controller_test.exs b/test/web/twitter_api/twitter_api_controller_test.exs
index a30d415a7..c16c0cdc0 100644
--- a/test/web/twitter_api/twitter_api_controller_test.exs
+++ b/test/web/twitter_api/twitter_api_controller_test.exs
@@ -9,6 +9,7 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.TwitterAPI.TwitterAPI
alias Comeonin.Pbkdf2
+ alias Ecto.Changeset
import Pleroma.Factory
@@ -270,7 +271,7 @@ test "with credentials", %{conn: conn, user: current_user} do
since_id = List.last(activities).id
current_user =
- Ecto.Changeset.change(current_user, following: [User.ap_followers(user)])
+ Changeset.change(current_user, following: [User.ap_followers(user)])
|> Repo.update!()
conn =
@@ -832,6 +833,46 @@ test "it returns errors on a problem", %{conn: conn} do
end
end
+ describe "POST /api/account/password_reset, with valid parameters" do
+ setup %{conn: conn} do
+ user = insert(:user)
+ conn = post(conn, "/api/account/password_reset?email=#{user.email}")
+ %{conn: conn, user: user}
+ end
+
+ test "it returns 204", %{conn: conn} do
+ assert json_response(conn, :no_content)
+ end
+
+ test "it creates a PasswordResetToken record for user", %{user: user} do
+ token_record = Repo.get_by(Pleroma.PasswordResetToken, user_id: user.id)
+ assert token_record
+ end
+
+ test "it sends an email to user", %{user: user} do
+ token_record = Repo.get_by(Pleroma.PasswordResetToken, user_id: user.id)
+
+ Swoosh.TestAssertions.assert_email_sent(
+ Pleroma.UserEmail.password_reset_email(user, token_record.token)
+ )
+ end
+ end
+
+ describe "POST /api/account/password_reset, with invalid parameters" do
+ setup [:valid_user]
+
+ test "it returns 500 when user is not found", %{conn: conn, user: user} do
+ conn = post(conn, "/api/account/password_reset?email=nonexisting_#{user.email}")
+ assert json_response(conn, :internal_server_error)
+ end
+
+ test "it returns 500 when user is not local", %{conn: conn, user: user} do
+ {:ok, user} = Repo.update(Changeset.change(user, local: false))
+ conn = post(conn, "/api/account/password_reset?email=#{user.email}")
+ assert json_response(conn, :internal_server_error)
+ end
+ end
+
describe "GET /api/externalprofile/show" do
test "it returns the user", %{conn: conn} do
user = insert(:user)