v1
Some checks failed
CI / test (3.11) (push) Failing after 5m36s
CI / build-image (push) Has been skipped

This commit is contained in:
2025-10-22 16:48:55 +02:00
commit 4cefd4e3ab
53 changed files with 5837 additions and 0 deletions

View File

@@ -0,0 +1 @@
"""Service layer namespace."""

112
server/services/contact.py Normal file
View File

@@ -0,0 +1,112 @@
"""Business logic for contact submissions."""
from __future__ import annotations
import logging
import smtplib
from dataclasses import dataclass
from datetime import datetime, timezone
from email.message import EmailMessage
from typing import Any, Dict, Tuple
from .. import settings
from ..database import save_contact
from ..metrics import record_submission
from ..utils import is_valid_email
@dataclass
class ContactSubmission:
name: str
email: str
company: str | None
message: str
timeline: str | None
created_at: str = datetime.now(timezone.utc).isoformat()
def validate_submission(raw: Dict[str, Any]) -> Tuple[ContactSubmission | None, Dict[str, str]]:
"""Validate the incoming payload and return a submission object."""
name = (raw.get("name") or "").strip()
email = (raw.get("email") or "").strip()
message = (raw.get("message") or "").strip()
consent = raw.get("consent")
company = (raw.get("company") or "").strip()
errors: Dict[str, str] = {}
if not name:
errors["name"] = "Name is required."
elif len(name) > 200:
errors["name"] = "Name is too long (max 200 chars)."
if not is_valid_email(email):
errors["email"] = "Valid email is required."
if not message:
errors["message"] = "Message is required."
elif len(message) > 5000:
errors["message"] = "Message is too long (max 5000 chars)."
if not consent:
errors["consent"] = "Consent is required."
if company and len(company) > 200:
errors["company"] = "Organisation name is too long (max 200 chars)."
if errors:
return None, errors
submission = ContactSubmission(
name=name,
email=email,
company=company or None,
message=message,
timeline=(raw.get("timeline") or "").strip() or None,
)
return submission, {}
def persist_submission(submission: ContactSubmission) -> int:
"""Persist the submission and update metrics."""
record_id = save_contact(submission)
record_submission()
return record_id
def send_notification(submission: ContactSubmission) -> bool:
"""Send an email notification for the submission if SMTP is configured."""
if not settings.SMTP_SETTINGS["host"] or not settings.SMTP_SETTINGS["recipients"]:
logging.info("SMTP not configured; skipping email notification")
return False
sender = settings.SMTP_SETTINGS["sender"] or "no-reply@example.com"
recipients = settings.SMTP_SETTINGS["recipients"]
msg = EmailMessage()
msg["Subject"] = f"Neue Kontaktanfrage von {submission.name}"
msg["From"] = sender
msg["To"] = ", ".join(recipients)
msg.set_content(
"\n".join(
[
f"Name: {submission.name}",
f"E-Mail: {submission.email}",
f"Organisation: {submission.company or ''}",
f"Zeithorizont: {submission.timeline or ''}",
"",
"Nachricht:",
submission.message,
"",
f"Eingang: {submission.created_at}",
]
)
)
try:
with smtplib.SMTP(settings.SMTP_SETTINGS["host"], settings.SMTP_SETTINGS["port"], timeout=15) as server:
if settings.SMTP_SETTINGS["use_tls"]:
server.starttls()
if settings.SMTP_SETTINGS["username"]:
server.login(
settings.SMTP_SETTINGS["username"], settings.SMTP_SETTINGS["password"] or "")
server.send_message(msg)
logging.info("Notification email dispatched to %s", recipients)
return True
except Exception as exc: # pragma: no cover - SMTP failures are logged only
logging.error("Failed to send notification email: %s", exc)
return False

View File

@@ -0,0 +1,96 @@
"""Business logic for newsletter subscriptions."""
from __future__ import annotations
from datetime import datetime, timezone
from ..database import save_subscriber, delete_subscriber, update_subscriber
from ..utils import is_valid_email
def validate_email(email: str) -> bool:
"""Return True when the provided email passes a basic sanity check."""
return is_valid_email(email)
def subscribe(email: str) -> bool:
"""Persist the subscription and return False when it already exists."""
created_at = datetime.now(timezone.utc).isoformat()
return save_subscriber(email, created_at=created_at)
def unsubscribe(email: str) -> bool:
"""Remove the subscription and return True if it existed."""
return delete_subscriber(email)
def update_email(old_email: str, new_email: str) -> bool:
"""Update the email for a subscription. Return True if updated."""
return update_subscriber(old_email, new_email)
def send_newsletter_to_subscribers(subject: str, content: str, emails: list[str], sender_name: str | None = None) -> int:
"""Send newsletter to list of email addresses. Returns count of successful sends."""
import logging
from .. import settings
if not settings.SMTP_SETTINGS["host"]:
logging.error("SMTP not configured, cannot send newsletter")
return 0
try:
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
# Create message
msg = MIMEMultipart('alternative')
msg['Subject'] = subject
msg['From'] = settings.SMTP_SETTINGS["sender"] or "noreply@example.com"
# Format content
formatted_content = content.replace('\n', '<br>')
html_content = f"""
<html>
<body>
{formatted_content}
</body>
</html>
"""
# Add HTML content
html_part = MIMEText(html_content, 'html')
msg.attach(html_part)
# Send to each recipient individually for better deliverability
success_count = 0
with smtplib.SMTP(settings.SMTP_SETTINGS["host"], settings.SMTP_SETTINGS["port"]) as server:
if settings.SMTP_SETTINGS["use_tls"]:
server.starttls()
if settings.SMTP_SETTINGS["username"] and settings.SMTP_SETTINGS["password"]:
server.login(
settings.SMTP_SETTINGS["username"], settings.SMTP_SETTINGS["password"])
for email in emails:
try:
# Create a fresh copy for each recipient
recipient_msg = MIMEMultipart('alternative')
recipient_msg['Subject'] = subject
recipient_msg['From'] = msg['From']
recipient_msg['To'] = email
# Add HTML content
recipient_msg.attach(MIMEText(html_content, 'html'))
server.sendmail(msg['From'], email,
recipient_msg.as_string())
success_count += 1
except Exception as exc:
logging.exception(
"Failed to send newsletter to %s: %s", email, exc)
return success_count
except Exception as exc:
logging.exception("Failed to send newsletter: %s", exc)
return 0