feat: add smtplib email delivery as alternative to MAIL_BINARY

Add SMTP_HOST/PORT/USER/PASSWORD/TLS settings. When SMTP_HOST is set,
send_email() uses smtplib (supports STARTTLS, SSL, or plain); auth is
skipped when SMTP_USER is empty. Falls back to MAIL_BINARY when SMTP_HOST
is unset, preserving existing behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vlad Doloman
2026-06-24 16:04:12 +03:00
parent f3e1471d1a
commit 9f339e9254
2 changed files with 46 additions and 12 deletions

View File

@@ -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:<CA_PASSPHRASE>` 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

View File

@@ -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)