Files
br 5fff17531d
Formeleon Deployment Pipeline / Deploy to Dev Environment (push) Successful in 1m1s
Formeleon Deployment Pipeline / Deploy to Production Environment (push) Skipped
i18 drin
mail drin
fehlende routen drin
prod-reife erreicht.
TODOS:

Deine aktuellen offenen TODOs
1. Rate-Limiting (Spam-Schutz VOR dem reCAPTCHA)
Das reCAPTCHA hält zwar Bots ab, aber wenn jemand böswillig ein Script schreibt, das 1.000 POST-Requests pro Sekunde an /api/v1/submissions sendet, muss dein Backend jedes Mal Google kontaktieren und die Datenbank befragen. Das frisst Server-Ressourcen.

Todo: Einbinden von Flask-Limiter und Absichern der API-Route (z.B. max. 10 Submissions pro Minute pro IP-Adresse oder Tenant).
2. Billing & Feature-Enforcement (Limits pro Tenant durchsetzen)
Die Mandanten-Architektur und das config-Feld im JSON existieren bereits. Aber wir erzwingen diese Limits aktuell nicht im Backend.

Todo: Im Dashboard (templates/admin/tenants/view.html) UI-Controls einbauen, um Feature-Flags für einen Tenant festzulegen:
max_forms (z.B. 1 für Free-User, 10 für Pro-User).
allow_instant_mail (Dürfen E-Mails an den Tenant gesendet werden?).
allow_autoresponder (Darf der Autoresponder für Endkunden genutzt werden?).
Todo: Diese Limits hart im Backend (beim Erstellen von Forms und beim Mailversand) kontrollieren. (Der Mailer prüft das zwar schon halbwegs, das UI zum Pflegen fehlt aber noch komplett).
3. E-Mail Templates aufwerten (HTML statt Plain-Text)
Bisher versendet der Service E-Mails als reinen Text (plain). Für eine professionelle SaaS-Lösung sollten diese Mails ansprechend aussehen.

Todo: Im services/mailer.py den MIMEMultipart so erweitern, dass er saubere HTML-Mails aus einem Jinja2-Template (templates/emails/...) generiert, inklusive Branding/Logo deines Services.
4. Frontend-Snippet "Graceful Degradation"
Das embed.js arbeitet aktuell sehr gut. Aber was ist, wenn das Formeleon-Backend mal für 5 Minuten down ist?

Todo: Einen sauberen Timeout im fetch einbauen und dem Endkunden auf der Webseite eine schöne, fallback HTML-Meldung anzeigen ("Entschuldigung, das Formular ist momentan nicht erreichbar"), anstatt dass sich das Script lautlos in der Console mit einem CORS/Network-Error verabschiedet.
5. CI/CD Pipeline (Deployment)
Wenn der Code fertig ist, willst du ihn sicher nicht immer händisch per git pull auf deinen Produktionsserver schieben und dort docker compose up tippen.

Todo: Eine simple Pipeline (Github Actions / Gitea Actions) anlegen, die den Code testet und automatisch auf dem Zielserver ausrollt.
2026-09-29 05:16:01 +02:00

126 lines
5.0 KiB
Python

# ==============================================================================
# Formeleon - Multi-tenant Form Generator
# Author: Björn Runden <br@runden.net>
# Module: services/mailer.py
# Description: Service for handling email notifications and autoresponders.
# ==============================================================================
import smtplib
import logging
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from typing import List, Dict, Any, Optional
from flask import current_app
from jinja2 import Template, TemplateError
logger = logging.getLogger(__name__)
class MailerService:
"""Service class for handling email dispatch."""
@classmethod
def is_configured(cls) -> bool:
"""Check if SMTP settings are provided in configuration."""
server = current_app.config.get('SMTP_SERVER')
# Wir betrachten den Service als "konfiguriert", wenn entweder ein Server da ist,
# oder wir im Dev-Mode sind (um das Terminal-Logging zu erlauben)
if current_app.config.get('DEV_MODE', False) and not server:
return True
port = current_app.config.get('SMTP_PORT')
return bool(server and port)
@classmethod
def send_email(cls, to_addresses: List[str], subject: str, body_text: str, reply_to: Optional[str] = None) -> bool:
"""
Sends an email using the configured SMTP server.
:param to_addresses: List of recipient email addresses
:param subject: Email subject line
:param body_text: Plain text email body
:param reply_to: Optional Reply-To address
:return: True if successful, False otherwise
"""
if not cls.is_configured():
logger.warning("MailerService: Attempted to send email, but SMTP is not configured.")
return False
if current_app.config.get('DEV_MODE', False) and not current_app.config.get('SMTP_SERVER'):
logger.info(f"MailerService [DEV MODE]: Would send email to {to_addresses} | Subject: '{subject}'")
return True
smtp_server = current_app.config.get('SMTP_SERVER')
smtp_port = int(current_app.config.get('SMTP_PORT', 587))
smtp_user = current_app.config.get('SMTP_USERNAME')
smtp_pass = current_app.config.get('SMTP_PASSWORD')
use_tls = str(current_app.config.get('SMTP_USE_TLS', 'true')).lower() == 'true'
from_address = current_app.config.get('EMAIL_FROM', 'formeleon@localhost')
msg = MIMEMultipart()
msg['From'] = from_address
msg['To'] = ", ".join(to_addresses)
msg['Subject'] = subject
if reply_to:
msg['Reply-To'] = reply_to
msg.attach(MIMEText(body_text, 'plain', 'utf-8'))
try:
with smtplib.SMTP(smtp_server, smtp_port) as server:
if use_tls:
server.starttls()
if smtp_user and smtp_pass:
server.login(smtp_user, smtp_pass)
server.send_message(msg)
logger.info(f"Successfully sent email to {to_addresses}")
return True
except Exception as e:
logger.error(f"Failed to send email to {to_addresses}: {str(e)}", exc_info=True)
return False
@classmethod
def render_template(cls, template_string: str, payload: Dict[str, Any]) -> str:
"""
Renders a Jinja2 template string using the submission payload.
:param template_string: The template string (e.g., "Hello {{ name }}")
:param payload: Dictionary containing the form submission data
:return: Rendered string
"""
try:
template = Template(template_string)
return template.render(**payload)
except TemplateError as e:
logger.error(f"Template rendering failed: {str(e)}")
# Fallback: Return original string if rendering fails
return template_string
@classmethod
def send_admin_notification(cls, form_name: str, to_addresses: List[str], payload: Dict[str, Any]) -> bool:
"""
Sends an alert to the tenant admins about a new submission.
"""
subject = f"New Submission: {form_name}"
# Build a simple readable key-value body
body_lines = [f"A new submission was received for the form '{form_name}'.\n", "-" * 40]
for key, value in payload.items():
body_lines.append(f"{key}: {value}")
body_lines.append("-" * 40)
body_text = "\n".join(body_lines)
return cls.send_email(to_addresses, subject, body_text)
@classmethod
def send_autoresponder(cls, to_address: str, subject_template: str, body_template: str, payload: Dict[str, Any]) -> bool:
"""
Sends a templated autoresponder to the person who submitted the form.
"""
subject = cls.render_template(subject_template, payload)
body = cls.render_template(body_template, payload)
return cls.send_email([to_address], subject, body)