""" 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 (customer invitations, billing notices) shows a per-tenant identity — WITHOUT risking deliverability. Two independent things vary: 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 gov.jqc.app -> From: "Gov Services QC" (branded NAME, deliverable authenticated ADDRESS) Once a tenant's own domain authorizes this mail server (SPF include + DKIM), add it to SENDER_AUTHORIZED_DOMAINS and it upgrades to: From: "Gov Services QC" Ported from the single-tenant tree with one multi-tenant change: the display name comes from the TENANT, not a hardcoded host→name dict. Resolution order: 1. TenantSettings.company_name — what the tenant typed into Branding 2. Tenant.name (control plane) — always known for a resolved tenant 3. BRAND_NAMES[domain] — ST's host map, kept for non-tenant hosts 4. DEFAULT_BRAND_NAME See CLAUDE.md rule 64. """ from urllib.parse import urlparse from flask import current_app, g # ── Per-domain display names (single-tenant / non-tenant hosts only) ───────── # In multi-tenant mode the tenant record supplies the name and this map is never # consulted. It remains for the single-tenant deployment and for hosts that # resolve to no tenant. 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. # NOTE: written as set([...]) deliberately. A brace literal containing only # comments is an empty *dict*, which silently becomes a *set* the moment a line # is uncommented. Membership works either way, so this is cosmetic — but the # declared type shouldn't change based on whether a comment is uncommented. SENDER_AUTHORIZED_DOMAINS = set([ # '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): """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 _tenant_brand_name(): """Display name from the current tenant, or None. Prefers what the tenant typed into Branding (TenantSettings.company_name), then the control-plane tenant name. Returns None in single-tenant mode or when no tenant is bound. Deliberately swallows errors: TenantSettings.query raises ProgrammingError when a freshly provisioned tenant DB has not been migrated yet, and an un-migrated tenant must not break sending mail. """ if not current_app.config.get('MULTI_TENANT_ENABLED'): return None tenant = getattr(g, 'tenant', None) if g else None if tenant is None: return None try: from app.models.tenant_settings import TenantSettings settings = TenantSettings.get_or_default() if settings is not None: name = (settings.company_name or '').strip() if name: return name except Exception: pass return (getattr(tenant, 'name', '') or '').strip() or None def branded_sender(base_url=None): """Return a ``(display_name, address)`` sender tuple for branded email. The display name tracks the tenant (falling back to 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() tenant_name = _tenant_brand_name() domain = _host_domain(base_url) if not domain: # Unknown host → safe authenticated address; still brand the name if a # tenant is bound. return (tenant_name or DEFAULT_BRAND_NAME, auth_sender) display_name = tenant_name or 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)