Files
Key-service-Server/Website/modules/emailservice/email.py
T
2026-08-27 14:01:55 +02:00

431 lines
19 KiB
Python

from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.application import MIMEApplication
import smtplib
import time
import os
import tempfile
from fpdf import FPDF
def _build_smtp_client():
smtp = smtplib.SMTP(
"mail.invario-software.de",
587,
timeout=10,
)
smtp.ehlo()
if True: # Always use TLS for security
smtp.starttls()
smtp.ehlo()
smtp.login("no-reply@invario-software.de", "#,EATwIn,68")
return smtp
def send(
email: list | str,
subject: str,
text_body: str,
html_body: str = None,
attachments: list = None
) -> bool:
"""Sends the email with Plain Text, HTML, and optional File Attachments support."""
if isinstance(email, str):
email = [email]
if attachments is None:
attachments = []
TEXT_SIGNATURE = (
"\n\n--\n"
"Mit freundlichen Grüßen\n"
"Ihr Invario Team\n\n"
"Invario UG\n"
"Am Sportplatz 10\n"
"83052 Bruckmühl"
)
HTML_SIGNATURE = """
<br><br>
<table cellpadding="0" cellspacing="0" border="0" style="font-family: Arial, Helvetica, sans-serif; font-size: 13px; color: #555555; line-height: 1.5; border-top: 1px solid #eaebed; padding-top: 15px; width: 100%;">
<tr>
<td>
<p style="margin:0 0 5px 0;">Mit freundlichen Grüßen,</p>
<p style="margin:0;"><strong style="font-size:15px; color: #333333;">Ihr Invario Team</strong></p><br>
<p style="margin:0 0 0 0; font-size: 12px; color: #888888;">
<strong>Invario UG</strong><br>
Am Sportplatz 10<br>
83052 Bruckmühl
</p>
</td>
</tr>
</table>
"""
text_content = f"{text_body}{TEXT_SIGNATURE}"
if html_body:
html_content = f"<html><body style='background-color: #f9f9f9; padding: 20px;'><div style='background-color: #ffffff; padding: 30px; border-radius: 8px; max-width: 600px; margin: 0 auto; box-shadow: 0px 2px 5px rgba(0,0,0,0.05);'>{html_body}{HTML_SIGNATURE}</div></body></html>"
else:
# Fallback if only text is provided
html_safe_text = text_body.replace('\n', '<br>')
html_content = f"<html><body><p style='font-family: Arial, sans-serif; color: #333333;'>{html_safe_text}</p>{HTML_SIGNATURE}</body></html>"
mails_per_second = 10
interval = 1.0 / mails_per_second
smtp = None
try:
smtp = _build_smtp_client()
for i, recipient in enumerate(email):
start_time = time.time()
# Root message structure for email with attachments
msg = MIMEMultipart("mixed")
msg["Subject"] = subject
msg["From"] = "Invario Team <no-reply@invario-software.de>"
msg["To"] = str(recipient)
# Sub-container for Plain Text and HTML bodies
msg_body = MIMEMultipart("alternative")
msg_body.attach(MIMEText(text_content, "plain"))
msg_body.attach(MIMEText(html_content, "html"))
msg.attach(msg_body)
# Attach files if provided
for file_path in attachments:
if file_path and os.path.isfile(file_path):
filename = os.path.basename(file_path)
with open(file_path, "rb") as file:
part = MIMEApplication(file.read(), Name=filename)
part['Content-Disposition'] = f'attachment; filename="{filename}"'
msg.attach(part)
smtp.sendmail(
from_addr="no-reply@invario-software.de",
to_addrs=[recipient],
msg=msg.as_string()
)
elapsed_time = time.time() - start_time
sleep_time = interval - elapsed_time
if sleep_time > 0 and i < len(email) - 1:
time.sleep(sleep_time)
return True
except Exception as e:
print(f"Debug: Fehler beim Senden der E-Mail: {e}")
return False
finally:
if smtp:
try:
smtp.quit()
except Exception:
pass
def send_register_token(email: str, token: str) -> bool:
"""Sends a professionally styled registration token to the user."""
subject = "Aktion erforderlich: Ihr Bestätigungscode für Invario"
text_note = (
"Herzlich willkommen bei Invario!\n\n"
"Um die Einrichtung Ihres Kontos abzuschließen und Ihre E-Mail-Adresse zu bestätigen, "
"geben Sie bitte den folgenden Sicherheitscode ein:\n\n"
f"Code: {token}\n\n"
"Dieser Code ist aus Sicherheitsgründen nur für begrenzte Zeit gültig. "
"Falls Sie diese Registrierung nicht angefordert haben, können Sie diese E-Mail einfach ignorieren."
)
html_note = f"""
<div style="font-family: Arial, Helvetica, sans-serif; color: #333333;">
<h2 style="color: #2c3e50; margin-top: 0;">Willkommen bei Invario!</h2>
<p style="font-size: 15px; line-height: 1.6;">
Vielen Dank für Ihre Registrierung. Um die Einrichtung Ihres Kontos abzuschließen und Ihre E-Mail-Adresse zu verifizieren, verwenden Sie bitte den folgenden Bestätigungscode:
</p>
<div style="background-color: #f1f5f9; border-left: 4px solid #3b82f6; border-radius: 4px; padding: 25px; text-align: center; margin: 30px 0;">
<span style="font-size: 32px; font-weight: bold; letter-spacing: 8px; color: #1e293b;">{token}</span>
</div>
<p style="font-size: 13px; color: #64748b; line-height: 1.5;">
Dieser Code ist aus Sicherheitsgründen nur für eine begrenzte Zeit gültig.<br>
Falls Sie kein Konto bei uns erstellt haben, können Sie diese E-Mail sicher ignorieren.
</p>
</div>
"""
return send(email, subject, text_body=text_note, html_body=html_note)
def send_password_reset_token(email: str, token: str) -> bool:
"""Sends a professionally styled password reset token to the user."""
subject = "Aktion erforderlich: Ihr Passwort-Zurücksetzungs-Code für Invario"
text_note = (
"Sie haben eine Anfrage zum Zurücksetzen Ihres Passworts gestellt.\n\n"
"Um Ihr Passwort zurückzusetzen, geben Sie bitte den folgenden Sicherheitscode ein:\n\n"
f"Code: {token}\n\n"
"Dieser Code ist aus Sicherheitsgründen nur für begrenzte Zeit gültig. "
"Falls Sie diese Anfrage nicht gestellt haben, können Sie diese E-Mail einfach ignorieren."
)
html_note = f"""
<div style="font-family: Arial, Helvetica, sans-serif; color: #333333;">
<h2 style="color: #2c3e50; margin-top: 0;">Passwort zurücksetzen</h2>
<p style="font-size: 15px; line-height: 1.6;">
Sie haben eine Anfrage zum Zurücksetzen Ihres Passworts gestellt. Um fortzufahren, verwenden Sie bitte den folgenden Bestätigungscode:
</p>
<div style="background-color: #f1f5f9; border-left: 4px solid #3b82f6; border-radius: 4px; padding: 25px; text-align: center; margin: 30px 0;">
<span style="font-size: 32px; font-weight: bold; letter-spacing: 8px; color: #1e293b;">{token}</span>
</div>
<p style="font-size: 13px; color: #64748b; line-height: 1.5;">
Dieser Code ist aus Sicherheitsgründen nur für eine begrenzte Zeit gültig.<br>
Falls Sie kein Zurücksetzen des Passworts angefordert haben, können Sie diese E-Mail sicher ignorieren.
</p>
</div>
"""
return send(email, subject, text_body=text_note, html_body=html_note)
def generate_main_contract_pdf(
school_name: str,
address: str,
price: str,
date: str,
output_path: str,
software_name: str = "Invario Inventarsystem"
):
"""Generiert den Hauptvertrag mit zentrierter und aufeinander abgestimmter Kopf- und Fußzeilen-Formatierung."""
pdf = FPDF()
pdf.add_page()
pdf.set_auto_page_break(auto=True, margin=15)
# --- KOPFBEREICH ---
pdf.set_font("Helvetica", style="B", size=13)
pdf.cell(0, 6, "Vertrag über die Nutzung der Software-Lizenz", ln=True, align="C")
pdf.cell(0, 6, f'"{software_name}"', ln=True, align="C")
pdf.ln(6)
pdf.set_font("Helvetica", style="B", size=10)
pdf.cell(0, 5, "Zwischen", ln=True, align="C")
pdf.set_font("Helvetica", size=10)
pdf.cell(0, 5, "Invario UG", ln=True, align="C")
pdf.cell(0, 5, "Am Sportplatz 10, 83052 Bruckmühl", ln=True, align="C")
pdf.set_font("Helvetica", style="I", size=9)
pdf.cell(0, 5, "- Anbieter / Lizenzgeber -", ln=True, align="C")
pdf.ln(4)
pdf.set_font("Helvetica", style="B", size=10)
pdf.cell(0, 5, "und", ln=True, align="C")
pdf.set_font("Helvetica", size=10)
pdf.cell(0, 5, f"{school_name}", ln=True, align="C")
pdf.cell(0, 5, f"{address}", ln=True, align="C")
pdf.set_font("Helvetica", style="I", size=9)
pdf.cell(0, 5, "- Auftraggeber / Schule -", ln=True, align="C")
pdf.ln(8)
# --- PARAGRAPHEN ---
pdf.set_font("Helvetica", style="B", size=10)
pdf.cell(0, 6, "1. Vertragsgegenstand", ln=True)
pdf.set_font("Helvetica", size=10)
pdf.multi_cell(0, 5,
f'(1) Der Anbieter stellt dem Auftraggeber die Software "{software_name}" (nachfolgend "Software") als webbasierte Anwendung im Wege des Software-as-a-Service (SaaS) zur Verfügung. Die Software wird auf der vom Anbieter genutzten Infrastruktur betrieben.\n'
"(2) Dieser Hauptvertrag wird durch folgende Dokumente ergänzt, die als integrale Bestandteile dieses Vertrages gelten:\n"
"a) Anlage 1: Allgemeine Geschäftsbedingungen (AGB) und Lizenzbedingungen (EULA) des Anbieters\n"
"b) Anlage 2: Auftragsverarbeitungsvertrag (AVV) gemäß Art. 28 DS-GVO nebst technischen und organisatorischen Maßnahmen (TOM)"
)
pdf.ln(4)
pdf.set_font("Helvetica", style="B", size=10)
pdf.cell(0, 6, "2. Vertragsbeginn", ln=True)
pdf.set_font("Helvetica", size=10)
pdf.multi_cell(0, 5,
"(1) Dieser Vertrag kommt entweder mit dem Datum der beidseitigen Unterzeichnung dieses Dokumentes oder durch die Nutzung der integrierten digitalen Bestellfunktion im Invario-Portal durch einen hierzu autorisierten Vertreter des Auftraggebers zustande.\n"
"(2) Die Vertragslaufzeit beginnt mit der Bereitstellung der Zugangsdaten durch den Anbieter."
)
pdf.ln(4)
pdf.set_font("Helvetica", style="B", size=10)
pdf.cell(0, 6, "3. Laufzeit und Kündigung", ln=True)
pdf.set_font("Helvetica", size=10)
pdf.multi_cell(0, 5,
"(1) Der Vertrag wird zunächst für die Dauer von zwölf (12) Monaten geschlossen (Mindestlaufzeit).\n"
"(2) Er verlängert sich automatisch um jeweils ein weiteres Jahr, sofern er nicht von einer Partei mit einer Frist von drei (3) Monaten zum Ende der jeweiligen Laufzeit gekündigt wird.\n"
"(3) Die Kündigung kann in Textform (z. B. per E-Mail) oder über die dafür vorgesehene Funktion im Invario-Portal erfolgen. Das Recht zur außerordentlichen Kündigung aus wichtigem Grund bleibt unberührt."
)
pdf.ln(4)
pdf.set_font("Helvetica", style="B", size=10)
pdf.cell(0, 6, "4. Vergütung und Bezahlung", ln=True)
pdf.set_font("Helvetica", size=10)
pdf.multi_cell(0, 5,
f"(1) Für die Nutzung der Software zahlt der Auftraggeber eine jährliche Lizenzgebühr in Höhe von {price} EUR (zzgl. ggf. anfallender gesetzlicher Umsatzsteuer).\n"
"(2) Die Vergütung ist jeweils für ein Vertragsjahr im Voraus fällig.\n"
"(3) Der Rechnungsbetrag ist innerhalb von 30 Tagen nach Erhalt der Rechnung ohne Abzug zur Zahlung fällig."
)
pdf.ln(4)
pdf.set_font("Helvetica", style="B", size=10)
pdf.cell(0, 6, "5. Nutzung und Support", ln=True)
pdf.set_font("Helvetica", size=10)
pdf.multi_cell(0, 5,
"(1) Der Umfang der Lizenzeinräumung sowie die Nutzungsbeschränkungen richten sich nach den Bestimmungen der anliegenden EULA (Anlage 1).\n"
"(2) Der technische Support wird innerhalb der in den AGB (Anlage 1) definierten Servicezeiten über das integrierte Ticket-System oder per E-Mail erbracht."
)
pdf.ln(4)
pdf.set_font("Helvetica", style="B", size=10)
pdf.cell(0, 6, "6. Datenschutz und Datensicherheit", ln=True)
pdf.set_font("Helvetica", size=10)
pdf.multi_cell(0, 5,
"(1) Die Parteien verpflichten sich zur Einhaltung der datenschutzrechtlichen Bestimmungen. Die Details der Verarbeitung der personenbezogenen Daten werden im anliegenden AVV (Anlage 2) verbindlich geregelt.\n"
"(2) Der Anbieter sichert zu, dass die Speicherung und Verarbeitung der Daten des Auftraggebers ausschließlich in Rechenzentren mit Standort innerhalb der Bundesrepublik Deutschland erfolgt."
)
pdf.ln(4)
pdf.set_font("Helvetica", style="B", size=10)
pdf.cell(0, 6, "7. Haftung", ln=True)
pdf.set_font("Helvetica", size=10)
pdf.multi_cell(0, 5,
"(1) Die Haftung des Anbieters richtet sich nach den Regelungen in Ziffer 7 der Allgemeinen Geschäftsbedingungen (Anlage 1).\n"
"(2) Für Unterbrechungen der Verfügbarkeit durch höhere Gewalt, Störungen im Bereich außerhalb des Übergabepunktes (z. B. Internet-Backbone) sowie für unvorhersehbare Sicherheitslücken, die nach dem aktuellen Stand der Technik zum Zeitpunkt des Auftretens noch nicht abwehrbar waren (Zero-Day-Exploits), haftet der Anbieter nur nach Maßgabe der Haftungsregelungen in den AGB, es sei denn, ihn trifft ein Verschulden an der verzögerten Behebung. Gesetzliche Datenschutzpflichten und Meldepflichten bleiben unberührt."
)
pdf.ln(4)
pdf.set_font("Helvetica", style="B", size=10)
pdf.cell(0, 6, "8. Schlussbestimmungen", ln=True)
pdf.set_font("Helvetica", size=10)
pdf.multi_cell(0, 5,
"(1) Änderungen und Ergänzungen dieses Vertrages bedürfen der Textform.\n"
"(2) Sollten einzelne Bestimmungen dieses Vertrages unwirksam oder undurchführbar sein oder werden, bleibt die Wirksamkeit der übrigen Bestimmungen hiervon unberührt. Anstelle der unwirksamen oder undurchführbaren Bestimmung gelten die gesetzlichen Vorschriften. Sofern es sich nicht um Allgemeine Geschäftsbedingungen handelt, verpflichten sich die Parteien, eine wirksame Regelung zu vereinbaren, die dem wirtschaftlichen Zweck der unwirksamen oder undurchführbaren Bestimmung möglichst nahekommt.\n"
"(3) Es gilt das Recht der Bundesrepublik Deutschland. Ausschließlicher Gerichtsstand für alle Streitigkeiten aus oder im Zusammenhang mit diesem Vertrag ist, soweit gesetzlich zulässig, der Sitz des Anbieters."
)
pdf.ln(6)
# --- UNTERSCHRIFTEN ---
pdf.set_font("Helvetica", style="B", size=10)
pdf.cell(0, 6, "Unterschriften und Einverständnis", ln=True, align="C")
pdf.set_font("Helvetica", size=9)
pdf.multi_cell(0, 4, "Mit der Unterzeichnung bestätigen die Parteien den Abschluss des Hauptvertrages inklusive der beigefügten Anlagen (AGB, EULA und AVV).", align="C")
pdf.ln(8)
col_width = 85
start_x = (210 - (col_width * 2)) / 2
pdf.set_font("Helvetica", size=9)
pdf.set_x(start_x)
pdf.cell(col_width, 5, "Für den Auftraggeber", align="C", ln=False)
pdf.cell(col_width, 5, "Für den Anbieter", align="C", ln=True)
pdf.ln(8)
pdf.set_x(start_x)
pdf.cell(col_width, 5, f"{date} ________________________", align="C", ln=False)
pdf.cell(col_width, 5, f"{date} ___________ _____________", align="C", ln=True)
pdf.set_x(start_x)
pdf.cell(col_width, 5, "(Verantwortlicher)", align="C", ln=False)
pdf.cell(col_width, 5, "(Simon Brieger)", align="C", ln=True)
pdf.output(output_path)
return output_path
def send_accreditation_email(
recipient: str,
domain: str,
username: str,
password: str,
school_name: str = "[Name der Schule / des Auftraggebers]",
address: str = "[Straße, Hausnummer] [PLZ, Ort]",
price: str = "250,00",
date: str = "[Ort, Datum]",
software_name: str = "Invario Inventarsystem",
) -> bool:
"""Generiert den Hauptvertrag temporär und versendet ihn zusammen mit Rechnung, AGB & AVV per E-Mail."""
# Clean Sanitization für Dateinamen
safe_school_name = "".join([c for c in school_name if c.isalnum() or c in (' ', '_', '-')]).rstrip()
contract_filename = f"Hauptvertrag_Invario_{safe_school_name}.pdf"
contract_pdf_path = os.path.join(tempfile.gettempdir(), contract_filename)
# 1. Hauptvertrag dynamisch erzeugen
generate_main_contract_pdf(
school_name=school_name,
address=address,
price=price,
date=date,
software_name=software_name,
output_path=contract_pdf_path
)
# 2. Pfade zu den statischen Dokumenten
agb_pdf_path = os.path.join("/opt", "Key-service-Server", "Website", "static", "download", "AGB Invario.pdf")
avv_pdf_path = os.path.join("/opt", "Key-service-Server", "Website", "static", "download", "AVV Invario.pdf")
# 3. E-Mail Inhalte anpassen
subject = "Ihre Zugangsdaten und Unterlagen für Invario"
text_note = (
f"Sehr geehrte/r {username},\n\n"
"Vielen Dank für Ihre Akkreditierung bei Invario. "
"Im Anhang finden Sie Ihre Rechnung, den Hauptvertrag sowie die AGB und den AVV.\n\n"
f"Ihre Zugangsdaten:\n"
f"Domain: https://{domain}\n"
f"Benutzername: {username}\n"
f"Passwort: {password}\n\n"
"Bitte bewahren Sie diese Informationen sicher auf."
)
html_note = f"""
<div style="font-family: Arial, Helvetica, sans-serif; color: #333333;">
<h2 style="color: #2c3e50; margin-top: 0;">Willkommen bei Invario!</h2>
<p style="font-size: 15px; line-height: 1.6;">
Vielen Dank für Ihre Akkreditierung. Im Anhang finden Sie Ihre Rechnung, den Hauptvertrag sowie die AGB und den AVV.
</p>
<h3 style="color: #2c3e50;">Ihre Zugangsdaten:</h3>
<ul style="font-size: 15px; line-height: 1.6;">
<li><strong>Domain:</strong> <a href="https://{domain}">https://{domain}</a></li>
<li><strong>Benutzername:</strong> {username}</li>
<li><strong>Passwort:</strong> {password}</li>
</ul>
<p style="font-size: 13px; color: #64748b; line-height: 1.5;">
Bitte bewahren Sie diese Informationen sicher auf.
</p>
</div>
"""
# 4. Vorhandene Anhänge einsammeln
attachments = []
# Hauptvertrag anfügen
if os.path.exists(contract_pdf_path):
attachments.append(contract_pdf_path)
# AGB & AVV anfügen
if os.path.exists(agb_pdf_path):
attachments.append(agb_pdf_path)
else:
print(f"[WARNING] AGB unter {agb_pdf_path} nicht gefunden.")
if os.path.exists(avv_pdf_path):
attachments.append(avv_pdf_path)
else:
print(f"[WARNING] AVV unter {avv_pdf_path} nicht gefunden.")
# 5. E-Mail versenden & temporäre Datei aufräumen
try:
success = send(recipient, subject, text_body=text_note, html_body=html_note, attachments=attachments)
return success
finally:
if os.path.exists(contract_pdf_path):
try:
os.remove(contract_pdf_path)
except OSError as e:
print(f"[WARNING] Konnte temporären Vertrag nicht löschen: {e}")