PhoenixKit.Emails.RateLimiter (phoenix_kit v1.6.15)

View Source

Rate limiting and spam protection for the email system.

Provides multiple layers of protection against abuse, spam, and suspicious email patterns:

  • Per-recipient limits - Prevent spam to individual email addresses
  • Per-sender limits - Control email volume from specific senders
  • Global system limits - Overall system protection
  • User-specific limits - Temporary reduced limits for flagged users
  • Automatic blocklists - Dynamic blocking of suspicious patterns
  • Pattern detection - ML-style spam pattern recognition
  • User monitoring - Event tracking for suspicious behavior

Settings Integration

All rate limiting settings are stored in phoenix_kit_settings:

  • email_rate_limit_per_recipient - Max emails per recipient per hour (default: 100)
  • email_rate_limit_global - Global max emails per hour (default: 10_000)
  • email_blocklist_enabled - Enable automatic blocklisting (default: true)

User-specific settings (stored as JSON):

  • user_rate_limits_<user_id> - Temporary reduced limits for specific users
  • user_monitoring_<user_id> - Event tracking log for user behavior

Usage Examples

# Check if sending is allowed
case PhoenixKit.Emails.RateLimiter.check_limits(email) do
  :ok ->
    # Send email

  {:blocked, :recipient_limit} ->
    # Handle recipient rate limit

  {:blocked, :global_limit} ->
    # Handle global rate limit

  {:blocked, :blocklist} ->
    # Handle blocklisted recipient
end

# Flag suspicious user activity
PhoenixKit.Emails.RateLimiter.flag_suspicious_activity(user_id, "high_bounce_rate")
# => :flagged (user gets reduced limits for 24 hours)

# Check user's current limit status
status = PhoenixKit.Emails.RateLimiter.get_user_limit_status(user_id)
# => %{has_custom_limits: true, active_recipient_limit: 10, ...}

# Clear user's custom limits
PhoenixKit.Emails.RateLimiter.clear_user_rate_limits(user_id)
# => :ok

# Add suspicious email to blocklist
PhoenixKit.Emails.RateLimiter.add_to_blocklist(
  "spam@example.com",
  "suspicious_pattern",
  expires_at: DateTime.add(DateTime.utc_now(), 86_400)
)

# Check current rate limit status
status = PhoenixKit.Emails.RateLimiter.get_rate_limit_status()
# => %{recipient_count: 45, global_count: 2341, blocked_count: 12}

Rate Limiting Strategy

Uses a sliding window approach with Redis-like atomic operations in PostgreSQL:

  1. Sliding Window: Tracks counts over rolling time periods
  2. Efficient Storage: Uses single table with automatic cleanup
  3. Atomic Operations: Prevents race conditions with database locks
  4. Memory Efficient: Automatically expires old tracking data
  5. User-Specific Limits: JSON settings for temporary user restrictions

User Behavior Management

  • Reduced Limits: Automatically reduce limits for users with high bounce rates
  • Email Blocking: Block user emails for serious violations (spam complaints)
  • Activity Monitoring: Track suspicious patterns for future analysis
  • Automatic Expiration: Limits and blocks expire after configured periods
  • Manual Override: Admin can clear user restrictions via API

Automatic Blocklist Features

  • Pattern Detection: Identifies bulk spam patterns
  • Bounce Rate Monitoring: Blocks high-bounce senders
  • Complaint Rate Monitoring: Blocks high-complaint addresses
  • Frequency Analysis: Detects unusual sending patterns
  • Temporary Blocks: Automatic expiration of blocks
  • User Integration: Links blocked emails to user accounts

Integration Points

Integrates with:

Summary

Functions

Add email address to blocklist.

Check if email address is blocklisted.

Check global system-wide rate limits.

Check all rate limits for an outgoing email.

Check if recipient email address is within rate limits.

Check if sender email address is within rate limits.

Checks if a user has custom rate limits applied.

Clears custom rate limits for a specific user.

Count blocked emails with optional filtering.

Analyze email for suspicious spam patterns.

Flag suspicious activity for a user.

Get blocklist statistics.

Get current rate limit status across all dimensions.

Gets comprehensive rate limit status for a specific user.

Gets monitoring events for a specific user.

Check if email address is currently blocked.

List all blocked emails with optional filtering.

Remove email address from blocklist.

Functions

add_to_blocklist(email, reason, opts \\ [])

Add email address to blocklist.

Options

  • :reason - Reason for blocking (string)
  • :expires_at - When block expires (DateTime, nil for permanent)
  • :user_id - User ID that triggered the block

Examples

# Temporary block for 24 hours
RateLimiter.add_to_blocklist(
  "spam@example.com",
  "bulk_spam_pattern",
  expires_at: DateTime.add(DateTime.utc_now(), 86_400)
)

# Permanent block
RateLimiter.add_to_blocklist("malicious@example.com", "manual_block")

check_blocklist(email)

Check if email address is blocklisted.

Examples

iex> RateLimiter.check_blocklist("user@example.com")
:ok

iex> RateLimiter.check_blocklist("spam@blocked.com")
{:blocked, :blocklist}

check_global_limit(period \\ :hour)

Check global system-wide rate limits.

Examples

iex> RateLimiter.check_global_limit()
:ok

check_limits(email_attrs)

Check all rate limits for an outgoing email.

Returns :ok if email can be sent, or {:blocked, reason} if blocked.

Examples

iex> RateLimiter.check_limits(%{to: "user@example.com", from: "app@mysite.com"})
:ok

iex> RateLimiter.check_limits(%{to: "blocked@spam.com"})
{:blocked, :blocklist}

check_recipient_limit(recipient_email, period \\ :hour)

Check if recipient email address is within rate limits.

Examples

iex> RateLimiter.check_recipient_limit("user@example.com")
:ok

iex> RateLimiter.check_recipient_limit("high-volume@example.com")  
{:blocked, :recipient_limit}

check_sender_limit(sender_email, period \\ :hour)

Check if sender email address is within rate limits.

Examples

iex> RateLimiter.check_sender_limit("app@mysite.com")
:ok

check_user_limits(user_id)

Checks if a user has custom rate limits applied.

Returns user's custom limits if they exist and haven't expired, otherwise returns nil.

Examples

iex> RateLimiter.check_user_limits(123)
%{
  "recipient_limit" => 10,
  "sender_limit" => 50,
  "reason" => "high_bounce_rate",
  "applied_at" => "2025-01-15T12:00:00Z",
  "expires_at" => "2025-01-16T12:00:00Z"
}

iex> RateLimiter.check_user_limits(999)
nil

clear_user_rate_limits(user_id)

Clears custom rate limits for a specific user.

Removes any reduced limits or custom restrictions applied to the user, returning them to default system limits.

Examples

iex> RateLimiter.clear_user_rate_limits(123)
:ok

Returns

  • :ok - Limits cleared successfully

count_blocklist(opts \\ %{})

Count blocked emails with optional filtering.

Examples

iex> RateLimiter.count_blocklist()
42

iex> RateLimiter.count_blocklist(%{reason: "bounce_limit"})
15

detect_spam_patterns(email_log)

Analyze email for suspicious spam patterns.

Returns a list of detected patterns or empty list if clean.

Examples

iex> RateLimiter.detect_spam_patterns(email_log)
[]

iex> RateLimiter.detect_spam_patterns(suspicious_email_log)
["high_frequency", "bulk_template"]

flag_suspicious_activity(user_id, reason)

Flag suspicious activity for a user.

Automatically triggers blocklist or rate limit adjustments based on activity patterns.

Examples

iex> RateLimiter.flag_suspicious_activity(123, "high_bounce_rate")
:flagged

iex> RateLimiter.flag_suspicious_activity(456, "complaint_spam")
:blocked

get_blocklist_stats()

Get blocklist statistics.

Returns a map with statistics about blocked emails.

Examples

iex> RateLimiter.get_blocklist_stats()
%{
  total_blocks: 42,
  active_blocks: 38,
  expired_today: 4,
  by_reason: %{"manual_block" => 10, "bounce_limit" => 28, ...}
}

get_rate_limit_status()

Get current rate limit status across all dimensions.

Examples

iex> RateLimiter.get_rate_limit_status()
%{
  global: %{count: 1250, limit: 10_000, percentage: 12.5},
  recipients: %{active_limits: 5, total_emails: 892},
  senders: %{active_limits: 2, total_emails: 1250},
  blocklist: %{active_blocks: 15, expired_today: 3}
}

get_user_limit_status(user_id)

Gets comprehensive rate limit status for a specific user.

Returns a map with user's current limits, monitoring status, and any active restrictions.

Examples

iex> RateLimiter.get_user_limit_status(123)
%{
  user_id: 123,
  has_custom_limits: true,
  custom_limits: %{"recipient_limit" => 10, "sender_limit" => 50},
  monitoring: %{"event_count" => 5, "last_event_at" => "..."},
  is_blocked: false,
  default_recipient_limit: 100,
  default_sender_limit: 1000
}

iex> RateLimiter.get_user_limit_status(999)
%{
  user_id: 999,
  has_custom_limits: false,
  custom_limits: nil,
  monitoring: nil,
  is_blocked: false,
  default_recipient_limit: 100,
  default_sender_limit: 1000
}

get_user_monitoring_events(user_id)

Gets monitoring events for a specific user.

Returns the monitoring log with all tracked events for the user, or nil if no monitoring exists.

Examples

iex> RateLimiter.get_user_monitoring_events(123)
%{
  "events" => [
    %{"event_type" => "bulk_sending", "timestamp" => "...", "metadata" => %{...}},
    %{"event_type" => "high_bounce_rate", "timestamp" => "...", "metadata" => %{...}}
  ],
  "event_count" => 2,
  "first_event_at" => "2025-01-15T12:00:00Z",
  "last_event_at" => "2025-01-15T18:00:00Z"
}

iex> RateLimiter.get_user_monitoring_events(999)
nil

is_blocked?(email)

Check if email address is currently blocked.

Examples

iex> RateLimiter.is_blocked?("user@example.com")
false

iex> RateLimiter.is_blocked?("blocked@spam.com")
true

list_blocklist(opts \\ %{})

List all blocked emails with optional filtering.

Options

  • :search - Search term for email address
  • :reason - Filter by block reason
  • :include_expired - Include expired blocks (default: false)
  • :limit - Limit number of results
  • :offset - Offset for pagination
  • :order_by - Order field (:email, :inserted_at, :expires_at)
  • :order_dir - Order direction (:asc, :desc)

Examples

iex> RateLimiter.list_blocklist()
[%EmailBlocklist{}, ...]

iex> RateLimiter.list_blocklist(%{reason: "manual_block", limit: 10})
[%EmailBlocklist{}, ...]

remove_from_blocklist(email)

Remove email address from blocklist.

Examples

iex> RateLimiter.remove_from_blocklist("user@example.com")
:ok