feat: MIME email composer with .ovpn attachment via msmtp/sendmail

Add send_email() function that composes multipart MIME messages with
template-based body and .ovpn file attachment, piped to mail binary via
subprocess.Popen. Supports Cryptgeon URL insertion and sender metadata.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vlad Doloman
2026-06-23 23:58:00 +03:00
parent 5b9ee8d448
commit a2593b644e
3 changed files with 102 additions and 0 deletions

View File

@@ -309,6 +309,50 @@ def create_note(content: str, base_url: str) -> str:
return f"{base_url.rstrip('/')}/#/note/{note_id}/{key_fragment}"
# ============================================================
# === MAILER ===
# ============================================================
def send_email(
to_address: str,
cn: str,
one_time_url: str,
ovpn_path: str,
mail_from: str,
subject: str,
template_path: str,
mail_binary: str,
) -> None:
"""Compose and send email with .ovpn attachment via msmtp/sendmail."""
config_name = os.path.basename(ovpn_path)
body = Path(template_path).read_text().format(
cn=cn, url=one_time_url, config_name=config_name,
)
msg = email.mime.multipart.MIMEMultipart()
msg["From"] = mail_from
msg["To"] = to_address
msg["Subject"] = subject
msg.attach(email.mime.text.MIMEText(body, "plain"))
with open(ovpn_path, "rb") as fh:
part = email.mime.base.MIMEBase("application", "octet-stream")
part.set_payload(fh.read())
email.encoders.encode_base64(part)
part.add_header("Content-Disposition", f'attachment; filename="{config_name}"')
msg.attach(part)
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()}"
)
# (remaining sections added in later tasks)
# ============================================================