Packages
phoenix_kit
1.7.29
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/modules/emails/web/blocklist.ex
defmodule PhoenixKit.Modules.Emails.Web.Blocklist do
@moduledoc """
LiveView for managing email blocklist and blocked addresses.
Provides comprehensive management of blocked email addresses, including:
- **Blocklist Viewing**: List all blocked email addresses with filtering
- **Block Management**: Add/remove email addresses from blocklist
- **Bulk Operations**: Import/export blocklists, bulk add/remove
- **Temporary Blocks**: Set expiration dates for temporary blocks
- **Block Reasons**: Categorize blocks by reason (spam, bounce, manual, etc.)
- **Search & Filter**: Find blocked addresses by email, reason, or date
## Features
- **Real-time Updates**: Live updates when blocks are added/removed
- **CSV Import/Export**: Bulk management through CSV files
- **Automatic Blocking**: Integration with rate limiter for auto-blocks
- **Audit Trail**: Track who blocked addresses and when
- **Expiration Management**: Automatic cleanup of expired blocks
- **Statistics**: Analytics on blocked addresses and reasons
## Route
This LiveView is mounted at `{prefix}/admin/emails/blocklist` and requires
appropriate admin permissions.
Note: `{prefix}` is your configured PhoenixKit URL prefix (default: `/phoenix_kit`).
## Usage
# In your Phoenix router
live "/email-blocklist", PhoenixKitWeb.Live.Modules.Emails.EmailBlocklistLive, :index
## Permissions
Access is restricted to users with admin or owner roles in PhoenixKit.
"""
use PhoenixKitWeb, :live_view
require Logger
alias PhoenixKit.Modules.Emails
alias PhoenixKit.Modules.Emails.RateLimiter
alias PhoenixKit.Settings
alias PhoenixKit.Utils.Date, as: UtilsDate
alias PhoenixKit.Utils.Routes
import PhoenixKitWeb.Components.Core.Icons, only: [icon_arrow_left: 1]
# Auto-refresh every 30 seconds
@refresh_interval 30_000
# Items per page for pagination
@per_page 50
## --- Lifecycle Callbacks ---
@impl true
def mount(_params, _session, socket) do
# Check if email system is enabled
if Emails.enabled?() do
# Get project title from settings
project_title = Settings.get_project_title()
# Schedule periodic refresh
if connected?(socket) do
Process.send_after(self(), :refresh_blocklist, @refresh_interval)
end
socket =
socket
|> assign(:project_title, project_title)
|> assign(:url_path, Routes.path("/admin/emails/blocklist"))
|> assign(:loading, true)
|> assign(:blocked_emails, [])
|> assign(:total_blocked, 0)
|> assign(:page, 1)
|> assign(:per_page, @per_page)
|> assign(:search_term, "")
|> assign(:reason_filter, "")
|> assign(:status_filter, "all")
|> assign(:selected_emails, [])
|> assign(:show_add_form, false)
|> assign(:show_import_form, false)
|> assign(:bulk_action, nil)
|> assign(:last_updated, DateTime.utc_now())
|> assign(:statistics, %{})
|> load_blocklist_data()
{:ok, socket}
else
{:ok,
socket
|> put_flash(:error, "Email is not enabled")
|> push_navigate(to: Routes.path("/admin"))}
end
end
## --- Event Handlers ---
@impl true
def handle_event("refresh", _params, socket) do
{:noreply,
socket
|> assign(:loading, true)
|> load_blocklist_data()}
end
@impl true
def handle_event("filter_search", %{"search" => search_term}, socket) do
{:noreply,
socket
|> assign(:search_term, search_term)
|> assign(:page, 1)
|> load_blocklist_data()}
end
@impl true
def handle_event("filter_reason", %{"reason" => reason}, socket) do
{:noreply,
socket
|> assign(:reason_filter, reason)
|> assign(:page, 1)
|> load_blocklist_data()}
end
@impl true
def handle_event("filter_status", %{"status" => status}, socket) do
{:noreply,
socket
|> assign(:status_filter, status)
|> assign(:page, 1)
|> load_blocklist_data()}
end
@impl true
def handle_event("change_page", %{"page" => page}, socket) do
case Integer.parse(page) do
{page_num, _} when page_num > 0 ->
{:noreply,
socket
|> assign(:page, page_num)
|> load_blocklist_data()}
_ ->
{:noreply, socket}
end
end
@impl true
def handle_event("toggle_add_form", _params, socket) do
{:noreply,
socket
|> assign(:show_add_form, !socket.assigns.show_add_form)}
end
@impl true
def handle_event("toggle_import_form", _params, socket) do
{:noreply,
socket
|> assign(:show_import_form, !socket.assigns.show_import_form)}
end
@impl true
def handle_event("add_block", params, socket) do
%{
"email" => email,
"reason" => reason,
"expires_at" => expires_at
} = params
opts = []
opts =
if expires_at && expires_at != "" do
case Date.from_iso8601(expires_at) do
{:ok, date} ->
expires_datetime = DateTime.new!(date, ~T[23:59:59])
[expires_at: expires_datetime] ++ opts
_ ->
opts
end
else
opts
end
case RateLimiter.add_to_blocklist(email, reason, opts) do
:ok ->
{:noreply,
socket
|> assign(:show_add_form, false)
|> put_flash(:info, "Email address blocked successfully")
|> load_blocklist_data()}
{:error, reason} ->
{:noreply,
socket
|> put_flash(:error, "Failed to block email: #{reason}")}
end
end
@impl true
def handle_event("remove_block", %{"email" => email}, socket) do
RateLimiter.remove_from_blocklist(email)
{:noreply,
socket
|> put_flash(:info, "Email address unblocked successfully")
|> load_blocklist_data()}
end
@impl true
def handle_event("toggle_email_selection", %{"email" => email}, socket) do
selected = socket.assigns.selected_emails
new_selected =
if email in selected do
List.delete(selected, email)
else
[email | selected]
end
{:noreply,
socket
|> assign(:selected_emails, new_selected)}
end
@impl true
def handle_event("select_all_visible", _params, socket) do
all_emails = Enum.map(socket.assigns.blocked_emails, & &1.email)
{:noreply,
socket
|> assign(:selected_emails, all_emails)}
end
@impl true
def handle_event("clear_selection", _params, socket) do
{:noreply,
socket
|> assign(:selected_emails, [])
|> assign(:bulk_action, nil)}
end
@impl true
def handle_event("set_bulk_action", %{"action" => action}, socket) do
{:noreply,
socket
|> assign(:bulk_action, action)}
end
@impl true
def handle_event("execute_bulk_action", _params, socket) do
case socket.assigns.bulk_action do
"remove" ->
execute_bulk_remove(socket)
"export" ->
execute_bulk_export(socket)
_ ->
{:noreply,
socket
|> put_flash(:error, "Invalid bulk action")}
end
end
@impl true
def handle_event("export_blocklist", %{"format" => format}, socket) do
case format do
"csv" ->
csv_content = export_blocklist_csv(socket.assigns.blocked_emails)
filename = "email_blocklist_#{Date.utc_today()}.csv"
{:noreply,
socket
|> push_event("download", %{
filename: filename,
content: csv_content,
mime_type: "text/csv"
})}
"json" ->
json_content = Jason.encode!(socket.assigns.blocked_emails, pretty: true)
filename = "email_blocklist_#{Date.utc_today()}.json"
{:noreply,
socket
|> push_event("download", %{
filename: filename,
content: json_content,
mime_type: "application/json"
})}
_ ->
{:noreply,
socket
|> put_flash(:error, "Unsupported export format")}
end
end
@impl true
def handle_event("import_csv", %{"csv_content" => csv_content}, socket) do
case import_blocklist_csv(csv_content) do
{:ok, imported_count} ->
{:noreply,
socket
|> assign(:show_import_form, false)
|> put_flash(:info, "Successfully imported #{imported_count} blocked emails")
|> load_blocklist_data()}
{:error, reason} ->
{:noreply,
socket
|> put_flash(:error, "Import failed: #{reason}")}
end
end
@impl true
def handle_info(:refresh_blocklist, socket) do
# Schedule next refresh
Process.send_after(self(), :refresh_blocklist, @refresh_interval)
{:noreply,
socket
|> assign(:last_updated, DateTime.utc_now())
|> load_blocklist_data()}
end
defp load_blocklist_data(socket) do
filters = build_filters(socket.assigns)
# Load blocked emails using RateLimiter API
blocked_emails = load_blocked_emails(filters)
total_blocked = count_blocked_emails(filters)
statistics = load_blocklist_statistics()
# Calculate total pages for pagination
total_pages = ceil(total_blocked / socket.assigns.per_page)
socket
|> assign(:blocked_emails, blocked_emails)
|> assign(:total_blocked, total_blocked)
|> assign(:total_pages, total_pages)
|> assign(:statistics, statistics)
|> assign(:loading, false)
end
defp build_filters(assigns) do
opts = %{}
opts =
if assigns.search_term && assigns.search_term != "" do
Map.put(opts, :search, assigns.search_term)
else
opts
end
opts =
if assigns.reason_filter && assigns.reason_filter != "" do
Map.put(opts, :reason, assigns.reason_filter)
else
opts
end
opts =
if assigns.status_filter == "expired" do
Map.put(opts, :include_expired, true)
else
opts
end
# Pagination
offset = (assigns.page - 1) * assigns.per_page
opts
|> Map.put(:limit, assigns.per_page)
|> Map.put(:offset, offset)
|> Map.put(:order_by, :inserted_at)
|> Map.put(:order_dir, :desc)
end
defp load_blocked_emails(filters) do
RateLimiter.list_blocklist(filters)
end
defp count_blocked_emails(filters) do
# Remove pagination params for count
filters
|> Map.delete(:limit)
|> Map.delete(:offset)
|> RateLimiter.count_blocklist()
end
defp load_blocklist_statistics do
RateLimiter.get_blocklist_stats()
end
defp execute_bulk_remove(socket) do
selected_emails = socket.assigns.selected_emails
success_count =
Enum.reduce(selected_emails, 0, fn email, acc ->
RateLimiter.remove_from_blocklist(email)
acc + 1
end)
message = "Removed #{success_count} of #{length(selected_emails)} emails from blocklist"
{:noreply,
socket
|> assign(:selected_emails, [])
|> assign(:bulk_action, nil)
|> put_flash(:info, message)
|> load_blocklist_data()}
end
defp execute_bulk_export(socket) do
selected_emails = socket.assigns.selected_emails
blocked_data = Enum.filter(socket.assigns.blocked_emails, &(&1.email in selected_emails))
csv_content = export_blocklist_csv(blocked_data)
filename = "selected_blocklist_#{Date.utc_today()}.csv"
{:noreply,
socket
|> push_event("download", %{
filename: filename,
content: csv_content,
mime_type: "text/csv"
})}
end
defp export_blocklist_csv(blocked_emails) do
headers = "email,reason,added_at,expires_at\n"
rows =
Enum.map_join(blocked_emails, "\n", fn blocked ->
expires_str =
if blocked.expires_at,
do: Date.to_iso8601(DateTime.to_date(blocked.expires_at)),
else: ""
"#{blocked.email},#{blocked.reason},#{Date.to_iso8601(DateTime.to_date(blocked.inserted_at))},#{expires_str}"
end)
headers <> rows
end
defp import_blocklist_csv(csv_content) do
lines =
String.split(csv_content, "\n") |> Enum.map(&String.trim/1) |> Enum.reject(&(&1 == ""))
# Skip header line if it looks like headers
lines =
case List.first(lines) do
"email,reason" <> _ -> List.delete_at(lines, 0)
_ -> lines
end
imported_count =
Enum.reduce(lines, 0, fn line, acc ->
case parse_csv_line(line) do
{:ok, email, reason, expires_at} ->
opts = if expires_at, do: [expires_at: expires_at], else: []
case RateLimiter.add_to_blocklist(email, reason, opts) do
:ok -> acc + 1
_ -> acc
end
_ ->
acc
end
end)
{:ok, imported_count}
rescue
_ -> {:error, "Invalid CSV format"}
end
defp parse_csv_line(line) do
parts = String.split(line, ",") |> Enum.map(&String.trim/1)
case parts do
[email, reason] ->
{:ok, email, reason, nil}
[email, reason, ""] ->
{:ok, email, reason, nil}
[email, reason, expires_str] ->
case Date.from_iso8601(expires_str) do
{:ok, date} -> {:ok, email, reason, DateTime.new!(date, ~T[23:59:59])}
_ -> {:ok, email, reason, nil}
end
_ ->
{:error, "Invalid line format"}
end
end
end