# `Mailglass.Suppression.Entry`
[🔗](https://github.com/szTheory/mailglass/blob/v0.1.0/lib/mailglass/suppression/entry.ex#L1)

Ecto schema for a row in `mailglass_suppressions`.

## Atom Sets (D-07, D-10, D-11)

- `:scope` — `:address | :domain | :address_stream`. NO default;
  changeset `validate_required` enforces (MAIL-07 prevention).
- `:stream` — `:transactional | :operational | :bulk` (nullable;
  populated only when `scope = :address_stream`).
- `:reason` — `:hard_bounce | :complaint | :unsubscribe | :manual |
  :policy | :invalid_recipient`.

## Coupling invariants (D-07)

- `scope = :address_stream` REQUIRES `stream`.
- `scope IN (:address, :domain)` REJECTS `stream`.

Enforced both at the changeset layer (`validate_scope_stream_coupling/1`)
and at the DB layer (`mailglass_suppressions_stream_scope_check`
CHECK constraint — Plan 02). Belt-and-suspenders: either layer alone
would suffice, but lint-time errors (changeset) are cheaper than
runtime errors (Postgres).

## Address normalization

The column is `CITEXT`, so the DB compares case-insensitively.
Additionally, `downcase_address/1` lowercases at cast time — defense
in depth, and makes "did I downcase?" questions moot for reads that
bypass citext (e.g. analytics exports).

# `t`

```elixir
@type t() :: %Mailglass.Suppression.Entry{
  __meta__: term(),
  address: String.t() | nil,
  expires_at: DateTime.t() | nil,
  id: Ecto.UUID.t() | nil,
  inserted_at: DateTime.t() | nil,
  metadata: map(),
  reason: atom() | nil,
  scope: :address | :domain | :address_stream | nil,
  source: String.t() | nil,
  stream: :transactional | :operational | :bulk | nil,
  tenant_id: String.t() | nil
}
```

# `__reasons__`
*since 0.1.0* 

```elixir
@spec __reasons__() :: [atom()]
```

Closed reason atom set.

# `__scopes__`
*since 0.1.0* 

```elixir
@spec __scopes__() :: [atom()]
```

Closed scope atom set.

# `__streams__`
*since 0.1.0* 

```elixir
@spec __streams__() :: [atom()]
```

Closed stream atom set.

# `changeset`
*since 0.1.0* 

```elixir
@spec changeset(map()) :: Ecto.Changeset.t()
```

Builds a changeset for a new `%Entry{}` from an attr map.

Enforces three invariants at the Elixir layer:

1. `:scope` is required with no default (MAIL-07 prevention — D-11).
2. Scope/stream coupling (D-07) via `validate_scope_stream_coupling/1`.
3. Address normalization via `downcase_address/1` — belt-and-suspenders
   with the underlying `CITEXT` column.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
