From e0e3170de31d1c4971c13cb5183f4586ed267c81 Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Wed, 29 Apr 2026 21:43:38 -0400 Subject: [PATCH] feat: regex support in email allowlist Each entry in email_allowlist.json is treated as a re.fullmatch pattern (case-insensitive). Allows domain wildcards, plus-addressing, and any variation expressible as a regex. Invalid patterns are logged and skipped. Co-Authored-By: Claude Sonnet 4.6 --- cortex/tools/notify.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/cortex/tools/notify.py b/cortex/tools/notify.py index dfcf916..cb3f565 100644 --- a/cortex/tools/notify.py +++ b/cortex/tools/notify.py @@ -8,6 +8,7 @@ email_send uses the server SMTP config from .env (smtp_server, smtp_from_*). import asyncio import json import logging +import re from config import settings from persona import get_user @@ -15,11 +16,11 @@ from persona import get_user logger = logging.getLogger(__name__) -def _email_allowlist(username: str) -> list[str]: +def _load_allowlist(username: str) -> list[str]: """Load the per-user email allowlist. Returns empty list if not configured.""" path = settings.home_root() / username / "email_allowlist.json" try: - return [a.lower().strip() for a in json.loads(path.read_text())] + return [str(p).strip() for p in json.loads(path.read_text()) if str(p).strip()] except FileNotFoundError: return [] except Exception as e: @@ -27,18 +28,30 @@ def _email_allowlist(username: str) -> list[str]: return [] +def _email_allowed(address: str, patterns: list[str]) -> bool: + """Return True if address matches any pattern (regex, case-insensitive full match).""" + addr = address.strip() + for pattern in patterns: + try: + if re.fullmatch(pattern, addr, re.IGNORECASE): + return True + except re.error: + logger.warning("invalid regex in email allowlist: %r", pattern) + return False + + async def email_send(to: str, subject: str, body: str) -> str: """Send an email via the server's configured SMTP account.""" username = get_user() - allowlist = _email_allowlist(username) + allowlist = _load_allowlist(username) if not allowlist: return ( "Email blocked — no allowlist configured. " - f"Add allowed addresses to home/{username}/email_allowlist.json as a JSON array." + f"Add allowed patterns to home/{username}/email_allowlist.json as a JSON array." ) - if to.lower().strip() not in allowlist: - return f"Email blocked — {to} is not in the allowlist for {username}." + if not _email_allowed(to, allowlist): + return f"Email blocked — {to} does not match any allowed pattern for {username}." from email_utils import send_email ok = await asyncio.to_thread(