summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlexander Strizhakov <alex.strizhakov@gmail.com>2020-11-29 14:45:16 +0300
committerAlexander Strizhakov <alex.strizhakov@gmail.com>2021-04-22 08:02:11 +0300
commit69f8f9446e862286d631b823e836d5fa049573ed (patch)
treed58ccf2bd2a2acb5d4aaba2cfea01be4b72ebb09
parentb050adb5e2d1add4cb2f5d24010ace33af16a381 (diff)
batch mention email notifications in timeframefeature/2295-email-mention-notification
-rw-r--r--CHANGELOG.md1
-rw-r--r--config/config.exs8
-rw-r--r--docs/administration/CLI_tasks/user.md33
-rw-r--r--docs/configuration/cheatsheet.md9
-rw-r--r--docs/development/API/differences_in_mastoapi_responses.md1
-rw-r--r--lib/mix/tasks/pleroma/app.ex2
-rw-r--r--lib/mix/tasks/pleroma/user.ex29
-rw-r--r--lib/pleroma/emails/user_email.ex89
-rw-r--r--lib/pleroma/migration_helper/notification_backfill.ex11
-rw-r--r--lib/pleroma/notification.ex47
-rw-r--r--lib/pleroma/user.ex22
-rw-r--r--lib/pleroma/web/api_spec/operations/account_operation.ex28
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/account_controller.ex1
-rw-r--r--lib/pleroma/web/mastodon_api/views/account_view.ex3
-rw-r--r--lib/pleroma/web/templates/email/digest.html.eex2
-rw-r--r--lib/pleroma/web/templates/email/mentions.html.eex439
-rw-r--r--lib/pleroma/workers/cron/email_mentions_worker.ex67
-rw-r--r--priv/repo/migrations/20201128144004_change_user_email_notifications_setting.exs32
-rw-r--r--priv/repo/migrations/20201214160053_add_notified_at_to_notifications.exs15
-rw-r--r--priv/repo/migrations/20201221060622_fill_notifications_notified_at.exs23
-rw-r--r--priv/repo/migrations/20201222052839_add_index_to_notifications.exs7
-rw-r--r--test/mix/tasks/pleroma/user_test.exs89
-rw-r--r--test/pleroma/notification_test.exs90
-rw-r--r--test/pleroma/user_test.exs25
-rw-r--r--test/pleroma/web/mastodon_api/update_credentials_test.exs32
-rw-r--r--test/pleroma/web/mastodon_api/views/account_view_test.exs12
-rw-r--r--test/pleroma/workers/cron/email_mentions_worker_test.exs107
-rw-r--r--test/support/factory.ex3
28 files changed, 1167 insertions, 60 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bfa76a89a..135e0ff5e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -85,6 +85,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Ability to define custom HTTP headers per each frontend
- MRF (`NoEmptyPolicy`): New MRF Policy which will deny empty statuses or statuses of only mentions from being created by local users
- New users will receive a simple email confirming their registration if no other emails will be dispatched. (e.g., Welcome, Confirmation, or Approval Required)
+- Email with missed mentions in a specific period.
<details>
<summary>API Changes</summary>
diff --git a/config/config.exs b/config/config.exs
index 4381068ac..10b8498ef 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -563,10 +563,12 @@ config :pleroma, Oban,
remote_fetcher: 2,
attachments_cleanup: 1,
new_users_digest: 1,
- mute_expire: 5
+ mute_expire: 5,
+ email_mentions: 1
],
plugins: [Oban.Plugins.Pruner],
crontab: [
+ {"*/15 * * * *", Pleroma.Workers.Cron.EmailMentionsWorker},
{"0 0 * * 0", Pleroma.Workers.Cron.DigestEmailsWorker},
{"0 0 * * *", Pleroma.Workers.Cron.NewUsersDigestWorker}
]
@@ -851,6 +853,10 @@ config :pleroma, ConcurrentLimiter, [
{Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy, [max_running: 5, max_waiting: 5]}
]
+config :pleroma, Pleroma.Workers.Cron.EmailMentionsWorker,
+ enabled: false,
+ timeframe: 30
+
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs"
diff --git a/docs/administration/CLI_tasks/user.md b/docs/administration/CLI_tasks/user.md
index 24fdaeab4..61774f469 100644
--- a/docs/administration/CLI_tasks/user.md
+++ b/docs/administration/CLI_tasks/user.md
@@ -300,3 +300,36 @@
```sh
mix pleroma.user unconfirm_all
```
+
+## Update email notifications settings for user
+
+=== "OTP"
+
+ ```sh
+ ./bin/pleroma_ctl user email_notifications <nickname> [option ...]
+ ```
+
+=== "From Source"
+
+ ```sh
+ mix pleroma.user email_notifications <nickname> [option ...]
+ ```
+
+### Options
+
+- `--digest`/`--no-digest` - whether the user should receive digest emails
+- `--notifications` - what types of email notifications user should receive (can be aliased with `-n`). To disable all types pass `off` value.
+
+Example:
+
+=== "OTP"
+
+ ```sh
+ ./bin/pleroma_ctl user email_notifications lain --digest -n mention
+ ```
+
+=== "From Source"
+
+ ```sh
+ mix pleroma.user email_notifications lain --digest -n mention
+ ```
diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md
index 069421722..262978b7d 100644
--- a/docs/configuration/cheatsheet.md
+++ b/docs/configuration/cheatsheet.md
@@ -715,6 +715,7 @@ Pleroma has these periodic job workers:
* `Pleroma.Workers.Cron.DigestEmailsWorker` - digest emails for users with new mentions and follows
* `Pleroma.Workers.Cron.NewUsersDigestWorker` - digest emails for admins with new registrations
+* `Pleroma.Workers.Cron.EmailMentionsWorker` - email with missed mentions notifications in special timeframe
```elixir
config :pleroma, Oban,
@@ -726,6 +727,7 @@ config :pleroma, Oban,
federator_outgoing: 50
],
crontab: [
+ {"*/15 * * * *", Pleroma.Workers.Cron.EmailMentionsWorker},
{"0 0 * * 0", Pleroma.Workers.Cron.DigestEmailsWorker},
{"0 0 * * *", Pleroma.Workers.Cron.NewUsersDigestWorker}
]
@@ -1154,3 +1156,10 @@ Each job has these settings:
* `:max_running` - max concurrently runnings jobs
* `:max_waiting` - max waiting jobs
+
+## Mention emails (Pleroma.Workers.Cron.EmailMentionsWorker)
+
+The worker sends email notifications not read in a certain timeframe.
+
+* `:enabled` - enables email notifications for missed mentions & chat mentions
+* `:timeframe` - the period after which the sending of emails begins for missed mentions (in minutes)
diff --git a/docs/development/API/differences_in_mastoapi_responses.md b/docs/development/API/differences_in_mastoapi_responses.md
index 2ff56d3ca..72949c9d7 100644
--- a/docs/development/API/differences_in_mastoapi_responses.md
+++ b/docs/development/API/differences_in_mastoapi_responses.md
@@ -107,6 +107,7 @@ Has these additional fields under the `pleroma` object:
- `notification_settings`: object, can be absent. See `/api/v1/pleroma/notification_settings` for the parameters/keys returned.
- `accepts_chat_messages`: boolean, but can be null if we don't have that information about a user
- `favicon`: nullable URL string, Favicon image of the user's instance
+- `email_notifications`: map with settings for `digest` emails (boolean) and `notifications` setting (list with notification types).
### Source
diff --git a/lib/mix/tasks/pleroma/app.ex b/lib/mix/tasks/pleroma/app.ex
index 0bf7ffabc..aa5c9cd54 100644
--- a/lib/mix/tasks/pleroma/app.ex
+++ b/lib/mix/tasks/pleroma/app.ex
@@ -21,7 +21,7 @@ defmodule Mix.Tasks.Pleroma.App do
scopes =
if opts[:scopes] do
- String.split(opts[:scopes], ",")
+ String.split(opts[:scopes], ",", trim: true)
else
["read", "write", "follow", "push"]
end
diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex
index 53d5fc6d9..d2a545e91 100644
--- a/lib/mix/tasks/pleroma/user.ex
+++ b/lib/mix/tasks/pleroma/user.ex
@@ -433,6 +433,35 @@ defmodule Mix.Tasks.Pleroma.User do
|> Stream.run()
end
+ def run(["email_notifications", nickname | options]) do
+ start_pleroma()
+
+ {opts, _} =
+ OptionParser.parse!(options,
+ strict: [digest: :boolean, notifications: :string],
+ aliases: [n: :notifications]
+ )
+
+ params =
+ Map.new(opts, fn
+ {:digest, v} ->
+ {"digest", v}
+
+ {:notifications, v} ->
+ types = if v == "off", do: [], else: String.split(v, ",", trim: true)
+ {"notifications", types}
+ end)
+
+ with keys when keys != [] <- Map.keys(params),
+ %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
+ {:ok, user} = User.update_email_notifications(user, params)
+ shell_info("Email notifications for user #{user.nickname} were successfully updated.")
+ else
+ [] -> shell_error("No changes passed")
+ _ -> shell_error("No local user #{nickname}")
+ end
+ end
+
defp set_moderator(user, value) do
{:ok, user} =
user
diff --git a/lib/pleroma/emails/user_email.ex b/lib/pleroma/emails/user_email.ex
index 52f3d419d..73cea4962 100644
--- a/lib/pleroma/emails/user_email.ex
+++ b/lib/pleroma/emails/user_email.ex
@@ -8,6 +8,7 @@ defmodule Pleroma.Emails.UserEmail do
use Phoenix.Swoosh, view: Pleroma.Web.EmailView, layout: {Pleroma.Web.LayoutView, :email}
alias Pleroma.Config
+ alias Pleroma.Notification
alias Pleroma.User
alias Pleroma.Web.Endpoint
alias Pleroma.Web.Router
@@ -120,6 +121,27 @@ defmodule Pleroma.Emails.UserEmail do
|> html_body(html_body)
end
+ defp prepare_mention(%Notification{type: type} = notification, acc)
+ when type in ["mention", "pleroma:chat_mention"] do
+ object = Pleroma.Object.normalize(notification.activity, fetch: false)
+
+ if object do
+ object = update_in(object.data["content"], &format_links/1)
+
+ mention = %{
+ data: notification,
+ object: object,
+ from: User.get_by_ap_id(notification.activity.actor)
+ }
+
+ [mention | acc]
+ else
+ acc
+ end
+ end
+
+ defp prepare_mention(_, acc), do: acc
+
@doc """
Email used in digest email notifications
Includes Mentions and New Followers data
@@ -127,25 +149,12 @@ defmodule Pleroma.Emails.UserEmail do
"""
@spec digest_email(User.t()) :: Swoosh.Email.t() | nil
def digest_email(user) do
- notifications = Pleroma.Notification.for_user_since(user, user.last_digest_emailed_at)
+ notifications = Notification.for_user_since(user, user.last_digest_emailed_at)
mentions =
notifications
|> Enum.filter(&(&1.activity.data["type"] == "Create"))
- |> Enum.map(fn notification ->
- object = Pleroma.Object.normalize(notification.activity, fetch: false)
-
- if not is_nil(object) do
- object = update_in(object.data["content"], &format_links/1)
-
- %{
- data: notification,
- object: object,
- from: User.get_by_ap_id(notification.activity.actor)
- }
- end
- end)
- |> Enum.filter(& &1)
+ |> Enum.reduce([], &prepare_mention/2)
followers =
notifications
@@ -165,7 +174,6 @@ defmodule Pleroma.Emails.UserEmail do
unless Enum.empty?(mentions) do
styling = Config.get([__MODULE__, :styling])
- logo = Config.get([__MODULE__, :logo])
html_data = %{
instance: instance_name(),
@@ -176,20 +184,15 @@ defmodule Pleroma.Emails.UserEmail do
styling: styling
}
- logo_path =
- if is_nil(logo) do
- Path.join(:code.priv_dir(:pleroma), "static/static/logo.svg")
- else
- Path.join(Config.get([:instance, :static_dir]), logo)
- end
+ {logo_path, logo} = logo_path()
new()
|> to(recipient(user))
|> from(sender())
|> subject("Your digest from #{instance_name()}")
|> put_layout(false)
- |> render_body("digest.html", html_data)
- |> attachment(Swoosh.Attachment.new(logo_path, filename: "logo.svg", type: :inline))
+ |> render_body("digest.html", Map.put(html_data, :logo, logo))
+ |> attachment(Swoosh.Attachment.new(logo_path, filename: logo, type: :inline))
end
end
@@ -242,4 +245,42 @@ defmodule Pleroma.Emails.UserEmail do
|> subject("Your account archive is ready")
|> html_body(html_body)
end
+
+ @spec mentions_notification_email(User.t(), [Notification.t()]) :: Swoosh.Email.t()
+ def mentions_notification_email(user, mentions) do
+ html_data = %{
+ instance: instance_name(),
+ user: user,
+ mentions: Enum.reduce(mentions, [], &prepare_mention/2),
+ unsubscribe_link: unsubscribe_url(user, "mentions_email"),
+ styling: Config.get([__MODULE__, :styling])
+ }
+
+ now = NaiveDateTime.utc_now()
+
+ {logo_path, logo} = logo_path()
+
+ new()
+ |> to(recipient(user))
+ |> from(sender())
+ |> subject(
+ "[Pleroma] New mentions from #{instance_name()} for #{
+ Timex.format!(now, "{Mfull} {D}, {YYYY} at {h12}:{m} {AM}")
+ }"
+ )
+ |> put_layout(false)
+ |> render_body("mentions.html", Map.put(html_data, :logo, logo))
+ |> attachment(Swoosh.Attachment.new(logo_path, filename: logo, type: :inline))
+ end
+
+ defp logo_path do
+ logo_path =
+ if logo = Config.get([__MODULE__, :logo]) do
+ Path.join(Config.get([:instance, :static_dir]), logo)
+ else
+ Path.join(:code.priv_dir(:pleroma), "static/static/logo.svg")
+ end
+
+ {logo_path, Path.basename(logo_path)}
+ end
end
diff --git a/lib/pleroma/migration_helper/notification_backfill.ex b/lib/pleroma/migration_helper/notification_backfill.ex
index 62b710f82..e634ba326 100644
--- a/lib/pleroma/migration_helper/notification_backfill.ex
+++ b/lib/pleroma/migration_helper/notification_backfill.ex
@@ -11,9 +11,11 @@ defmodule Pleroma.MigrationHelper.NotificationBackfill do
def fill_in_notification_types do
query =
- from(n in Pleroma.Notification,
+ from(n in "notifications",
where: is_nil(n.type),
- preload: :activity
+ join: a in "activities",
+ on: n.activity_id == a.id,
+ select: %{id: n.id, activity: %{id: a.id, data: a.data}}
)
query
@@ -22,9 +24,8 @@ defmodule Pleroma.MigrationHelper.NotificationBackfill do
if notification.activity do
type = type_from_activity(notification.activity)
- notification
- |> Ecto.Changeset.change(%{type: type})
- |> Repo.update()
+ from(n in "notifications", where: n.id == ^notification.id)
+ |> Repo.update_all(set: [type: type])
end
end)
end
diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex
index 7efbdc49a..8adbc07f4 100644
--- a/lib/pleroma/notification.ex
+++ b/lib/pleroma/notification.ex
@@ -37,6 +37,7 @@ defmodule Pleroma.Notification do
field(:type, :string)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)
+ field(:notified_at, :naive_datetime)
timestamps()
end
@@ -249,7 +250,7 @@ defmodule Pleroma.Notification do
iex> Pleroma.Notification.for_user_since(%Pleroma.User{}, ~N[2019-04-15 11:22:33])
[]
"""
- @spec for_user_since(Pleroma.User.t(), NaiveDateTime.t()) :: [t()]
+ @spec for_user_since(User.t(), NaiveDateTime.t()) :: [t()]
def for_user_since(user, date) do
from(n in for_user_query(user),
where: n.updated_at > ^date
@@ -664,4 +665,48 @@ defmodule Pleroma.Notification do
)
|> Repo.update_all(set: [seen: true])
end
+
+ defp unread_mentions_in_timeframe_query(query \\ __MODULE__, args) do
+ types = args[:types] || ["mention", "pleroma:chat_mention"]
+ max_at = args[:max_at]
+
+ from(n in query,
+ where: n.seen == false,
+ where: is_nil(n.notified_at),
+ where: n.type in ^types,
+ where: n.inserted_at <= ^max_at
+ )
+ end
+
+ @spec users_ids_with_unread_mentions(NaiveDateTime.t()) :: [String.t()]
+ def users_ids_with_unread_mentions(max_at) do
+ from(n in unread_mentions_in_timeframe_query(%{max_at: max_at}),
+ join: u in assoc(n, :user),
+ where: not is_nil(u.email),
+ distinct: n.user_id,
+ select: n.user_id
+ )
+ |> Repo.all()
+ end
+
+ @spec for_user_unread_mentions(User.t(), NaiveDateTime.t()) :: [t()]
+ def for_user_unread_mentions(%User{} = user, max_at) do
+ args = %{
+ max_at: max_at,
+ types: user.email_notifications["notifications"]
+ }
+
+ user
+ |> for_user_query()
+ |> unread_mentions_in_timeframe_query(args)
+ |> Repo.all()
+ end
+
+ @spec update_notified_at([pos_integer()]) :: {non_neg_integer(), nil}
+ def update_notified_at(ids \\ []) do
+ from(n in __MODULE__,
+ where: n.id in ^ids
+ )
+ |> Repo.update_all(set: [notified_at: NaiveDateTime.utc_now()])
+ end
end
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index b78777141..c5ea1ba10 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -131,7 +131,11 @@ defmodule Pleroma.User do
field(:hide_followers, :boolean, default: false)
field(:hide_follows, :boolean, default: false)
field(:hide_favorites, :boolean, default: true)
- field(:email_notifications, :map, default: %{"digest" => false})
+
+ field(:email_notifications, :map,
+ default: %{"digest" => false, "notifications" => ["mention", "pleroma:chat_mention"]}
+ )
+
field(:mascot, :map, default: nil)
field(:emoji, :map, default: %{})
field(:pleroma_settings_store, :map, default: %{})
@@ -525,7 +529,8 @@ defmodule Pleroma.User do
:is_discoverable,
:actor_type,
:accepts_chat_messages,
- :disclose_client
+ :disclose_client,
+ :email_notifications
]
)
|> unique_constraint(:nickname)
@@ -2390,17 +2395,14 @@ defmodule Pleroma.User do
|> update_and_set_cache()
end
+ @spec update_email_notifications(t(), map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
def update_email_notifications(user, settings) do
- email_notifications =
- user.email_notifications
- |> Map.merge(settings)
- |> Map.take(["digest"])
+ email_notifications = Map.merge(user.email_notifications, settings)
- params = %{email_notifications: email_notifications}
fields = [:email_notifications]
user
- |> cast(params, fields)
+ |> cast(%{email_notifications: email_notifications}, fields)
|> validate_required(fields)
|> update_and_set_cache()
end
@@ -2431,8 +2433,8 @@ defmodule Pleroma.User do
end
end
- @spec add_to_block(User.t(), User.t()) ::
- {:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
+ @spec remove_from_block(User.t(), User.t()) ::
+ {:ok, UserRelationship.t() | nil} | {:error, Ecto.Changeset.t()}
defp remove_from_block(%User{} = user, %User{} = blocked) do
with {:ok, relationship} <- UserRelationship.delete_block(user, blocked) do
@cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex
index 54e5ebc76..d0d7d751e 100644
--- a/lib/pleroma/web/api_spec/operations/account_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/account_operation.ex
@@ -635,7 +635,8 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
description:
"Discovery (listing, indexing) of this account by external services (search bots etc.) is allowed."
},
- actor_type: ActorType
+ actor_type: ActorType,
+ email_notifications: email_notifications()
},
example: %{
bot: false,
@@ -760,6 +761,31 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
}
end
+ defp email_notifications do
+ %Schema{
+ title: "EmailNotificationsObject",
+ description: "User Email notification settings",
+ type: :object,
+ properties: %{
+ digest: %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Whether the account receives digest email"
+ },
+ notifications: %Schema{
+ type: :array,
+ nullable: true,
+ description: "List of notification types to receive by Email",
+ items: %Schema{type: :string}
+ }
+ },
+ example: %{
+ "digest" => true,
+ "notifications" => ["mention", "pleroma:chat_mention"]
+ }
+ }
+ end
+
defp array_of_lists do
%Schema{
title: "ArrayOfLists",
diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
index 7a1e99044..a8f11786b 100644
--- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
@@ -213,6 +213,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|> Maps.put_if_present(:is_locked, params[:locked])
# Note: param name is indeed :discoverable (not an error)
|> Maps.put_if_present(:is_discoverable, params[:discoverable])
+ |> Maps.put_if_present(:email_notifications, params[:email_notifications])
# What happens here:
#
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index ac25aefdd..4165aa890 100644
--- a/lib/pleroma/web/mastodon_api/views/account_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -279,7 +279,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
skip_thread_containment: user.skip_thread_containment,
background_image: image_url(user.background) |> MediaProxy.url(),
accepts_chat_messages: user.accepts_chat_messages,
- favicon: favicon
+ favicon: favicon,
+ email_notifications: user.email_notifications
}
}
|> maybe_put_role(user, opts[:for])
diff --git a/lib/pleroma/web/templates/email/digest.html.eex b/lib/pleroma/web/templates/email/digest.html.eex
index 60eceff22..9afb2ca49 100644
--- a/lib/pleroma/web/templates/email/digest.html.eex
+++ b/lib/pleroma/web/templates/email/digest.html.eex
@@ -126,7 +126,7 @@
<div align="center" class="img-container center"
style="padding-right: 0px;padding-left: 0px;">
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr style="line-height:0px"><td style="padding-right: 0px;padding-left: 0px;" align="center"><![endif]--><img
- align="center" alt="Image" border="0" class="center" src="cid:logo.svg"
+ align="center" alt="Image" border="0" class="center" src="cid:<%= @logo %>"
style="text-decoration: none; -ms-interpolation-mode: bicubic; border: 0; height: 80px; width: auto; max-height: 80px; display: block;"
title="Image" height="80" />
<!--[if mso]></td></tr></table><![endif]-->
diff --git a/lib/pleroma/web/templates/email/mentions.html.eex b/lib/pleroma/web/templates/email/mentions.html.eex
new file mode 100644
index 000000000..c95dc97f1
--- /dev/null
+++ b/lib/pleroma/web/templates/email/mentions.html.eex
@@ -0,0 +1,439 @@
+<!DOCTYPE html
+ PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office"
+ xmlns:v="urn:schemas-microsoft-com:vml">
+
+<head>
+ <!--[if gte mso 9]><xml><o:OfficeDocumentSettings><o:AllowPNG/><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml><![endif]-->
+ <meta content="text/html; charset=utf-8" http-equiv="Content-Type" />
+ <meta content="width=device-width" name="viewport" />
+ <!--[if !mso]><!-->
+ <meta content="IE=edge" http-equiv="X-UA-Compatible" />
+ <!--<![endif]-->
+ <title><%= @email.subject %><</title>
+ <!--[if !mso]><!-->
+ <!--<![endif]-->
+ <style type="text/css">
+ body {
+ margin: 0;
+ padding: 0;
+ }
+
+ a {
+
+ color: <%= @styling.link_color %>;
+ text-decoration: none;
+ }
+
+ table,
+ td,
+ tr {
+ vertical-align: top;
+ border-collapse: collapse;
+ }
+
+ * {
+ line-height: inherit;
+ }
+
+ a[x-apple-data-detectors=true] {
+ color: inherit !important;
+ text-decoration: none !important;
+ }
+ </style>
+ <style id="media-query" type="text/css">
+ @media (max-width: 610px) {
+
+ .block-grid,
+ .col {
+ min-width: 320px !important;
+ max-width: 100% !important;
+ display: block !important;
+ }
+
+ .block-grid {
+ width: 100% !important;
+ }
+
+ .col {
+ width: 100% !important;
+ }
+
+ .col>div {
+ margin: 0 auto;
+ }
+
+ .no-stack .col {
+ min-width: 0 !important;
+ display: table-cell !important;
+ }
+
+ .no-stack.two-up .col {
+ width: 50% !important;
+ }
+
+ .no-stack .col.num4 {
+ width: 33% !important;
+ }
+
+ .no-stack .col.num8 {
+ width: 66% !important;
+ }
+
+ .no-stack .col.num4 {
+ width: 33% !important;
+ }
+
+ .no-stack .col.num3 {
+ width: 25% !important;
+ }
+
+ .no-stack .col.num6 {
+ width: 50% !important;
+ }
+
+ .no-stack .col.num9 {
+ width: 75% !important;
+ }
+
+ }
+ </style>
+</head>
+
+<body class="clean-body" style="margin: 0; padding: 0; -webkit-text-size-adjust: 100%; background-color: <%= @styling.background_color %>;">
+ <!--[if IE]><div class="ie-browser"><![endif]-->
+ <table bgcolor="<%= @styling.background_color %>" cellpadding="0" cellspacing="0" class="nl-container" role="presentation"
+ style="table-layout: fixed; vertical-align: top; min-width: 320px; Margin: 0 auto; border-spacing: 0; border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: <%= @styling.background_color %>; width: 100%;"
+ valign="top" width="100%">
+ <tbody>
+ <tr style="vertical-align: top;" valign="top">
+ <td style="word-break: break-word; vertical-align: top;" valign="top">
+ <!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td align="center" style="background-color:<%= @styling.background_color %>"><![endif]-->
+ <div style="background-color:transparent;">
+ <div class="block-grid"
+ style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;">
+ <div style="border-collapse: collapse;display: table;width: 100%;background-color:<%= @styling.content_background_color%>;">
+ <!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:transparent;"><tr><td align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:590px"><tr class="layout-full-width" style="background-color:<%= @styling.content_background_color%>"><![endif]-->
+ <!--[if (mso)|(IE)]><td align="center" width="590" style="background-color:<%= @styling.content_background_color%>;width:590px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 0px; padding-left: 0px; padding-top:5px; padding-bottom:5px;"><![endif]-->
+ <div class="col num12"
+ style="min-width: 320px; max-width: 590px; display: table-cell; vertical-align: top; width: 590px;">
+ <div style="width:100% !important;">
+ <!--[if (!mso)&(!IE)]><!-->
+ <div
+ style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 0px; padding-left: 0px;">
+ <!--<![endif]-->
+ <div align="center" class="img-container center"
+ style="padding-right: 0px;padding-left: 0px;">
+ <!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr style="line-height:0px"><td style="padding-right: 0px;padding-left: 0px;" align="center"><![endif]--><img
+ align="center" alt="Image" border="0" class="center" src="cid:<%= @logo %>"
+ style="text-decoration: none; -ms-interpolation-mode: bicubic; border: 0; height: 80px; width: auto; max-height: 80px; display: block;"
+ title="Image" height="80" />
+ <!--[if mso]></td></tr></table><![endif]-->
+ </div>
+ <!--[if (!mso)&(!IE)]><!-->
+ </div>
+ <!--<![endif]-->
+ </div>
+ </div>
+ <!--[if (mso)|(IE)]></td></tr></table><![endif]-->
+ <!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
+ </div>
+ </div>
+ </div>
+ <div style="background-color:transparent;">
+ <div class="block-grid"
+ style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;">
+ <div style="border-collapse: collapse;display: table;width: 100%;background-color:<%= @styling.content_background_color%>;">
+ <!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:transparent;"><tr><td align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:590px"><tr class="layout-full-width" style="background-color:<%= @styling.content_background_color%>"><![endif]-->
+ <!--[if (mso)|(IE)]><td align="center" width="590" style="background-color:<%= @styling.content_background_color%>;width:590px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 0px; padding-left: 0px; padding-top:5px; padding-bottom:5px;"><![endif]-->
+ <div class="col num12"
+ style="min-width: 320px; max-width: 590px; display: table-cell; vertical-align: top; width: 590px;">
+ <div style="width:100% !important;">
+ <!--[if (!mso)&(!IE)]><!-->
+ <div
+ style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 0px; padding-left: 0px;">
+ <!--<![endif]-->
+ <!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
+ <div
+ style="line-height:120%;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
+ <div
+ style="font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif;line-height: 14px; color: <%= @styling.header_color %>;">
+ <p style="line-height: 36px; text-align: center; margin: 0;"><span
+ style="font-size: 30px; color: <%= @styling.header_color %>;">Hey <%= @user.nickname %>, here is what you've missed!</span></p>
+ </div>
+ </div>
+ <!--[if mso]></td></tr></table><![endif]-->
+ <!--[if (!mso)&(!IE)]><!-->
+ </div>
+ <!--<![endif]-->
+ </div>
+ </div>
+ <!--[if (mso)|(IE)]></td></tr></table><![endif]-->
+ <!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
+ </div>
+ </div>
+ </div>
+ <div style="background-color:transparent;">
+ <div class="block-grid"
+ style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;">
+ <div style="border-collapse: collapse;display: table;width: 100%;background-color:<%= @styling.content_background_color%>;">
+ <!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:transparent;"><tr><td align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:590px"><tr class="layout-full-width" style="background-color:<%= @styling.content_background_color%>"><![endif]-->
+ <!--[if (mso)|(IE)]><td align="center" width="590" style="background-color:<%= @styling.content_background_color%>;width:590px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 15px; padding-left: 15px; padding-top:5px; padding-bottom:5px;"><![endif]-->
+ <div class="col num12"
+ style="min-width: 320px; max-width: 590px; display: table-cell; vertical-align: top; width: 590px;">
+ <div style="width:100% !important;">
+ <!--[if (!mso)&(!IE)]><!-->
+ <div
+ style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 15px; padding-left: 15px;">
+ <!--<![endif]-->
+ <table border="0" cellpadding="0" cellspacing="0" class="divider" role="presentation"
+ style="table-layout: fixed; vertical-align: top; border-spacing: 0; border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; min-width: 100%; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;"
+ valign="top" width="100%">
+ <tbody>
+ <tr style="vertical-align: top;" valign="top">
+ <td class="divider_inner"
+ style="word-break: break-word; vertical-align: top; min-width: 100%; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; padding-top: 10px; padding-right: 10px; padding-bottom: 10px; padding-left: 10px;"
+ valign="top">
+ <table align="center" border="0" cellpadding="0" cellspacing="0" class="divider_content"
+ height="0" role="presentation"
+ style="table-layout: fixed; vertical-align: top; border-spacing: 0; border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; border-top: 1px solid <%= @styling.text_color %>; height: 0px;"
+ valign="top" width="100%">
+ <tbody>
+ <tr style="vertical-align: top;" valign="top">
+ <td height="0"
+ style="word-break: break-word; vertical-align: top; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;"
+ valign="top"><span></span></td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
+ <div
+ style="color:<%= @styling.text_color %>;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;line-height:120%;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
+ <p
+ style="font-size: 12px; line-height: 24px; text-align: center; color: <%= @styling.text_color %>; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; margin: 0;">
+ <span style="font-size: 20px;">Mentions</span></p>
+ </div>
+ <!--[if mso]></td></tr></table><![endif]-->
+ <!--[if (!mso)&(!IE)]><!-->
+ </div>
+ <!--<![endif]-->
+ </div>
+ </div>
+ <!--[if (mso)|(IE)]></td></tr></table><![endif]-->
+ <!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
+ </div>
+ </div>
+ </div>
+
+ <%= for %{data: mention, object: object, from: from} <- @mentions do %>
+ <%# mention START %>
+ <%# user card START %>
+ <div style="background-color:transparent;">
+ <div class="block-grid mixed-two-up no-stack"
+ style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;">
+ <div style="border-collapse: collapse;display: table;width: 100%;background-color:<%= @styling.content_background_color%>;">
+ <!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:transparent;"><tr><td align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:590px"><tr class="layout-full-width" style="background-color:<%= @styling.content_background_color%>"><![endif]-->
+ <!--[if (mso)|(IE)]><td align="center" width="147" style="background-color:<%= @styling.content_background_color%>;width:76px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 0px; padding-left: 20px; padding-top:5px; padding-bottom:5px;"><![endif]-->
+ <div class="col num3"
+ style="display: table-cell; vertical-align: top; max-width: 320px; min-width: 76px; width: 76px;">
+ <div style="width:100% !important;">
+ <!--[if (!mso)&(!IE)]><!-->
+ <div
+ style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 0px; padding-left: 20px;">
+ <!--<![endif]-->
+ <div align="left" class="img-container left "
+ style="padding-right: 0px;padding-left: 0px;">
+ <!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr style="line-height:0px"><td style="padding-right: 0px;padding-left: 0px;" align="left"><![endif]--><img
+ alt="<%= from.name %>" border="0" class="left " src="<%= avatar_url(from) %>"
+ style="text-decoration: none; -ms-interpolation-mode: bicubic; border: 0; height: auto; width: 100%; max-width: 76px; display: block;"
+ title="<%= from.name %>" width="76" />
+ <!--[if mso]></td></tr></table><![endif]-->
+ </div>
+ <!--[if (!mso)&(!IE)]><!-->
+ </div>
+ <!--<![endif]-->
+ </div>
+ </div>
+
+ <!--[if (mso)|(IE)]></td></tr></table><![endif]-->
+ <!--[if (mso)|(IE)]></td><td align="center" width="442" style="background-color:<%= @styling.content_background_color%>;width:442px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 0px; padding-left: 0px; padding-top:5px; padding-bottom:5px;"><![endif]-->
+ <div class="col num9"
+ style="display: table-cell; vertical-align: top; min-width: 320px; max-width: 441px; width: 442px;">
+ <div style="width:100% !important;">
+ <!--[if (!mso)&(!IE)]><!-->
+ <div
+ style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 0px; padding-left: 0px;">
+ <!--<![endif]-->
+ <!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
+ <div
+ style="color:<%= @styling.text_color %>;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;line-height:120%;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
+ <div
+ style="font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; font-size: 12px; line-height: 14px; color: <%= @styling.text_color %>;">
+ <p style="font-size: 14px; line-height: 19px; margin: 0;"><span
+ style="font-size: 16px; color: <%= @styling.text_color %>;"><%= from.name %></span></p>
+ <p style="font-size: 14px; line-height: 19px; margin: 0;"><span
+ style="font-size: 16px;"><%= link "@" <> from.nickname, style: "color: #{@styling.link_color};text-decoration: none;", to: mention.activity.actor %></span></p>
+ </div>
+ </div>
+ <!--[if mso]></td></tr></table><![endif]-->
+ <!--[if (!mso)&(!IE)]><!-->
+ </div>
+ <!--<![endif]-->
+ </div>
+ </div>
+ <!--[if (mso)|(IE)]></td></tr></table><![endif]-->
+ <!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
+ </div>
+ </div>
+ </div>
+ <%# user card END %>
+
+ <div style="background-color:transparent;">
+ <div class="block-grid"
+ style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;">
+ <div style="border-collapse: collapse;display: table;width: 100%;background-color:<%= @styling.content_background_color%>;">
+ <!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:transparent;"><tr><td align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:590px"><tr class="layout-full-width" style="background-color:<%= @styling.content_background_color%>"><![endif]-->
+ <!--[if (mso)|(IE)]><td align="center" width="590" style="background-color:<%= @styling.content_background_color%>;width:590px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 15px; padding-left: 15px; padding-top:5px; padding-bottom:5px;"><![endif]-->
+ <div class="col num12"
+ style="min-width: 320px; max-width: 590px; display: table-cell; vertical-align: top; width: 590px;">
+ <div style="width:100% !important;">
+ <!--[if (!mso)&(!IE)]><!-->
+ <div
+ style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 15px; padding-left: 15px;">
+ <!--<![endif]-->
+ <!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
+ <div
+ style="color:<%= @styling.text_color %>;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;line-height:120%;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
+ <div
+ style="font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; font-size: 12px; line-height: 14px; color: <%= @styling.text_color %>;">
+ <span style="font-size: 16px; line-height: 19px;"><%= raw object.data["content"] %></span></div>
+ </div>
+ <!--[if mso]></td></tr></table><![endif]-->
+ <!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 15px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
+ <div
+ style="color:<%= @styling.text_muted_color %>;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;line-height:120%;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:15px;">
+ <div
+ style="font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; font-size: 12px; line-height: 14px; color: <%= @styling.text_muted_color %>;">
+ <p style="font-size: 14px; line-height: 16px; margin: 0;"><%= format_date object.data["published"] %></p>
+ </div>
+ </div>
+ <!--[if mso]></td></tr></table><![endif]-->
+ <!--[if (!mso)&(!IE)]><!-->
+ </div>
+ <!--<![endif]-->
+ </div>
+ </div>
+ <!--[if (mso)|(IE)]></td></tr></table><![endif]-->
+ <!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
+ </div>
+ </div>
+ </div>
+ <%# mention END %>
+ <% end %>
+
+ <%# divider start %>
+ <div style="background-color:transparent;">
+ <div class="block-grid"
+ style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;">
+ <div style="border-collapse: collapse;display: table;width: 100%;background-color:<%= @styling.content_background_color%>;">
+ <!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:transparent;"><tr><td align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:590px"><tr class="layout-full-width" style="background-color:<%= @styling.content_background_color%>"><![endif]-->
+ <!--[if (mso)|(IE)]><td align="center" width="590" style="background-color:<%= @styling.content_background_color%>;width:590px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 0px; padding-left: 0px; padding-top:5px; padding-bottom:5px;"><![endif]-->
+ <div class="col num12"
+ style="min-width: 320px; max-width: 590px; display: table-cell; vertical-align: top; width: 590px;">
+ <div style="width:100% !important;">
+ <!--[if (!mso)&(!IE)]><!-->
+ <div
+ style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 0px; padding-left: 0px;">
+ <!--<![endif]-->
+ <table border="0" cellpadding="0" cellspacing="0" class="divider" role="presentation"
+ style="table-layout: fixed; vertical-align: top; border-spacing: 0; border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; min-width: 100%; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;"
+ valign="top" width="100%">
+ <tbody>
+ <tr style="vertical-align: top;" valign="top">
+ <td class="divider_inner"
+ style="word-break: break-word; vertical-align: top; min-width: 100%; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; padding-top: 10px; padding-right: 10px; padding-bottom: 10px; padding-left: 10px;"
+ valign="top">
+ <table align="center" border="0" cellpadding="0" cellspacing="0" class="divider_content"
+ height="0" role="presentation"
+ style="table-layout: fixed; vertical-align: top; border-spacing: 0; border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; border-top: 1px solid <%= @styling.text_color %>; height: 0px;"
+ valign="top" width="100%">
+ <tbody>
+ <tr style="vertical-align: top;" valign="top">
+ <td height="0"
+ style="word-break: break-word; vertical-align: top; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;"
+ valign="top"><span></span></td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <!--[if (!mso)&(!IE)]><!-->
+ </div>
+ <!--<![endif]-->
+ </div>
+ </div>
+ <!--[if (mso)|(IE)]></td></tr></table><![endif]-->
+ <!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
+ </div>
+ </div>
+ </div>
+
+ <%# divider end %>
+
+
+ <div style="background-color:transparent;">
+ <div class="block-grid"
+ style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;">
+ <div style="border-collapse: collapse;display: table;width: 100%;background-color:<%= @styling.content_background_color%>;">
+ <!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:transparent;"><tr><td align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:590px"><tr class="layout-full-width" style="background-color:<%= @styling.content_background_color%>"><![endif]-->
+ <!--[if (mso)|(IE)]><td align="center" width="590" style="background-color:<%= @styling.content_background_color%>;width:590px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 0px; padding-left: 0px; padding-top:5px; padding-bottom:5px;"><![endif]-->
+ <div class="col num12"
+ style="min-width: 320px; max-width: 590px; display: table-cell; vertical-align: top; width: 590px;">
+ <div style="width:100% !important;">
+ <!--[if (!mso)&(!IE)]><!-->
+ <div
+ style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 0px; padding-left: 0px;">
+ <!--<![endif]-->
+ <!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
+ <div
+ style="color:<%= @styling.text_color %>;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;line-height:120%;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
+ <p
+ style="font-size: 12px; line-height: 16px; text-align: center; color: <%= @styling.text_color %>; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; margin: 0;">
+ <span style="font-size: 14px;">You have received this email because you have signed up to receive digest emails from <b><%= @instance %></b> Pleroma instance.</span></p>
+ <p
+ style="font-size: 12px; line-height: 14px; text-align: center; color: <%= @styling.text_color %>; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; margin: 0;">
+  </p>
+ <p
+ style="font-size: 12px; line-height: 16px; text-align: center; color: <%= @styling.text_color %>; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; margin: 0;">
+ <span style="font-size: 14px;">The email address you are subscribed as is <a href="mailto:<%= @user.email %>" style="color: <%= @styling.link_color %>;text-decoration: none;"><%= @user.email %></a>. </span></p>
+ <p
+ style="font-size: 12px; line-height: 16px; text-align: center; color: <%= @styling.text_color %>; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; margin: 0;">
+ <span style="font-size: 14px;">To unsubscribe, please go <%= link "here", style: "color: #{@styling.link_color};text-decoration: none;", to: @unsubscribe_link %>.</span></p>
+ </div>
+ <!--[if mso]></td></tr></table><![endif]-->
+ <!--[if (!mso)&(!IE)]><!-->
+ </div>
+ <!--<![endif]-->
+ </div>
+ </div>
+ <!--[if (mso)|(IE)]></td></tr></table><![endif]-->
+ <!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
+ </div>
+ </div>
+ </div>
+ <!--[if (mso)|(IE)]></td></tr></table><![endif]-->
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <!--[if (IE)]></div><![endif]-->
+</body>
+
+</html>
diff --git a/lib/pleroma/workers/cron/email_mentions_worker.ex b/lib/pleroma/workers/cron/email_mentions_worker.ex
new file mode 100644
index 000000000..791f09c28
--- /dev/null
+++ b/lib/pleroma/workers/cron/email_mentions_worker.ex
@@ -0,0 +1,67 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Workers.Cron.EmailMentionsWorker do
+ use Pleroma.Workers.WorkerHelper, queue: "email_mentions"
+
+ @impl true
+ def perform(%Job{args: %{"op" => "email_mentions", "user_id" => id}}) do
+ user = Pleroma.User.get_cached_by_id(id)
+
+ timeframe =
+ Pleroma.Config.get([__MODULE__, :timeframe], 30)
+ |> :timer.minutes()
+
+ max_inserted_at =
+ NaiveDateTime.utc_now()
+ |> NaiveDateTime.add(-timeframe, :millisecond)
+ |> NaiveDateTime.truncate(:second)
+
+ mentions = Pleroma.Notification.for_user_unread_mentions(user, max_inserted_at)
+
+ if mentions != [] do
+ user
+ |> Pleroma.Emails.UserEmail.mentions_notification_email(mentions)
+ |> Pleroma.Emails.Mailer.deliver()
+ |> case do
+ {:ok, _} ->
+ Enum.map(mentions, & &1.id)
+
+ _ ->
+ []
+ end
+ |> Pleroma.Notification.update_notified_at()
+ end
+
+ :ok
+ end
+
+ @impl true
+ def perform(_) do
+ config = Pleroma.Config.get(__MODULE__, [])
+
+ if Keyword.get(config, :enabled, false) do
+ timeframe = Keyword.get(config, :timeframe, 30)
+ period = timeframe * 60
+
+ max_at =
+ NaiveDateTime.utc_now()
+ |> NaiveDateTime.add(-:timer.minutes(timeframe), :millisecond)
+ |> NaiveDateTime.truncate(:second)
+
+ Pleroma.Notification.users_ids_with_unread_mentions(max_at)
+ |> Enum.each(&insert_job(&1, unique: [period: period]))
+ end
+
+ :ok
+ end
+
+ defp insert_job(user_id, args) do
+ Pleroma.Workers.Cron.EmailMentionsWorker.enqueue(
+ "email_mentions",
+ %{"user_id" => user_id},
+ args
+ )
+ end
+end
diff --git a/priv/repo/migrations/20201128144004_change_user_email_notifications_setting.exs b/priv/repo/migrations/20201128144004_change_user_email_notifications_setting.exs
new file mode 100644
index 000000000..6ccedfbd0
--- /dev/null
+++ b/priv/repo/migrations/20201128144004_change_user_email_notifications_setting.exs
@@ -0,0 +1,32 @@
+defmodule Pleroma.Repo.Migrations.ChangeUserEmailNotificationsSetting do
+ use Ecto.Migration
+
+ import Ecto.Query, only: [from: 2]
+
+ def up, do: stream_and_update_users(:up)
+
+ def down, do: stream_and_update_users(:down)
+
+ defp stream_and_update_users(direction) do
+ from(u in Pleroma.User, select: [:id, :email_notifications])
+ |> Pleroma.Repo.stream()
+ |> Stream.each(&update_user_email_notifications_settings(&1, direction))
+ |> Stream.run()
+ end
+
+ defp update_user_email_notifications_settings(user, direction) do
+ email_notifications = change_email_notifications(user.email_notifications, direction)
+
+ user
+ |> Ecto.Changeset.change(email_notifications: email_notifications)
+ |> Pleroma.Repo.update()
+ end
+
+ defp change_email_notifications(email_notifications, :up) do
+ Map.put(email_notifications, "notifications", ["mention", "pleroma:chat_mention"])
+ end
+
+ defp change_email_notifications(email_notifications, :down) do
+ Map.delete(email_notifications, "notifications")
+ end
+end
diff --git a/priv/repo/migrations/20201214160053_add_notified_at_to_notifications.exs b/priv/repo/migrations/20201214160053_add_notified_at_to_notifications.exs
new file mode 100644
index 000000000..4e9255e09
--- /dev/null
+++ b/priv/repo/migrations/20201214160053_add_notified_at_to_notifications.exs
@@ -0,0 +1,15 @@
+defmodule Pleroma.Repo.Migrations.AddNotifiedAtToNotifications do
+ use Ecto.Migration
+
+ def up do
+ alter table(:notifications) do
+ add_if_not_exists(:notified_at, :naive_datetime)
+ end
+ end
+
+ def down do
+ alter table(:notifications) do
+ remove_if_exists(:notified_at, :naive_datetime)
+ end
+ end
+end
diff --git a/priv/repo/migrations/20201221060622_fill_notifications_notified_at.exs b/priv/repo/migrations/20201221060622_fill_notifications_notified_at.exs
new file mode 100644
index 000000000..f02899ab7
--- /dev/null
+++ b/priv/repo/migrations/20201221060622_fill_notifications_notified_at.exs
@@ -0,0 +1,23 @@
+defmodule Pleroma.Repo.Migrations.FillNotificationsNotifiedAt do
+ use Ecto.Migration
+
+ import Ecto.Query, only: [from: 2]
+
+ @types ["mention", "pleroma:chat_mention"]
+
+ def up do
+ from(n in "notifications",
+ where: is_nil(n.notified_at),
+ where: n.type in ^@types
+ )
+ |> Pleroma.Repo.update_all(set: [notified_at: NaiveDateTime.utc_now()])
+ end
+
+ def down do
+ from(n in "notifications",
+ where: not is_nil(n.notified_at),
+ where: n.type in ^@types
+ )
+ |> Pleroma.Repo.update_all(set: [notified_at: nil])
+ end
+end
diff --git a/priv/repo/migrations/20201222052839_add_index_to_notifications.exs b/priv/repo/migrations/20201222052839_add_index_to_notifications.exs
new file mode 100644
index 000000000..5b0420296
--- /dev/null
+++ b/priv/repo/migrations/20201222052839_add_index_to_notifications.exs
@@ -0,0 +1,7 @@
+defmodule Pleroma.Repo.Migrations.AddIndexToNotifications do
+ use Ecto.Migration
+
+ def change do
+ create_if_not_exists(index(:notifications, [:seen, :notified_at, :type, :inserted_at]))
+ end
+end
diff --git a/test/mix/tasks/pleroma/user_test.exs b/test/mix/tasks/pleroma/user_test.exs
index a2178bbd1..5b170b57c 100644
--- a/test/mix/tasks/pleroma/user_test.exs
+++ b/test/mix/tasks/pleroma/user_test.exs
@@ -617,4 +617,93 @@ defmodule Mix.Tasks.Pleroma.UserTest do
assert mod.is_confirmed
end
end
+
+ describe "email_notifications" do
+ setup do
+ user = insert(:user, email_notifications: %{"digest" => false, "notifications" => []})
+ [user: user]
+ end
+
+ test "no changes error", %{user: user} do
+ Mix.Tasks.Pleroma.User.run(["email_notifications", user.nickname])
+
+ assert_received {:mix_shell, :error, ["No changes passed"]}
+ end
+
+ test "user not found" do
+ Mix.Tasks.Pleroma.User.run(["email_notifications", "nickname", "--digest"])
+
+ assert_received {:mix_shell, :error, ["No local user nickname"]}
+ end
+
+ test "all settings", %{user: user} do
+ assert user.email_notifications == %{"digest" => false, "notifications" => []}
+
+ Mix.Tasks.Pleroma.User.run([
+ "email_notifications",
+ user.nickname,
+ "--digest",
+ "-n",
+ "mention,pleroma:chat_mention,"
+ ])
+
+ from_db = User.get_cached_by_nickname(user.nickname)
+
+ assert from_db.email_notifications == %{
+ "digest" => true,
+ "notifications" => ["mention", "pleroma:chat_mention"]
+ }
+
+ Mix.Tasks.Pleroma.User.run([
+ "email_notifications",
+ user.nickname,
+ "--no-digest",
+ "-n",
+ "off"
+ ])
+
+ from_db = User.get_cached_by_nickname(user.nickname)
+ assert from_db.email_notifications == %{"digest" => false, "notifications" => []}
+ end
+
+ test "partial update", %{user: user} do
+ Mix.Tasks.Pleroma.User.run([
+ "email_notifications",
+ user.nickname,
+ "--digest"
+ ])
+
+ from_db = User.get_cached_by_nickname(user.nickname)
+ assert from_db.email_notifications == %{"digest" => true, "notifications" => []}
+
+ Mix.Tasks.Pleroma.User.run([
+ "email_notifications",
+ user.nickname,
+ "--no-digest"
+ ])
+
+ from_db = User.get_cached_by_nickname(user.nickname)
+ assert from_db.email_notifications == %{"digest" => false, "notifications" => []}
+
+ Mix.Tasks.Pleroma.User.run([
+ "email_notifications",
+ user.nickname,
+ "-n",
+ "mention"
+ ])
+
+ from_db = User.get_cached_by_nickname(user.nickname)
+ assert from_db.email_notifications == %{"digest" => false, "notifications" => ["mention"]}
+
+ Mix.Tasks.Pleroma.User.run([
+ "email_notifications",
+ user.nickname,
+ "-n",
+ "off"
+ ])
+
+ from_db = User.get_cached_by_nickname(user.nickname)
+ assert from_db.email_notifications == %{"digest" => false, "notifications" => []}
+ end
+ end
end
diff --git a/test/pleroma/notification_test.exs b/test/pleroma/notification_test.exs
index abf1b0410..28ac57ed2 100644
--- a/test/pleroma/notification_test.exs
+++ b/test/pleroma/notification_test.exs
@@ -1155,4 +1155,94 @@ defmodule Pleroma.NotificationTest do
assert length(Notification.for_user(user)) == 1
end
end
+
+ describe "users_ids_with_unread_mentions/0" do
+ setup do
+ now = NaiveDateTime.utc_now()
+ inserted_at = NaiveDateTime.add(now, -61)
+
+ insert(:notification, seen: true, type: "mention", inserted_at: inserted_at)
+ insert(:notification, type: "follow", inserted_at: inserted_at)
+ insert(:notification, type: "mention")
+ mention = insert(:notification, type: "mention", inserted_at: inserted_at)
+ chat_mention = insert(:notification, type: "pleroma:chat_mention", inserted_at: inserted_at)
+
+ insert(:notification,
+ type: "mention",
+ notified_at: now,
+ inserted_at: inserted_at
+ )
+
+ [
+ mention: mention,
+ chat_mention: chat_mention,
+ now: now
+ ]
+ end
+
+ test "when mentions are in the timeframe", %{
+ mention: mention,
+ chat_mention: chat_mention,
+ now: now
+ } do
+ assert Notification.users_ids_with_unread_mentions(NaiveDateTime.add(now, -60)) == [
+ mention.user_id,
+ chat_mention.user_id
+ ]
+ end
+
+ test "when mentions are out of the timeframe", %{now: now} do
+ assert Notification.users_ids_with_unread_mentions(NaiveDateTime.add(now, -62)) == []
+ end
+ end
+
+ describe "for_user_unread_mentions/1" do
+ setup do
+ [user, muted, blocked] = insert_list(3, :user)
+ {:ok, _} = User.mute(user, muted)
+ {:ok, _} = CommonAPI.post(muted, %{status: "hey @#{user.nickname}"})
+ {:ok, _} = User.block(user, blocked)
+ {:ok, _} = CommonAPI.post(blocked, %{status: "hey @#{user.nickname}"})
+
+ insert(:notification, type: "mention", user: user)
+ insert(:notification, type: "pleroma:chat_mention", user: user)
+
+ inserted_at = NaiveDateTime.add(NaiveDateTime.utc_now(), -61)
+ Repo.update_all(Notification, set: [inserted_at: inserted_at])
+ [user: user, max: NaiveDateTime.add(NaiveDateTime.utc_now(), -60)]
+ end
+
+ test "when mentions are in timeframe, exclude blocks and mutes", %{user: user, max: max} do
+ assert Repo.aggregate(Notification, :count, :id) == 4
+ assert user |> Notification.for_user_unread_mentions(max) |> length() == 2
+ end
+
+ test "when mentions are out of the timeframe, exclude blocks and mutes", %{
+ user: user,
+ max: max
+ } do
+ assert Notification.for_user_unread_mentions(user, NaiveDateTime.add(max, -2)) == []
+ end
+
+ test "respect user notification types", %{user: user, max: max} do
+ user =
+ Map.update!(
+ user,
+ :email_notifications,
+ &Map.put(&1, "notifications", ["pleroma:chat_mention"])
+ )
+
+ [mention] = Notification.for_user_unread_mentions(user, max)
+ assert mention.type == "pleroma:chat_mention"
+ end
+ end
+
+ test "update_notified_at/1" do
+ notifs = insert_list(2, :notification)
+
+ assert {2, nil} =
+ notifs
+ |> Enum.map(& &1.id)
+ |> Notification.update_notified_at()
+ end
end
diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs
index d81c1b8eb..060d14101 100644
--- a/test/pleroma/user_test.exs
+++ b/test/pleroma/user_test.exs
@@ -2239,18 +2239,19 @@ defmodule Pleroma.UserTest do
end
end
- describe "update_email_notifications/2" do
- setup do
- user = insert(:user, email_notifications: %{"digest" => true})
-
- {:ok, user: user}
- end
-
- test "Notifications are updated", %{user: user} do
- true = user.email_notifications["digest"]
- assert {:ok, result} = User.update_email_notifications(user, %{"digest" => false})
- assert result.email_notifications["digest"] == false
- end
+ test "update_email_notifications/2" do
+ user = insert(:user, email_notifications: %{"digest" => false, "notifications" => []})
+ assert user.email_notifications["digest"] == false
+ assert user.email_notifications["notifications"] == []
+
+ assert {:ok, result} =
+ User.update_email_notifications(user, %{
+ "digest" => true,
+ "notifications" => ["mention", "pleroma:chat_mention"]
+ })
+
+ assert result.email_notifications["digest"]
+ assert result.email_notifications["notifications"] == ["mention", "pleroma:chat_mention"]
end
describe "local_nickname/1" do
diff --git a/test/pleroma/web/mastodon_api/update_credentials_test.exs b/test/pleroma/web/mastodon_api/update_credentials_test.exs
index cfbe6cf0e..d24264fea 100644
--- a/test/pleroma/web/mastodon_api/update_credentials_test.exs
+++ b/test/pleroma/web/mastodon_api/update_credentials_test.exs
@@ -206,6 +206,38 @@ defmodule Pleroma.Web.MastodonAPI.UpdateCredentialsTest do
assert user_data["source"]["pleroma"]["no_rich_text"] == true
end
+ test "updates the user's email_notifications setting", %{conn: conn} do
+ resp =
+ patch(conn, "/api/v1/accounts/update_credentials", %{
+ email_notifications: %{
+ "digest" => true,
+ "notifications" => []
+ }
+ })
+
+ assert user_data = json_response_and_validate_schema(resp, 200)
+
+ assert user_data["pleroma"]["email_notifications"] == %{
+ "digest" => true,
+ "notifications" => []
+ }
+
+ resp =
+ patch(conn, "/api/v1/accounts/update_credentials", %{
+ email_notifications: %{
+ "digest" => false,
+ "notifications" => ["mention", "pleroma:chat_mention"]
+ }
+ })
+
+ assert user_data = json_response_and_validate_schema(resp, 200)
+
+ assert user_data["pleroma"]["email_notifications"] == %{
+ "digest" => false,
+ "notifications" => ["mention", "pleroma:chat_mention"]
+ }
+ end
+
test "updates the user's name", %{conn: conn} do
conn =
patch(conn, "/api/v1/accounts/update_credentials", %{"display_name" => "markorepairs"})
diff --git a/test/pleroma/web/mastodon_api/views/account_view_test.exs b/test/pleroma/web/mastodon_api/views/account_view_test.exs
index 5373a17c3..d69224319 100644
--- a/test/pleroma/web/mastodon_api/views/account_view_test.exs
+++ b/test/pleroma/web/mastodon_api/views/account_view_test.exs
@@ -90,7 +90,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
hide_follows_count: false,
relationship: %{},
skip_thread_containment: false,
- accepts_chat_messages: nil
+ accepts_chat_messages: nil,
+ email_notifications: %{
+ "digest" => false,
+ "notifications" => ["mention", "pleroma:chat_mention"]
+ }
}
}
@@ -190,7 +194,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
hide_follows_count: false,
relationship: %{},
skip_thread_containment: false,
- accepts_chat_messages: nil
+ accepts_chat_messages: nil,
+ email_notifications: %{
+ "digest" => false,
+ "notifications" => ["mention", "pleroma:chat_mention"]
+ }
}
}
diff --git a/test/pleroma/workers/cron/email_mentions_worker_test.exs b/test/pleroma/workers/cron/email_mentions_worker_test.exs
new file mode 100644
index 000000000..3ffe2561d
--- /dev/null
+++ b/test/pleroma/workers/cron/email_mentions_worker_test.exs
@@ -0,0 +1,107 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Workers.Cron.EmailMentionsWorkerTest do
+ use Pleroma.DataCase
+ use Oban.Testing, repo: Pleroma.Repo
+
+ import Pleroma.Factory
+ import Swoosh.TestAssertions
+
+ alias Pleroma.Workers.Cron.EmailMentionsWorker
+
+ setup do
+ clear_config(EmailMentionsWorker, enabled: true, timeframe: 1)
+ inserted_at = NaiveDateTime.add(NaiveDateTime.utc_now(), -61)
+
+ n1 = insert(:notification, seen: true, type: "mention", inserted_at: inserted_at)
+ n2 = insert(:notification, type: "follow", inserted_at: inserted_at)
+ n3 = insert(:notification, type: "mention")
+ mention = insert(:notification, type: "mention", inserted_at: inserted_at)
+ chat_mention = insert(:notification, type: "pleroma:chat_mention", inserted_at: inserted_at)
+
+ n4 =
+ insert(:notification,
+ type: "mention",
+ notified_at: NaiveDateTime.utc_now(),
+ inserted_at: inserted_at
+ )
+
+ [
+ mention: mention,
+ chat_mention: chat_mention,
+ other_user_ids: [n1.user_id, n2.user_id, n3.user_id, n4.user_id]
+ ]
+ end
+
+ test "creates jobs for users", %{
+ mention: mention,
+ chat_mention: chat_mention,
+ other_user_ids: ids
+ } do
+ assert EmailMentionsWorker.perform(%{}) == :ok
+
+ assert_enqueued(
+ worker: EmailMentionsWorker,
+ args: %{op: "email_mentions", user_id: mention.user_id}
+ )
+
+ assert_enqueued(
+ worker: EmailMentionsWorker,
+ args: %{op: "email_mentions", user_id: chat_mention.user_id}
+ )
+
+ Enum.each(ids, fn id ->
+ refute_enqueued(worker: EmailMentionsWorker, args: %{op: "email_mentions", user_id: id})
+ end)
+
+ assert Repo.aggregate(Oban.Job, :count, :id) == 2
+
+ EmailMentionsWorker.perform(%{})
+
+ # no duplicates
+ assert Repo.aggregate(Oban.Job, :count, :id) == 2
+ end
+
+ test "doesn't create jobs for users without emails", %{mention: mention} do
+ %{user: user} = Repo.preload(mention, :user)
+
+ user
+ |> Ecto.Changeset.change(email: nil)
+ |> Repo.update()
+
+ assert EmailMentionsWorker.perform(%{}) == :ok
+
+ refute_enqueued(
+ worker: EmailMentionsWorker,
+ args: %{op: "email_mentions", user_id: mention.user_id}
+ )
+ end
+
+ test "sends emails", %{mention: mention, chat_mention: chat_mention} do
+ assert EmailMentionsWorker.perform(%{}) == :ok
+
+ mention = Repo.preload(mention, :user)
+
+ assert EmailMentionsWorker.perform(%Oban.Job{
+ args: %{"op" => "email_mentions", "user_id" => mention.user_id}
+ }) == :ok
+
+ assert_email_sent(
+ to: {mention.user.name, mention.user.email},
+ html_body: ~r/here is what you've missed!/i
+ )
+
+ chat_mention = Repo.preload(chat_mention, :user)
+
+ assert EmailMentionsWorker.perform(%Oban.Job{
+ args: %{"op" => "email_mentions", "user_id" => chat_mention.user_id}
+ }) == :ok
+
+ assert_email_sent(
+ to: {chat_mention.user.name, chat_mention.user.email},
+ html_body: ~r/here is what you've missed!/i
+ )
+ end
+end
diff --git a/test/support/factory.ex b/test/support/factory.ex
index 5c4e65c81..136435396 100644
--- a/test/support/factory.ex
+++ b/test/support/factory.ex
@@ -469,7 +469,8 @@ defmodule Pleroma.Factory do
def notification_factory do
%Pleroma.Notification{
- user: build(:user)
+ user: build(:user),
+ activity: build(:note_activity)
}
end