From d0e72af188625d04c867e0e9a82ade9ee920a383 Mon Sep 17 00:00:00 2001 From: NguyenND Date: Tue, 7 Jul 2026 19:57:46 -0400 Subject: [PATCH] July 7 - Update invitation with Brand Name, based on hostname --- CLAUDE.md | 10 +++- app/routes/customers.py | 13 ++--- app/utils/mail_utils.py | 107 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 123 insertions(+), 7 deletions(-) create mode 100644 app/utils/mail_utils.py diff --git a/CLAUDE.md b/CLAUDE.md index f0fd6ac..c87c578 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -431,6 +431,14 @@ contract_notification_recipients: ### `audit.py` `log_action(action, entity_type, entity_id, entity_label, details)` — call **after** `db.session.commit()`. **This function calls `db.session.commit()` internally.** Calling it before the primary commit will prematurely persist any dirty ORM state in the session. +### `mail_utils.py` +`branded_sender(base_url=None)` — returns a Flask-Mail `(display_name, address)` sender tuple whose identity tracks the current host, used by the customer invite email. Two things vary independently: + +- **Display name** (always applied, zero DNS): per-domain brand from `BRAND_NAMES` (fallback `DEFAULT_BRAND_NAME`), e.g. `"Gov Services QC"`. +- **From address** (gated): the authenticated local part (`jqc.noreply`) with the host's registrable domain — but **only** for the authenticated sender's own domain or a domain listed in `SENDER_AUTHORIZED_DOMAINS`. Every other domain keeps the authenticated `MAIL_DEFAULT_SENDER` address so it still passes SPF/DMARC and delivers. + +So with no DNS work an invite from `jqc.govservicesinc.com` sends `From: "Gov Services QC" ` (branded name, deliverable address). After that domain's SPF `include:` + DKIM are live, add it to `SENDER_AUTHORIZED_DOMAINS` and it upgrades to `` — no code change. Falls back to the bare authenticated sender string for unparseable hosts (localhost, empty). Edit `BRAND_NAMES` / `SENDER_AUTHORIZED_DOMAINS` as brands and DNS come online. See rule 64. + ### `scope.py` `get_customer_scope(user)` — returns `list[int]` facility IDs for customers, `None` for non-customers. `get_inspector_scope(user)` — returns `list[int]` facility IDs for inspectors (empty list = no assignments = no access), `None` for non-inspectors. Derived from `InspectorAssignment` rows → project → active facilities. @@ -1104,7 +1112,7 @@ timeout = 30 | 61 | **Contract→Facility cascade UI pattern: contract selector is UI-only, not a WTForms field** | The "Log New Issue" form (`issues/form.html`) and both filter bars (`issues/list.html`, `inspections/list.html`) use a plain HTML `