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
|
## 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=""`
|
- 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)`
|
- 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
|
- `copy_crl()` does `chmod 644` after copy
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
import secrets
|
import secrets
|
||||||
import shutil
|
import shutil
|
||||||
|
import smtplib
|
||||||
import string
|
import string
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
@@ -50,7 +51,13 @@ CRYPTGEON_URL = "https://cryptgeon.example.com"
|
|||||||
MAIL_FROM = "vpn-admin@example.com"
|
MAIL_FROM = "vpn-admin@example.com"
|
||||||
MAIL_SUBJECT = "Your VPN Configuration"
|
MAIL_SUBJECT = "Your VPN Configuration"
|
||||||
EMAIL_TEMPLATE_PATH = "./email_template.txt"
|
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_PAST = 30
|
||||||
DAYS_AHEAD = 14
|
DAYS_AHEAD = 14
|
||||||
@@ -388,19 +395,39 @@ def send_email(
|
|||||||
subject: str,
|
subject: str,
|
||||||
template_path: str,
|
template_path: str,
|
||||||
mail_binary: str,
|
mail_binary: str,
|
||||||
|
smtp_host: str = "",
|
||||||
|
smtp_port: int = 587,
|
||||||
|
smtp_user: str = "",
|
||||||
|
smtp_password: str = "",
|
||||||
|
smtp_tls: str = "starttls",
|
||||||
) -> None:
|
) -> 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,
|
msg = build_mime_message(to_address, cn, one_time_url, ovpn_path,
|
||||||
mail_from, subject, template_path)
|
mail_from, subject, template_path)
|
||||||
proc = subprocess.Popen(
|
if smtp_host:
|
||||||
[mail_binary, "-t"],
|
if smtp_tls == "ssl":
|
||||||
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
conn: smtplib.SMTP = smtplib.SMTP_SSL(smtp_host, smtp_port)
|
||||||
)
|
else:
|
||||||
_, err = proc.communicate(input=msg.as_bytes())
|
conn = smtplib.SMTP(smtp_host, smtp_port)
|
||||||
if proc.returncode != 0:
|
if smtp_tls == "starttls":
|
||||||
raise RuntimeError(
|
conn.starttls()
|
||||||
f"mail delivery failed (exit {proc.returncode}): {err.decode().strip()}"
|
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:
|
try:
|
||||||
send_email(form.email, final_cn, one_time_url, ovpn_path,
|
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,
|
self._msg(stdscr,
|
||||||
f"Email sent to {form.email}\nConfig: {ovpn_path}",
|
f"Email sent to {form.email}\nConfig: {ovpn_path}",
|
||||||
wait=True)
|
wait=True)
|
||||||
@@ -1183,7 +1213,10 @@ class CliRunner:
|
|||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
send_email(email_addr, cn, one_time_url, ovpn_path,
|
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)
|
print(f"Email sent to {email_addr}", file=sys.stderr)
|
||||||
except RuntimeError as exc:
|
except RuntimeError as exc:
|
||||||
print(f"warning: email failed: {exc}", file=sys.stderr)
|
print(f"warning: email failed: {exc}", file=sys.stderr)
|
||||||
|
|||||||
Reference in New Issue
Block a user