155 lines
6.2 KiB
Python
155 lines
6.2 KiB
Python
"""
|
|
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" <jqc.noreply@ltservicesinc.com>
|
|
(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" <jqc.noreply@govservicesinc.com>
|
|
|
|
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)
|