- email_utils.py: send_email() via smtplib.SMTP_SSL (port 465, same server
as AE API); send_invite_email() renders plain-text + HTML invite template
- config.py: smtp_server, smtp_port, smtp_username, smtp_password,
smtp_from_email, smtp_from_name, cortex_base_url settings
- manage_passwords.py:
- profile.json helpers (get/set email stored in home/{username}/profile.json)
- invite command now accepts optional email arg, sends invite automatically;
falls back to stored email; prints link either way
- new 'email' command to store/update a user's email address
- 'list' command now shows email alongside password status
- .env.default: SMTP_* and CORTEX_BASE_URL documented
Usage after adding SMTP_PASSWORD to .env:
python manage_passwords.py invite holly holly@example.com
→ generates token, stores email, sends invite, prints link as fallback
All 80 tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
108 lines
2.9 KiB
Python
108 lines
2.9 KiB
Python
"""
|
|
Email utilities for Cortex — invite links and future notifications.
|
|
|
|
Uses smtplib.SMTP_SSL (port 465). Both plain-text and HTML bodies are sent.
|
|
SMTP credentials come from config.settings (set in .env).
|
|
"""
|
|
|
|
import logging
|
|
import smtplib
|
|
import ssl
|
|
from email.headerregistry import Address
|
|
from email.message import EmailMessage
|
|
|
|
from config import settings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def send_email(
|
|
to_email: str,
|
|
subject: str,
|
|
body_html: str,
|
|
body_text: str,
|
|
to_name: str = "",
|
|
) -> bool:
|
|
"""
|
|
Send an email via SMTP_SSL.
|
|
|
|
Returns True on success, False on any failure.
|
|
Logs errors but never raises — callers can check the return value.
|
|
"""
|
|
if not settings.smtp_server:
|
|
logger.error("SMTP not configured (SMTP_SERVER is empty)")
|
|
return False
|
|
|
|
msg = EmailMessage()
|
|
msg["Subject"] = subject
|
|
msg["From"] = Address(
|
|
display_name=settings.smtp_from_name,
|
|
addr_spec=settings.smtp_from_email,
|
|
)
|
|
msg["To"] = Address(
|
|
display_name=to_name or to_email,
|
|
addr_spec=to_email,
|
|
)
|
|
|
|
msg.set_content(body_text)
|
|
msg.add_alternative(f"<html><body>{body_html}</body></html>", subtype="html")
|
|
|
|
logger.info("sending email to %s — %s", to_email, subject)
|
|
try:
|
|
ctx = ssl.create_default_context()
|
|
with smtplib.SMTP_SSL(settings.smtp_server, settings.smtp_port, context=ctx) as server:
|
|
if settings.smtp_username and settings.smtp_password:
|
|
server.login(settings.smtp_username, settings.smtp_password)
|
|
server.send_message(msg)
|
|
logger.info("email sent to %s", to_email)
|
|
return True
|
|
except Exception as e:
|
|
logger.error("failed to send email to %s: %s", to_email, e)
|
|
return False
|
|
|
|
|
|
def send_invite_email(to_email: str, username: str, token: str, to_name: str = "") -> bool:
|
|
"""Send a Cortex invite link to a new user."""
|
|
url = f"{settings.cortex_base_url}/setup/{token}"
|
|
|
|
body_text = f"""\
|
|
You've been invited to Cortex.
|
|
|
|
Click the link below to set your password and create your persona:
|
|
{url}
|
|
|
|
This link expires in 72 hours and can only be used once.
|
|
|
|
— Cortex
|
|
"""
|
|
|
|
body_html = f"""\
|
|
<p>You've been invited to <strong>Cortex</strong>.</p>
|
|
<p>Click the link below to set your password and create your persona:</p>
|
|
<p><a href="{url}" style="
|
|
display:inline-block;
|
|
padding:10px 20px;
|
|
background:#7c3aed;
|
|
color:#fff;
|
|
text-decoration:none;
|
|
border-radius:6px;
|
|
font-family:sans-serif;
|
|
font-size:15px;
|
|
">Set up my account →</a></p>
|
|
<p style="font-size:13px;color:#666;">
|
|
Or copy this link:<br>
|
|
<code>{url}</code>
|
|
</p>
|
|
<p style="font-size:12px;color:#999;">
|
|
This link expires in 72 hours and can only be used once.
|
|
</p>
|
|
"""
|
|
|
|
return send_email(
|
|
to_email=to_email,
|
|
subject="You've been invited to Cortex",
|
|
body_html=body_html,
|
|
body_text=body_text,
|
|
to_name=to_name or username,
|
|
)
|