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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user