# MPS-23 — Consolidated transactional sender identity + shop contact email

**Status:** Core shipped (2026-05-12) — single sending identity + shop display
name + DNS are live. **Remaining:** the Reply-To / shop-contact-email decision
(see "Open question" below), and the optional `d=origin.makepostsell.com` DKIM
key for author-domain-exact alignment.

## Shipped
- `app.email.sender` defaults to `no-reply@origin.makepostsell.com` (overridable
  via `MPS_EMAIL_SENDER`); `app.email.from_name` defaults to `Make Post Sell`.
- `lib/mail.py`: `format_from_header()` helper; `send_pyramid_email()` sets the
  From display name to `request.shop.name` when in shop context, else
  `email.from_name`. `send_email()` gained a `from_name` kwarg.
- DNS: `origin.makepostsell.com TXT "v=spf1 a a:mx1.foxhop.net -all"` (in
  `proxy.unturf.com/ingress/pdns-init.sh`). opendkim on the origin already signs
  `*@*.makepostsell.com` with `d=makepostsell.com`, relayed via mx1's warm IP.
- Tests: `TestMailFromHeader` in `test_models.py`.

## Problem

MPS sends each shop's transactional mail (OTP login codes, purchase receipts,
sale notifications, gift cards, offers, invites) `From: no-reply@<request.domain>`
— i.e. from the shop's *own* hostname:

- `my.makepostsell.com` → `no-reply@my.makepostsell.com`
- `<shop>.makepostsell.com` → `no-reply@<shop>.makepostsell.com`
- operator custom domain `shop.unturf.com` → `no-reply@shop.unturf.com`

For operator custom domains, MPS controls no DKIM key for that domain and the
domain's SPF doesn't authorize MPS's sending infrastructure, so the mail is
**unauthenticated** (no DKIM, no SPF pass) → Gmail spam-folders it. Even
`no-reply@my.makepostsell.com` lands in spam on first contact because
`makepostsell.com` is a cold direct-signing identity (its mail historically
went through Mailgun's `mg.makepostsell.com`, never `makepostsell.com` itself).

Background: 2026-05-12 mail-infra work moved both origin boxes
(`origin.makepostsell.com`, `origin.remarkbox.com`) to DKIM-sign at origin and
relay through `mx1.foxhop.net` (warm IP). `my.makepostsell.com` / `*.makepostsell.com`
/ `remarkbox.com` mail now passes SPF+DKIM+DMARC. Operator-custom-domain shops
are the remaining gap, and even the makepostsell ones benefit from one
consistent warmed identity.

## Fix

Send **all** MPS transactional mail from one warm, authenticated identity:
`no-reply@origin.makepostsell.com`.

- opendkim on `origin.makepostsell.com` already signs `*@*.makepostsell.com`
  with `d=makepostsell.com` → `no-reply@origin.makepostsell.com` is signed
  (DMARC-aligned at the `makepostsell.com` org-domain level under relaxed mode).
- `origin.makepostsell.com` SPF record (added in `proxy.unturf.com/ingress/pdns-init.sh`):
  `v=spf1 a a:mx1.foxhop.net -all` → authorizes the origin box + the mx1 relay.
- Result: every shop's mail → DKIM-signed, SPF-pass, DMARC-pass, delivered
  from mx1's warm IP — independent of which shop/domain it's for.

Recipient clarity: put the shop name in the display name —
`From: "Acme Shop" <no-reply@origin.makepostsell.com>`.

## Open question — Reply-To / customer support

Shops have no contact email today (`Shop` model has no email/contact column).
If a customer replies to an OTP/receipt it hits `no-reply@…` and vanishes.
Options:

1. **Add `Shop.support_email`** (nullable) + a field in `shop-settings`; set
   `Reply-To: <support_email>` when the operator has filled it in. Opt-in,
   privacy-safe. *(recommended)*
2. Catch-all `reply@origin.makepostsell.com` → forward to the shop owner's
   account email. Requires inbound mail handling + a mapping table.
3. Default `Reply-To` to the shop owner's user email. Simple, but leaks the
   owner's personal address.

Until decided: ship without `Reply-To` (honest — it's `no-reply@`).

## Implementation

- **Config:** `app.email.sender = no-reply@origin.makepostsell.com` in
  `data/development.ini` + the prod pillar (`foxhop-pillar/uwsgi/makepostsell/init.sls`).
  `lib/mail.py:send_pyramid_email` already reads `request.app.get("email.sender", …)`.
- **`lib/mail.py`:** extract `_build_from_header(request, sender_email)` →
  `email.utils.formataddr((display_name, sender_email))`, `display_name` =
  `request.shop.name` when a shop is in context, else the site name. Add
  `Reply-To` per the chosen option. Use the helper in `send_email`/`send_pyramid_email`.
- **DNS (done):** `origin.makepostsell.com|TXT|v=spf1 a a:mx1.foxhop.net -all`.
- **Optional hardening:** dedicated DKIM key for `origin.makepostsell.com`
  (publish `<sel>._domainkey.origin.makepostsell.com`, add to opendkim) so the
  signature is `d=origin.makepostsell.com` — author-domain-exact rather than
  org-aligned. Marginal deliverability gain; not required.
- If option (1): `Shop.support_email` column + Alembic migration (`server_default ''`)
  + `shop-settings` form field + validation + the usual three-layer tests.
- **Tests:** unit (`_build_from_header`, `Shop.support_email`), integration
  (shop settings save), functional (settings POST, OTP flow still works).
- **Docs:** `docs/architecture.md` ticket index, `docs/design-system.md` if a
  new form field, `CLAUDE.md` note, portal `pricing.html`/`index.html` if
  surfaced to users.
