MailglassInbound.RateLimiter (MailglassInbound v0.2.0)

Copy Markdown View Source

Inbound-local multi-bucket ETS token-bucket rate limiter (IOPS-04, D-49-11).

Cloned from Mailglass.RateLimiter — the load-bearing :ets.update_counter/4 refill math is copied verbatim. Adapted for inbound:

  • Three buckets, fail-fast via with, in order tenant (1000/min) -> recipient (500/min) -> sender_domain (200/min). The first bucket to trip returns ITS OWN Retry-After — never a cross-bucket max (D-49-13).
  • No stream-based bypass clause — inbound has no stream semantics, so the core limiter's auth-stream short-circuit is intentionally dropped (D-49-13).
  • No %Mailglass.Message{} coupling — takes plain args.
  • Reads :mailglass_inbound config via MailglassInbound.Config, never :mailglass (D-49-02).
  • Builds Mailglass.RateLimitError internally (reuse the struct, never re-create it): :per_tenant for the tenant bucket, :per_domain for the recipient + sender_domain buckets.

Hot path is :ets.update_counter/4 on the :mailglass_inbound_rate_limit table owned by MailglassInbound.RateLimiter.TableOwner — no GenServer mailbox serialization (D-49-12).

PII discipline (D-49-16)

The sender bucket is keyed on the sender DOMAIN only, never the full sender address. The recipient bucket may key on the full recipient address — it is the routing identity, already persisted in clear, lives only in node-local ETS, and is never logged, serialized, or emitted. No hashing required. The error context and any telemetry/HTTP body carry the bucket type (:tenant | :recipient | :sender_domain), never the key value. This comment exists so a future lint pass does not false-positive on the recipient-address ETS key.

Per-node scope (D-49-18)

Counters live in node-local ETS — an N-node cluster enforces N x the limit. Acceptable for the single-node-default library posture; cluster-global enforcement is out of scope until a shared-backend option ships.

Summary

Functions

Returns :ok when the inbound message is under all three bucket limits, or {:error, %RateLimitError{}} when the first-evaluated bucket (tenant -> recipient -> sender_domain) is depleted.

Functions

check(tenant_id, recipient, sender_domain)

(since 1.2.0)
@spec check(String.t(), String.t() | nil, String.t() | nil) ::
  :ok | {:error, Mailglass.RateLimitError.t()}

Returns :ok when the inbound message is under all three bucket limits, or {:error, %RateLimitError{}} when the first-evaluated bucket (tenant -> recipient -> sender_domain) is depleted.

Arguments:

  • tenant_id — the resolved tenant scope (tenant bucket key).
  • recipient — the envelope recipient / first to address (recipient bucket key; may be the full address, see PII discipline).
  • sender_domain — the DOMAIN of the first from address (sender bucket key; never the full sender address).

The returned error's retry_after_ms is the tripped bucket's own refill interval. The context carries the PII-free bucket :bucket type and :limit (capacity) for the plug to classify and surface — never the recipient/sender value.