implementation of the Email add on
Release Inventarsystem / release-docker (push) Successful in 2m24s

This commit is contained in:
2026-09-15 20:53:18 +02:00
parent 00d45a9d1b
commit e9aea7e039
7 changed files with 174 additions and 31 deletions
+39 -19
View File
@@ -1,5 +1,6 @@
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.application import MIMEApplication
import smtplib
import time
@@ -17,18 +18,25 @@ def _build_smtp_client():
smtp.starttls()
smtp.ehlo()
if cfg.EMAIL_USERNAME:
smtp.login("no-reply@invario-software.de", "#,EATwIn,68" or "")
smtp.login(cfg.EMAIL_USERNAME, cfg.EMAIL_PASSWORD or "")
return smtp
def send(email: list | str, subject: str, note: str, sender: str) -> bool:
"""Sends the email with the link to the Clients."""
def _normalize_recipients(email: list | str) -> list[str]:
if isinstance(email, str):
email = email.replace(';', ',').split(',')
return [str(recipient).strip() for recipient in (email or []) if str(recipient).strip()]
def _send_message(email: list | str, subject: str, note: str, sender: str, attachment=None) -> bool:
"""Send a plain/HTML message, optionally with one PDF attachment."""
if not cfg.MODULES.is_enabled("mail"):
print("Debug: Module not enabled")
return False
if isinstance(email, str):
email = [email]
recipients = _normalize_recipients(email)
if not recipients:
return False
body_message = note
@@ -63,25 +71,27 @@ def send(email: list | str, subject: str, note: str, sender: str) -> bool:
try:
smtp = _build_smtp_client()
for i, recipient in enumerate(email):
for i, recipient in enumerate(recipients):
start_time = time.time()
msg = MIMEMultipart("alternative")
msg = MIMEMultipart("mixed" if attachment else "alternative")
msg["Subject"] = str(subject)
msg["From"] = f"{sender} <no-reply@invario-software.de>"
from_address = cfg.EMAIL_FROM_ADDRESS or cfg.EMAIL_USERNAME or "no-reply@invario-software.de"
msg["From"] = f"{sender} <{from_address}>"
msg["To"] = str(recipient)
msg.attach(MIMEText(text_content, "plain"))
msg.attach(MIMEText(html_content, "html"))
#smtp.sendmail(
# from_addr=cfg.EMAIL_USERNAME,
# to_addrs=[recipient],
# msg=msg.as_string()
#)
body = MIMEMultipart("alternative") if attachment else msg
body.attach(MIMEText(text_content, "plain"))
body.attach(MIMEText(html_content, "html"))
if attachment:
msg.attach(body)
attachment_payload, filename = attachment
pdf_part = MIMEApplication(attachment_payload, _subtype="pdf")
pdf_part.add_header("Content-Disposition", "attachment", filename=filename)
msg.attach(pdf_part)
smtp.sendmail(
from_addr="no-reply@invario-software.de",
from_addr=from_address,
to_addrs=[recipient],
msg=msg.as_string()
)
@@ -89,7 +99,7 @@ def send(email: list | str, subject: str, note: str, sender: str) -> bool:
elapsed_time = time.time() - start_time
sleep_time = interval - elapsed_time
if sleep_time > 0 and i < len(email) - 1:
if sleep_time > 0 and i < len(recipients) - 1:
time.sleep(sleep_time)
return True
@@ -101,4 +111,14 @@ def send(email: list | str, subject: str, note: str, sender: str) -> bool:
try:
smtp.quit()
except Exception:
pass
pass
def send(email: list | str, subject: str, note: str, sender: str) -> bool:
"""Send an HTML/plain email without an attachment."""
return _send_message(email, subject, note, sender)
def send_pdf(email: list | str, subject: str, note: str, sender: str, pdf_bytes: bytes, filename: str) -> bool:
"""Send an email with a PDF attachment."""
return _send_message(email, subject, note, sender, attachment=(pdf_bytes, filename))
+28 -3
View File
@@ -3,6 +3,7 @@ import Web.modules.terminplaner.backend_server as appointment_service
import Web.modules.database.settings as cfg
import Web.modules.database.termine as termin
import Web.modules.database.user as us
from Web.modules.emailservice.email import send_pdf
from Web.modules.terminplaner.backend_server import _resolve_public_base_url
import csv
import io
@@ -409,7 +410,7 @@ def client_slot_calendar_export(appointment_id):
return response
@appoint_bp.route('/export_pdf_brief/<plan_id>', methods=['GET'])
@appoint_bp.route('/export_pdf_brief/<plan_id>', methods=['GET', 'POST'])
def export_pdf_brief(plan_id):
# 1. Daten holen (Hier als Beispiel, passe dies auf deine Datenbank an)
# terminplan = Terminplan.query.get(plan_id)
@@ -532,12 +533,35 @@ def export_pdf_brief(plan_id):
# Buffer auf Anfang zurücksetzen
pdf_buffer.seek(0)
pdf_filename = f"Einladung_{plan_daten['titel'].replace(' ', '_')}.pdf"
if request.method == 'POST':
recipient_email = (request.form.get('recipient_email') or '').strip()
if not recipient_email:
flash('Bitte eine Empfänger-E-Mail-Adresse eingeben oder nur PDF auswählen.', 'error')
return redirect(url_for('terminplaner.main', tenant=tenant_id or None))
if not cfg.MODULES.is_enabled('mail'):
flash('Das E-Mail-Add-on ist deaktiviert. Der Einladungsbrief kann weiterhin als PDF heruntergeladen werden.', 'warning')
return redirect(url_for('terminplaner.main', tenant=tenant_id or None))
sent = send_pdf(
recipient_email,
f'Einladung zur Terminbuchung: {plan_daten["titel"]}',
f'Anbei erhalten Sie den Einladungsbrief für {plan_daten["titel"]} als PDF.',
'Terminplanungssystem',
pdf_buffer.getvalue(),
pdf_filename,
)
flash(
'Einladungsbrief wurde per E-Mail versendet.' if sent else 'Einladungsbrief konnte nicht per E-Mail versendet werden.',
'success' if sent else 'error',
)
return redirect(url_for('terminplaner.main', tenant=tenant_id or None))
# An Nutzer ausliefern
return send_file(
pdf_buffer,
mimetype='application/pdf',
as_attachment=True,
download_name=f"Einladung_{plan_daten['titel'].replace(' ', '_')}.pdf"
download_name=pdf_filename
)
@appoint_bp.route('/')
@@ -557,5 +581,6 @@ def main():
current_user=current_user,
upcoming_events=upcoming_events,
tenant_id=tenant_id,
appointment_module_enabled=cfg.MODULES.is_enabled('terminplan')
appointment_module_enabled=cfg.MODULES.is_enabled('terminplan'),
mail_service_enabled=cfg.MODULES.is_enabled('mail')
)