Packages

phoenix_kit

1.7.21
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
phoenix_kit lib modules emails web details.ex
Raw

lib/modules/emails/web/details.ex

defmodule PhoenixKit.Modules.Emails.Web.Details do
@moduledoc """
LiveView for displaying detailed information about a specific email log.
Provides comprehensive view of email metadata, delivery status, events timeline,
and performance analytics for individual emails.
## Features
- **Complete Email Metadata**: Headers, size, attachments, template info
- **Events Timeline**: Chronological view of all email events
- **Delivery Status**: Real-time status tracking and updates
- **Geographic Data**: Location info for opens and clicks
- **Performance Metrics**: Individual email analytics
- **Debugging Info**: Technical details for troubleshooting
- **Related Emails**: Other emails in same campaign/template
## Route
This LiveView is mounted at `{prefix}/admin/emails/:id` and requires
appropriate admin permissions.
Note: `{prefix}` is your configured PhoenixKit URL prefix (default: `/phoenix_kit`).
## Usage
# In your Phoenix router
live "/email-logs/:id", PhoenixKitWeb.Live.Modules.Emails.EmailDetailsLive, :show
## 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.Log
alias PhoenixKit.Settings
alias PhoenixKit.Utils.Date, as: UtilsDate
alias PhoenixKit.Utils.Routes
import PhoenixKitWeb.Components.Core.Icons, only: [icon_arrow_left: 1]
## --- Lifecycle Callbacks ---
@impl true
def mount(%{"id" => id}, _session, socket) do
# Check if email is enabled
if Emails.enabled?() do
case Integer.parse(id) do
{email_id, _} ->
# Get project title from settings
project_title = Settings.get_setting("project_title", "PhoenixKit")
socket =
socket
|> assign(:email_id, email_id)
|> assign(:project_title, project_title)
|> assign(:email_log, nil)
|> assign(:events, [])
|> assign(:related_emails, [])
|> assign(:loading, true)
|> assign(:syncing, false)
|> load_email_data()
{:ok, socket}
_ ->
{:ok,
socket
|> put_flash(:error, "Invalid email ID")
|> push_navigate(to: Routes.path("/admin/emails"))}
end
else
{:ok,
socket
|> put_flash(:error, "Email is not enabled")
|> push_navigate(to: Routes.path("/admin/emails"))}
end
end
## --- Event Handlers ---
@impl true
def handle_event("refresh", _params, socket) do
{:noreply,
socket
|> assign(:loading, true)
|> load_email_data()}
end
@impl true
def handle_event("sync_status", _params, socket) do
if socket.assigns.email_log do
# Determine which message ID to use for sync (prefer AWS message ID)
{message_id, id_type} =
if socket.assigns.email_log.aws_message_id do
{socket.assigns.email_log.aws_message_id, "AWS SES message ID"}
else
{socket.assigns.email_log.message_id, "internal message ID"}
end
socket = assign(socket, :syncing, true)
case Emails.sync_email_status(message_id) do
{:ok, result} ->
flash_message = build_sync_flash_message(result, id_type)
flash_type = determine_flash_type(result)
socket =
socket
|> assign(:syncing, false)
|> put_flash(flash_type, flash_message)
|> load_email_data()
{:noreply, socket}
{:error, reason} ->
flash_message = build_error_flash_message(reason, message_id, socket, id_type)
socket =
socket
|> assign(:syncing, false)
|> put_flash(:error, flash_message)
{:noreply, socket}
end
else
{:noreply, put_flash(socket, :error, "❌ Email log not found")}
end
end
@impl true
def handle_event("toggle_headers", _params, socket) do
{:noreply,
socket
|> assign(:show_headers, !Map.get(socket.assigns, :show_headers, false))}
end
@impl true
def handle_event("toggle_body", _params, socket) do
{:noreply,
socket
|> assign(:show_body, !Map.get(socket.assigns, :show_body, false))}
end
@impl true
def handle_event("view_related", %{"campaign_id" => campaign_id}, socket) do
{:noreply,
socket
|> push_navigate(to: Routes.path("/admin/emails?campaign_id=#{campaign_id}"))}
end
@impl true
def handle_event("view_related", %{"template_name" => template_name}, socket) do
{:noreply,
socket
|> push_navigate(to: Routes.path("/admin/emails?template_name=#{template_name}"))}
end
## --- Template ---
## --- Private Helper Functions ---
# Load email data and related information
defp load_email_data(socket) do
email_id = socket.assigns.email_id
try do
email_log = Emails.get_log!(email_id)
events = Emails.list_events_for_log(email_id)
related_emails = get_related_emails(email_log)
socket
|> assign(:email_log, email_log)
|> assign(:events, events)
|> assign(:related_emails, related_emails)
|> assign(:loading, false)
|> assign(:show_headers, false)
|> assign(:show_body, false)
|> assign(:page_title, "Email ##{email_id}")
rescue
Ecto.NoResultsError ->
socket
|> assign(:email_log, nil)
|> assign(:events, [])
|> assign(:related_emails, [])
|> assign(:loading, false)
error ->
Logger.error("Failed to load email data: #{inspect(error)}")
socket
|> assign(:loading, false)
|> put_flash(:error, "Failed to load email data")
end
end
# Get related emails (same campaign or template)
defp get_related_emails(%Log{
campaign_id: campaign_id,
template_name: template_name,
id: current_id
}) do
filters = %{limit: 10}
filters =
cond do
campaign_id -> Map.put(filters, :campaign_id, campaign_id)
template_name -> Map.put(filters, :template_name, template_name)
true -> filters
end
Emails.list_logs(filters)
|> Enum.reject(fn log -> log.id == current_id end)
end
defp get_related_emails(_), do: []
# Build event details list for success message
defp build_event_details(sqs_events, dlq_events, events_failed) do
details = []
details =
if sqs_events > 0, do: ["#{sqs_events} from SQS" | details], else: details
details =
if dlq_events > 0, do: ["#{dlq_events} from DLQ" | details], else: details
if events_failed > 0, do: ["#{events_failed} failed" | details], else: details
end
# Build success flash message
defp build_sync_flash_message(result, id_type) do
events_processed = Map.get(result, :events_processed, 0)
total_events_found = Map.get(result, :total_events_found, 0)
sqs_events = Map.get(result, :sqs_events_found, 0)
dlq_events = Map.get(result, :dlq_events_found, 0)
events_failed = Map.get(result, :events_failed, 0)
existing_log_found = Map.get(result, :existing_log_found, false)
log_updated = Map.get(result, :log_updated, false)
message = Map.get(result, :message, nil)
cond do
total_events_found > 0 and events_processed > 0 ->
details = build_event_details(sqs_events, dlq_events, events_failed)
source_info = if Enum.empty?(details), do: "", else: " (#{Enum.join(details, ", ")})"
status_info = if log_updated, do: " - Email status updated", else: ""
"✅ Processed #{events_processed}/#{total_events_found} events#{source_info}#{status_info} using #{id_type}"
total_events_found > 0 and events_processed == 0 ->
"⚠️ Found #{total_events_found} events but none could be processed successfully using #{id_type}"
not existing_log_found ->
"ℹ️ No email log found in database for #{id_type}. Events may be for a different email."
true ->
search_info = " (searched using #{id_type})"
(message || "No new events found in SQS or DLQ queues") <> search_info
end
end
# Determine flash type based on sync results
defp determine_flash_type(result) do
events_processed = Map.get(result, :events_processed, 0)
total_events_found = Map.get(result, :total_events_found, 0)
existing_log_found = Map.get(result, :existing_log_found, false)
cond do
events_processed > 0 -> :info
total_events_found > 0 and events_processed == 0 -> :warning
not existing_log_found -> :warning
true -> :info
end
end
# Build ID info string for error messages
defp build_id_info(message_id, email_log, id_type) do
if message_id == email_log.message_id do
" (using #{id_type})"
else
" (using #{id_type}: #{String.slice(message_id, 0, 20)}...)"
end
end
# Build error flash message
defp build_error_flash_message(reason, message_id, socket, id_type) do
case reason do
"AWS credentials not configured. Please set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables." ->
"❌ AWS credentials not configured. Please set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables."
"Email is disabled. Please enable it in settings." ->
"❌ Email is disabled. Please enable it in admin settings."
reason ->
id_info = build_id_info(message_id, socket.assigns.email_log, id_type)
"❌ Sync failed: #{reason}#{id_info}"
end
end
end