""" Notification tools — proactively send messages to user channels. nc_talk_send routes through notification.py → channels.json. email_send uses the server SMTP config from .env (smtp_server, smtp_from_*). """ import asyncio import json import logging import re from google.genai import types from config import settings from persona import get_user logger = logging.getLogger(__name__) 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 [str(p).strip() for p in json.loads(path.read_text()) if str(p).strip()] except FileNotFoundError: return [] except Exception as e: logger.warning("failed to read email_allowlist.json for %s: %s", username, e) 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 = _load_allowlist(username) if not allowlist: return ( "Email blocked — no allowlist configured. " f"Add allowed patterns to home/{username}/email_allowlist.json as a JSON array." ) 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( send_email, to_email=to, subject=subject, body_text=body, body_html=body.replace("\n", "
"), ) if ok: return f"Email sent to {to}." return "Failed to send email — check SMTP configuration in .env." async def web_push(title: str, body: str, url: str = "") -> str: """Send a browser push notification to the current user's registered devices.""" import push_utils username = get_user() result = await push_utils.send_push(username, title, body, url) if "error" in result: return f"Push failed: {result['error']}" return f"Push sent to {result['sent']} device(s) for {username} (pruned {result['pruned']} stale)." async def nc_talk_send(message: str) -> str: """Send a message to the user via their configured notification channel. Channel is resolved from the user's channels.json (notification_channel key). Falls back to Nextcloud Talk if configured. No-op if no channel is set. """ from notification import notify username = get_user() try: await notify(username, message) return f"Message sent to {username}'s notification channel." except Exception as e: logger.warning("nc_talk_send error for %s: %s", username, e) return f"Failed to send notification: {e}" DECLARATIONS = [ types.FunctionDeclaration( name="web_push", description=( "Send a browser push notification to the current user. Works even when the " "Cortex tab is not open. Use for completing long tasks, reminders that fire " "in the background, or anything the user should see immediately. " "url is optional — if set, clicking the notification opens that URL." ), parameters=types.Schema( type=types.Type.OBJECT, properties={ "title": types.Schema(type=types.Type.STRING, description="Notification title (short)"), "body": types.Schema(type=types.Type.STRING, description="Notification body text"), "url": types.Schema(type=types.Type.STRING, description="Optional URL to open on click"), }, required=["title", "body"], ), ), types.FunctionDeclaration( name="email_send", description=( "Send an email from the server's configured SMTP account. Use for delivering " "summaries, reports, reminders, or any content the user wants emailed. " "body is plain text; newlines are preserved." ), parameters=types.Schema( type=types.Type.OBJECT, properties={ "to": types.Schema(type=types.Type.STRING, description="Recipient email address"), "subject": types.Schema(type=types.Type.STRING, description="Email subject line"), "body": types.Schema(type=types.Type.STRING, description="Plain-text email body"), }, required=["to", "subject", "body"], ), ), types.FunctionDeclaration( name="nc_talk_send", description=( "Send a proactive message to the user via their configured notification channel " "(Nextcloud Talk by default). Use this to notify the user of completed background " "tasks, important events, or anything they should know between sessions. " "Requires notification_channel and notification_room set in channels.json." ), parameters=types.Schema( type=types.Type.OBJECT, properties={ "message": types.Schema(type=types.Type.STRING, description="The message to send to the user"), }, required=["message"], ), ), ]