diff --git a/docs/architecture.md b/docs/architecture.md index ad90456..d719766 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -235,6 +235,7 @@ mps_page_session (raw rows) | [MPS-20](tickets/mps-20.md) | Auction House Mode (eBay-style Bidding) | Complete | | [MPS-21](tickets/mps-21.md) | Make-an-Offer Mode | Complete | | [MPS-22](tickets/mps-22.md) | Kill-Switch Feature Flags — Karaoke + Torrent Off by Default | Complete | +| [MPS-23](tickets/mps-23.md) | Consolidated Transactional Sender Identity + Shop Contact Email | Open | ## Related Docs diff --git a/docs/tickets/mps-23.md b/docs/tickets/mps-23.md new file mode 100644 index 0000000..ba15cc2 --- /dev/null +++ b/docs/tickets/mps-23.md @@ -0,0 +1,81 @@ +# MPS-23 — Consolidated transactional sender identity + shop contact email + +**Status:** Open (DNS prep done; needs a decision on the Reply-To / shop-contact-email approach before code lands) + +## Problem + +MPS sends each shop's transactional mail (OTP login codes, purchase receipts, +sale notifications, gift cards, offers, invites) `From: no-reply@` +— i.e. from the shop's *own* hostname: + +- `my.makepostsell.com` → `no-reply@my.makepostsell.com` +- `.makepostsell.com` → `no-reply@.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" `. + +## 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: ` 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 `._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.