diff --git a/CLAUDE.md b/CLAUDE.md index 67ae77c..a9e5f43 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -78,6 +78,7 @@ python3 -m pytest tests/test_pki.py::test_sorted_ascending -v # single test ## Key constraints +- Email: set `SMTP_HOST` to use smtplib (SMTP_TLS: `"starttls"`/`"ssl"`/`""`); leave empty to use `MAIL_BINARY`. Auth skipped when `SMTP_USER=""` - EasyRSA called with `--batch`; `--passin=pass:` omitted when `CA_PASSPHRASE=""` - Cryptgeon: `raw_key=os.urandom(32)`, `aes_key=SHA-256(raw_key)`, AES-256-GCM, URL fragment=`base64url(raw_key)` - `copy_crl()` does `chmod 644` after copy diff --git a/openvpncertupdate.py b/openvpncertupdate.py index 6c453f8..2d72a2d 100644 --- a/openvpncertupdate.py +++ b/openvpncertupdate.py @@ -15,6 +15,7 @@ import json import os import secrets import shutil +import smtplib import string import subprocess import sys @@ -50,7 +51,13 @@ CRYPTGEON_URL = "https://cryptgeon.example.com" MAIL_FROM = "vpn-admin@example.com" MAIL_SUBJECT = "Your VPN Configuration" EMAIL_TEMPLATE_PATH = "./email_template.txt" -MAIL_BINARY = "msmtp" # or "sendmail" +MAIL_BINARY = "msmtp" # or "sendmail"; ignored when SMTP_HOST is set + +SMTP_HOST = "" # e.g. "smtp.example.com"; set to use smtplib instead of MAIL_BINARY +SMTP_PORT = 587 # 587=STARTTLS 465=SSL 25=plain +SMTP_USER = "" # leave empty to skip authentication +SMTP_PASSWORD = "" +SMTP_TLS = "starttls" # "starttls" | "ssl" | "" (plain) DAYS_PAST = 30 DAYS_AHEAD = 14 @@ -388,19 +395,39 @@ def send_email( subject: str, template_path: str, mail_binary: str, + smtp_host: str = "", + smtp_port: int = 587, + smtp_user: str = "", + smtp_password: str = "", + smtp_tls: str = "starttls", ) -> None: - """Compose and send email with .ovpn attachment via msmtp/sendmail.""" + """Compose and send email with .ovpn attachment. + + Uses smtplib when smtp_host is set; falls back to mail_binary otherwise. + """ msg = build_mime_message(to_address, cn, one_time_url, ovpn_path, mail_from, subject, template_path) - proc = subprocess.Popen( - [mail_binary, "-t"], - stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - ) - _, err = proc.communicate(input=msg.as_bytes()) - if proc.returncode != 0: - raise RuntimeError( - f"mail delivery failed (exit {proc.returncode}): {err.decode().strip()}" + if smtp_host: + if smtp_tls == "ssl": + conn: smtplib.SMTP = smtplib.SMTP_SSL(smtp_host, smtp_port) + else: + conn = smtplib.SMTP(smtp_host, smtp_port) + if smtp_tls == "starttls": + conn.starttls() + with conn: + if smtp_user: + conn.login(smtp_user, smtp_password) + conn.sendmail(mail_from, [to_address], msg.as_bytes()) + else: + proc = subprocess.Popen( + [mail_binary, "-t"], + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) + _, err = proc.communicate(input=msg.as_bytes()) + if proc.returncode != 0: + raise RuntimeError( + f"mail delivery failed (exit {proc.returncode}): {err.decode().strip()}" + ) # ============================================================ @@ -986,7 +1013,10 @@ class CursesApp: ): try: send_email(form.email, final_cn, one_time_url, ovpn_path, - MAIL_FROM, MAIL_SUBJECT, EMAIL_TEMPLATE_PATH, MAIL_BINARY) + MAIL_FROM, MAIL_SUBJECT, EMAIL_TEMPLATE_PATH, MAIL_BINARY, + smtp_host=SMTP_HOST, smtp_port=SMTP_PORT, + smtp_user=SMTP_USER, smtp_password=SMTP_PASSWORD, + smtp_tls=SMTP_TLS) self._msg(stdscr, f"Email sent to {form.email}\nConfig: {ovpn_path}", wait=True) @@ -1183,7 +1213,10 @@ class CliRunner: else: try: send_email(email_addr, cn, one_time_url, ovpn_path, - MAIL_FROM, MAIL_SUBJECT, EMAIL_TEMPLATE_PATH, MAIL_BINARY) + MAIL_FROM, MAIL_SUBJECT, EMAIL_TEMPLATE_PATH, MAIL_BINARY, + smtp_host=SMTP_HOST, smtp_port=SMTP_PORT, + smtp_user=SMTP_USER, smtp_password=SMTP_PASSWORD, + smtp_tls=SMTP_TLS) print(f"Email sent to {email_addr}", file=sys.stderr) except RuntimeError as exc: print(f"warning: email failed: {exc}", file=sys.stderr)