ai_qc/backend/email_service.py
nickviljoen 125c5e7064 Simplify settings UX and add client access request flow
Settings panel:
- Reference Assets tab: collapse the Brand Name + Tags + Description form to a single Name field; the user-entered name now drives the dropdown label on the main configuration page (falls back to filename for legacy records).
- Media Plan tab: add a Name field. Backend stores display_name on the plan record, and both the active-plan card and the main-page dropdown prefer display_name (falling back to original_filename for old plans).
- Modal footer is now context-aware: Save Profile + Cancel show only on the Profile / Create Profile tabs; Reference Assets / QC Tools / Media Plan show a single green Save button that closes the modal.

Client access request:
- New "Request Client Access" tile on the client picker, alongside the user's existing client tiles. Opens a modal that auto-fills name + email from the MSAL session (read-only), shows checkboxes for clients the user does not already have, and accepts an optional reason.
- New POST /api/access_request endpoint (auth-required) that takes identity from the verified session, validates the requested clients, looks up admin recipients via user_access.list_access_entries, and emails them via the new email_service module (Mailgun SMTP with STARTTLS). Reply-To is set to the requester. Logs an access_request event to the daily JSONL usage logs.
- New GET /api/all_clients endpoint so the form can list clients the requester currently cannot see.
- Mailgun SMTP credentials added to the four env files (and placeholders in the .env.template files) under SMTP_SERVER / SMTP_PORT / SMTP_USER / SMTP_PASSWORD / SENDER_EMAIL / ERROR_EMAIL / REPORT_EMAILS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 14:02:40 +02:00

71 lines
2.1 KiB
Python

#!/usr/bin/env python3
"""
SMTP email service for outbound notifications (access requests, etc).
Reads credentials from env vars loaded by api_server.load_environment_config():
SMTP_SERVER, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SENDER_EMAIL
Usage:
from email_service import send_email
ok, err = send_email(
['admin@example.com'],
subject='Access request',
body='Plain text body',
html_body='<p>HTML body</p>', # optional
)
"""
import os
import smtplib
import ssl
from email.message import EmailMessage
from typing import Iterable, Optional, Tuple
def is_configured() -> bool:
return all(os.environ.get(k) for k in ('SMTP_SERVER', 'SMTP_USER', 'SMTP_PASSWORD', 'SENDER_EMAIL'))
def send_email(
to_addresses: Iterable[str],
subject: str,
body: str,
html_body: Optional[str] = None,
reply_to: Optional[str] = None,
) -> Tuple[bool, Optional[str]]:
"""Send an email via SMTP with STARTTLS. Returns (success, error_message)."""
recipients = [a for a in (to_addresses or []) if a]
if not recipients:
return False, 'No recipients provided'
if not is_configured():
return False, 'SMTP is not configured (missing SMTP_SERVER / SMTP_USER / SMTP_PASSWORD / SENDER_EMAIL)'
server = os.environ['SMTP_SERVER']
port = int(os.environ.get('SMTP_PORT', '587'))
user = os.environ['SMTP_USER']
password = os.environ['SMTP_PASSWORD']
sender = os.environ['SENDER_EMAIL']
msg = EmailMessage()
msg['Subject'] = subject
msg['From'] = sender
msg['To'] = ', '.join(recipients)
if reply_to:
msg['Reply-To'] = reply_to
msg.set_content(body)
if html_body:
msg.add_alternative(html_body, subtype='html')
try:
context = ssl.create_default_context()
with smtplib.SMTP(server, port, timeout=15) as smtp:
smtp.ehlo()
smtp.starttls(context=context)
smtp.ehlo()
smtp.login(user, password)
smtp.send_message(msg)
return True, None
except Exception as e:
print(f'[email_service] send_email failed: {e}')
return False, str(e)