From e9aea7e0398216f9ae4c578372688e264d047458 Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Tue, 15 Sep 2026 20:53:18 +0200 Subject: [PATCH] implementation of the Email add on --- Web/app.py | 84 +++++++++++++++++++++++--- Web/modules/emailservice/email.py | 58 ++++++++++++------ Web/modules/terminplaner/blueprint.py | 31 +++++++++- Web/templates/admin_audit.html | 8 ++- Web/templates/admin_borrowings.html | 12 +++- Web/templates/student_cards_admin.html | 6 ++ Web/templates/terminplaner.html | 6 ++ 7 files changed, 174 insertions(+), 31 deletions(-) diff --git a/Web/app.py b/Web/app.py index 9bbbd25..04dd2ba 100755 --- a/Web/app.py +++ b/Web/app.py @@ -48,7 +48,7 @@ import Web.modules.log.audit_log as al import push_notifications as pn import Web.modules.inventarsystem.pdf_export as pdf_export import Web.modules.inventarsystem.excel_export as excel_export -from Web.modules.emailservice.email import send +from Web.modules.emailservice.email import send, send_pdf import datetime from apscheduler.schedulers.background import BackgroundScheduler from bson.objectid import ObjectId, InvalidId @@ -4892,7 +4892,8 @@ def student_cards_admin(): edit_mode=edit_mode, form_data=form_data, available_classes=available_classes, # Enthält nun die lesbaren Klassen - config=cfg.get_school_info() + config=cfg.get_school_info(), + email_service_enabled=cfg.MODULES.is_enabled('mail') ) @@ -5353,7 +5354,7 @@ def student_card_class_barcode_download(): flash('Fehler beim PDF-Download', 'error') return redirect(url_for('student_cards_admin')) -@app.route('/student_card_single_barcode_download/', methods=['GET']) +@app.route('/student_card_single_barcode_download/', methods=['GET', 'POST']) def student_card_single_barcode_download(card_id): """ Download PDF with single student card barcode. @@ -5533,11 +5534,34 @@ def student_card_single_barcode_download(card_id): c.save() pdf_buffer.seek(0) + pdf_filename = f'ausweis_{card["AusweisId"]}.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('student_cards_admin')) + if not cfg.MODULES.is_enabled('mail'): + flash('Das E-Mail-Add-on ist deaktiviert. Der Ausweis kann weiterhin als PDF heruntergeladen werden.', 'warning') + return redirect(url_for('student_cards_admin')) + sent = send_pdf( + recipient_email, + f'Bibliotheksausweis {card["AusweisId"]}', + f'Anbei erhalten Sie den Bibliotheksausweis für {card.get("SchülerName", "")} als PDF.', + 'Bibliotheksverwaltung', + pdf_buffer.getvalue(), + pdf_filename, + ) + flash( + 'Bibliotheksausweis wurde per E-Mail versendet.' if sent else 'Bibliotheksausweis konnte nicht per E-Mail versendet werden.', + 'success' if sent else 'error', + ) + return redirect(url_for('student_cards_admin')) + return send_file( pdf_buffer, mimetype='application/pdf', as_attachment=True, - download_name=f'ausweis_{card["AusweisId"]}.pdf' + download_name=pdf_filename ) except Exception as e: app.logger.error(f"Error occurred while generating PDF for card {card['AusweisId']}: {e}") @@ -8959,7 +8983,8 @@ def admin_borrowings(): entries=entries, library_module_enabled=cfg.MODULES.is_enabled('library'), student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'), - mail_module_enabled=cfg.MODULES.is_enabled('mail') + mail_module_enabled=cfg.MODULES.is_enabled('mail'), + email_service_enabled=cfg.MODULES.is_enabled('mail') ) """-----------------------------------------------------------Audit Routes-------------------------------------------------------""" @@ -9051,7 +9076,7 @@ def admin_audit_dashboard(): if client: client.close() -@app.route('/admin/audit/export/pdf/official', methods=['GET']) +@app.route('/admin/audit/export/pdf/official', methods=['GET', 'POST']) def admin_audit_export_pdf_official(): """Export audit report as professional PDF (Official Report - full DIN 5008 compliant).""" if 'username' not in session: @@ -9118,6 +9143,29 @@ def admin_audit_export_pdf_official(): export_type='official', school_info=school_info ) + + 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('admin_audit_dashboard')) + if not cfg.MODULES.is_enabled('mail'): + flash('Das E-Mail-Add-on ist deaktiviert. Der Auditbericht kann weiterhin als PDF heruntergeladen werden.', 'warning') + return redirect(url_for('admin_audit_dashboard')) + filename = f'audit-official-report-{datetime.datetime.now(ZoneInfo("Europe/Berlin")).strftime("%Y%m%d-%H%M%S")}.pdf' + sent = send_pdf( + recipient_email, + 'Amtlicher Auditbericht', + 'Anbei erhalten Sie den aktuellen amtlichen Auditbericht des Inventarsystems als PDF.', + 'Inventarsystem', + pdf_content, + filename, + ) + flash( + 'Auditbericht wurde per E-Mail versendet.' if sent else 'Auditbericht konnte nicht per E-Mail versendet werden.', + 'success' if sent else 'error', + ) + return redirect(url_for('admin_audit_dashboard')) response = make_response(pdf_content) response.headers['Content-Type'] = 'application/pdf' @@ -9492,11 +9540,33 @@ def admin_create_invoice(borrow_id): ) pdf_buffer = pdf_export._build_invoice_pdf(invoice_data) + pdf_filename = f'rechnung_{invoice_number}.pdf' + if request.form.get('delivery') == 'email': + 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('admin_borrowings')) + if not cfg.MODULES.is_enabled('mail'): + flash('Das E-Mail-Add-on ist deaktiviert. Die PDF-Rechnung kann weiterhin heruntergeladen werden.', 'warning') + return redirect(url_for('admin_borrowings')) + sent = send_pdf( + recipient_email, + f'Rechnung {invoice_number} - {item_name}', + f'Anbei erhalten Sie die Rechnung {invoice_number} zum Element {item_name}.', + 'Bibliotheksverwaltung', + pdf_buffer.getvalue(), + pdf_filename, + ) + flash( + 'PDF-Rechnung wurde per E-Mail versendet.' if sent else 'PDF-Rechnung konnte nicht per E-Mail versendet werden.', + 'success' if sent else 'error', + ) + return redirect(url_for('admin_borrowings')) return send_file( pdf_buffer, mimetype='application/pdf', as_attachment=True, - download_name=f'rechnung_{invoice_number}.pdf' + download_name=pdf_filename ) except Exception as e: app.logger.error(f"Error creating damage invoice for borrow {borrow_id}: {e}") diff --git a/Web/modules/emailservice/email.py b/Web/modules/emailservice/email.py index 721dd9f..42f57ef 100644 --- a/Web/modules/emailservice/email.py +++ b/Web/modules/emailservice/email.py @@ -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} " + 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 \ No newline at end of file + 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)) \ No newline at end of file diff --git a/Web/modules/terminplaner/blueprint.py b/Web/modules/terminplaner/blueprint.py index 7ad2568..02abd8b 100644 --- a/Web/modules/terminplaner/blueprint.py +++ b/Web/modules/terminplaner/blueprint.py @@ -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/', methods=['GET']) +@appoint_bp.route('/export_pdf_brief/', 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') ) \ No newline at end of file diff --git a/Web/templates/admin_audit.html b/Web/templates/admin_audit.html index c130ed5..b433805 100644 --- a/Web/templates/admin_audit.html +++ b/Web/templates/admin_audit.html @@ -110,10 +110,16 @@

📄 PDF-Export (DIN 5008 konform)

Professionelle Berichte für Schulträger und Behörden

-
+
📋 Amtlicher Bericht (DIN 5008) + {% if mail_module_enabled %} +
+ + +
+ {% endif %}
diff --git a/Web/templates/admin_borrowings.html b/Web/templates/admin_borrowings.html index dd0f2bc..40ad76f 100644 --- a/Web/templates/admin_borrowings.html +++ b/Web/templates/admin_borrowings.html @@ -195,6 +195,13 @@
+ {% if email_service_enabled %} +
+ + +
+ {% endif %} +
diff --git a/Web/templates/student_cards_admin.html b/Web/templates/student_cards_admin.html index e582e1c..28433e7 100644 --- a/Web/templates/student_cards_admin.html +++ b/Web/templates/student_cards_admin.html @@ -529,6 +529,12 @@ 📥 PDF + {% if email_service_enabled %} +
+ + +
+ {% endif %}
diff --git a/Web/templates/terminplaner.html b/Web/templates/terminplaner.html index 98da3c0..bd6e971 100644 --- a/Web/templates/terminplaner.html +++ b/Web/templates/terminplaner.html @@ -42,6 +42,12 @@ 📄 Brief als PDF herunterladen + {% if mail_service_enabled %} + + + +
+ {% endif %} {% if calendar_link %}