make_post_sell/docs/tickets/mps-23.md
russell@unturf.com a90979a46c
MPS-23: single warm sending identity for transactional mail
All transactional mail now sends From app.email.sender (default
no-reply@origin.makepostsell.com) instead of per-shop no-reply@<domain>,
with the shop name (or email.from_name) as the display name. The origin
identity is DKIM-signed (d=makepostsell.com) and SPF-authorized and
relays via mx1's warm IP, so operator custom-domain shops stop getting
spam-foldered. format_from_header() builds the From; send_email() gained
a from_name kwarg. Reply-To / per-shop contact email still TODO.
2026-05-12 11:13:11 -04:00

5.1 KiB

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.comno-reply@my.makepostsell.com
  • <shop>.makepostsell.comno-reply@<shop>.makepostsell.com
  • operator custom domain shop.unturf.comno-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.comno-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.