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>
71 lines
2.1 KiB
Python
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)
|