July 7 - Update invitation with Brand Name, based on hostname

This commit is contained in:
2026-07-07 19:57:46 -04:00
parent a10e314854
commit d0e72af188
3 changed files with 123 additions and 7 deletions
+7 -6
View File
@@ -188,12 +188,13 @@ def _send_invite_email(user, token, base_url=None):
effective_base = (base_url or current_app.config.get('APP_BASE_URL', '')).rstrip('/')
setup_link = f'{effective_base}{url_for("customers.set_password", token=token)}'
# From MUST be the authenticated SMTP identity (MAIL_DEFAULT_SENDER); a
# per-host noreply@<domain> is accepted by the relay but dropped downstream
# by SPF/DMARC. The per-domain host is still preserved in setup_link above.
sender = (current_app.config.get('MAIL_DEFAULT_SENDER')
or current_app.config.get('MAIL_USERNAME')
or 'noreply@janitorialqc.local')
# Per-domain branded From as a (display_name, address) tuple. The display
# NAME always tracks the host (e.g. "Gov Services QC"); the ADDRESS is
# branded only for DNS-authorized domains and otherwise stays the
# authenticated identity so the mail always delivers. See
# app/utils/mail_utils.py and CLAUDE.md rule 64.
from app.utils.mail_utils import branded_sender
sender = branded_sender(effective_base)
html_body = render_template_string("""<!DOCTYPE html>
<html>
+107
View File
@@ -0,0 +1,107 @@
"""
app/utils/mail_utils.py
-----------------------
Helpers for constructing outbound email sender identities.
`branded_sender()` returns a Flask-Mail sender as a ``(display_name, address)``
tuple so outbound branded email (currently the customer invitation) shows a
per-domain identity that tracks the host the user is on — WITHOUT risking
deliverability.
Two independent things vary by domain:
1. DISPLAY NAME (what the recipient sees, e.g. "Gov Services QC")
Always applied. Costs nothing, needs no DNS. This is the safe default.
2. FROM ADDRESS (the actual @domain, e.g. jqc.noreply@govservicesinc.com)
Only used for domains that are KNOWN to authorize this mail server
(the authenticated sender's own domain, plus anything you add to
SENDER_AUTHORIZED_DOMAINS). Every other domain keeps the authenticated
address so the message still passes SPF/DMARC and delivers.
Result, with no DNS work:
Invited from jqc.govservicesinc.com ->
From: "Gov Services QC" <jqc.noreply@ltservicesinc.com>
(branded NAME, deliverable authenticated ADDRESS)
Once govservicesinc.com's DNS authorizes this mail server (SPF include + DKIM),
add it to SENDER_AUTHORIZED_DOMAINS and it upgrades to:
From: "Gov Services QC" <jqc.noreply@govservicesinc.com>
See CLAUDE.md rule 64.
"""
from urllib.parse import urlparse
from flask import current_app
# ── Per-domain display names (edit as new brands launch) ─────────────────────
# Keyed by registrable domain. A domain not listed here falls back to
# DEFAULT_BRAND_NAME so the From never renders an ugly auto-derived label.
BRAND_NAMES = {
'ltservicesinc.com': 'LT Services QC',
'govservicesinc.com': 'Gov Services QC',
'efs.com': 'EFS QC',
}
DEFAULT_BRAND_NAME = 'Janitorial QC'
# ── Domains cleared to use a BRANDED FROM ADDRESS ────────────────────────────
# Add a registrable domain here ONLY after its DNS authorizes this mail server
# (SPF `include:` + DKIM). Until then the domain still gets its branded display
# NAME but sends from the authenticated address, so it always delivers.
# The authenticated sender's own domain is always treated as authorized and does
# NOT need to be listed here.
SENDER_AUTHORIZED_DOMAINS = {
# 'govservicesinc.com', # ← uncomment once SPF+DKIM are live for it
# 'efs.com',
}
def _authenticated_sender() -> str:
"""The SMTP identity we authenticate as — always deliverable."""
return (current_app.config.get('MAIL_DEFAULT_SENDER')
or current_app.config.get('MAIL_USERNAME')
or 'jqc.noreply@janitorialqc.local')
def _host_domain(base_url: str | None) -> str | None:
"""Registrable domain (last two labels) of base_url, or None if unparseable."""
base = (base_url or current_app.config.get('APP_BASE_URL', '') or '').strip().rstrip('/')
if not base:
return None
netloc = urlparse(base).netloc or urlparse('//' + base).netloc
host = netloc.split('@')[-1].split(':')[0].strip().lower() # drop userinfo/port
if not host or '.' not in host:
return None
labels = [l for l in host.split('.') if l]
return '.'.join(labels[-2:]) if len(labels) >= 2 else host
def branded_sender(base_url: str | None = None):
"""Return a ``(display_name, address)`` sender tuple for branded email.
The display name always tracks the host's domain; the address is branded
only for the authenticated domain and any SENDER_AUTHORIZED_DOMAINS, and
otherwise stays the authenticated address so the mail still delivers.
Falls back to the bare authenticated sender string whenever the host cannot
be parsed (localhost, empty, no dot), so this can never yield an invalid From.
"""
auth_sender = _authenticated_sender()
if '@' not in auth_sender:
return auth_sender
local, auth_domain = auth_sender.rsplit('@', 1)
auth_domain = auth_domain.lower()
domain = _host_domain(base_url)
if not domain:
# Unknown host → safe authenticated address, generic brand name.
return (DEFAULT_BRAND_NAME, auth_sender)
display_name = BRAND_NAMES.get(domain, DEFAULT_BRAND_NAME)
if domain == auth_domain or domain in SENDER_AUTHORIZED_DOMAINS:
address = f'{local}@{domain}' # branded address — DNS-authorized
else:
address = auth_sender # keep deliverable authenticated address
return (display_name, address)