Compare commits

3 Commits

Author SHA1 Message Date
32f10c3197 Merge pull request 'dev' (#6) from dev into main
Reviewed-on: #6
2026-07-07 13:45:25 +00:00
smueller
0e37dac816 Update dashboard to handle newsletter send status and redirect accordingly
- Added logic to retrieve and display the send status and message on the dashboard.
- Modified the send_now function to redirect to the dashboard with query parameters for status and message.
- Removed redundant context updates in the send_now function for cleaner code.
2026-07-07 15:44:37 +02:00
smueller
3d0f5e4f04 Enhance newsletter functionality with contact email support
- Added support for a contact email in newsletter generation, allowing users to specify a reply-to address.
- Updated the SMTP settings to include a reply-to field in the configuration.
- Modified the newsletter templates to display the contact email in plain text and HTML formats.
- Improved the dashboard UI to allow users to set the reply-to address when configuring SMTP settings.
2026-07-07 15:42:12 +02:00
4 changed files with 70 additions and 36 deletions

View File

@@ -2,6 +2,7 @@ import asyncio
import logging import logging
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from urllib.parse import urlencode
from pydantic import ValidationError from pydantic import ValidationError
@@ -101,6 +102,7 @@ async def _run_scheduled_send_if_due() -> None:
display=DEFAULT_DISPLAY, display=DEFAULT_DISPLAY,
editor_tip="", editor_tip="",
highlight_list=[], highlight_list=[],
contact_email=settings_snapshot.get("reply_to") or settings_snapshot.get("from_email", ""),
) )
if gen["wiki_error"] or not gen["articles"]: if gen["wiki_error"] or not gen["articles"]:
# Kein Versand ohne Inhalt; erneuter Versuch beim nächsten Intervall. # Kein Versand ohne Inhalt; erneuter Versuch beim nächsten Intervall.
@@ -296,6 +298,7 @@ async def _generate_newsletter(
display, display,
editor_tip: str, editor_tip: str,
highlight_list: list[str], highlight_list: list[str],
contact_email: str = "",
) -> dict: ) -> dict:
p = resolve_period(period_mode, days) p = resolve_period(period_mode, days)
result = { result = {
@@ -322,9 +325,9 @@ async def _generate_newsletter(
"articles_new": new_articles, "articles_new": new_articles,
"articles_edited": edited_articles, "articles_edited": edited_articles,
"subject": create_subject(p["month_label"], p["days"]), "subject": create_subject(p["month_label"], p["days"]),
"plain_text": create_plain_text(filtered, p["days"], display, category, editor_tip, highlight_list, p["label"], prange, article_type), "plain_text": create_plain_text(filtered, p["days"], display, category, editor_tip, highlight_list, p["label"], prange, article_type, contact_email),
"raw_html": create_html(filtered, p["days"], display, category, editor_tip, highlight_list, p["label"], prange, article_type, p["month_label"]), "raw_html": create_html(filtered, p["days"], display, category, editor_tip, highlight_list, p["label"], prange, article_type, p["month_label"], contact_email),
"outlook_html": create_outlook_html(filtered, p["days"], display, category, editor_tip, highlight_list, p["label"], prange, article_type, p["month_label"]), "outlook_html": create_outlook_html(filtered, p["days"], display, category, editor_tip, highlight_list, p["label"], prange, article_type, p["month_label"], contact_email),
} }
) )
except WikiFetchError as exc: except WikiFetchError as exc:
@@ -335,6 +338,10 @@ async def _generate_newsletter(
@app.get("/dashboard", response_class=HTMLResponse) @app.get("/dashboard", response_class=HTMLResponse)
async def dashboard(request: Request, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): async def dashboard(request: Request, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
context = _base_context(request, current_user, db) context = _base_context(request, current_user, db)
send_status = request.query_params.get("send_status")
send_result = None
if send_status:
send_result = {"status": send_status, "message": request.query_params.get("send_msg", "")}
context.update({ context.update({
"articles": [], "articles": [],
"articles_new": [], "articles_new": [],
@@ -345,7 +352,7 @@ async def dashboard(request: Request, db: Session = Depends(get_db), current_use
"subject": "", "subject": "",
"filters": _empty_filters(), "filters": _empty_filters(),
"wiki_error": None, "wiki_error": None,
"send_result": None, "send_result": send_result,
}) })
logs = db.query(SendLog).order_by(SendLog.created_at.desc()).limit(20).all() logs = db.query(SendLog).order_by(SendLog.created_at.desc()).limit(20).all()
context["send_logs"] = logs context["send_logs"] = logs
@@ -374,6 +381,8 @@ async def dashboard_generate(
article_type = "all" article_type = "all"
display = parse_display_options(show_date, show_user, show_category) display = parse_display_options(show_date, show_user, show_category)
highlight_list = parse_highlights(highlights) highlight_list = parse_highlights(highlights)
smtp_cfg = get_smtp_settings(db)
contact_email = smtp_cfg["reply_to"] or smtp_cfg["from_email"]
gen = await _generate_newsletter( gen = await _generate_newsletter(
period_mode=period, period_mode=period,
@@ -383,6 +392,7 @@ async def dashboard_generate(
display=display, display=display,
editor_tip=editor_tip, editor_tip=editor_tip,
highlight_list=highlight_list, highlight_list=highlight_list,
contact_email=contact_email,
) )
context = _base_context(request, current_user, db) context = _base_context(request, current_user, db)
@@ -434,6 +444,8 @@ async def send_now(
article_type = "all" article_type = "all"
highlight_list = parse_highlights(highlights) highlight_list = parse_highlights(highlights)
days = max(1, min(days, 90)) days = max(1, min(days, 90))
smtp_cfg = get_smtp_settings(db)
contact_email = smtp_cfg["reply_to"] or smtp_cfg["from_email"]
gen = await _generate_newsletter( gen = await _generate_newsletter(
period_mode=period, period_mode=period,
@@ -443,6 +455,7 @@ async def send_now(
display=display, display=display,
editor_tip=editor_tip, editor_tip=editor_tip,
highlight_list=highlight_list, highlight_list=highlight_list,
contact_email=contact_email,
) )
send_result: dict[str, str] send_result: dict[str, str]
@@ -455,31 +468,8 @@ async def send_now(
status_code, detail = send_newsletter(db, gen["subject"], gen["raw_html"], gen["plain_text"], recipients, scheduled=False) status_code, detail = send_newsletter(db, gen["subject"], gen["raw_html"], gen["plain_text"], recipients, scheduled=False)
send_result = {"status": status_code, "message": detail} send_result = {"status": status_code, "message": detail}
context = _base_context(request, current_user, db) query = urlencode({"send_status": send_result["status"], "send_msg": send_result["message"]})
context.update( return RedirectResponse(url=f"/dashboard?{query}", status_code=status.HTTP_303_SEE_OTHER)
{
"articles": gen["articles"],
"articles_new": gen["articles_new"],
"articles_edited": gen["articles_edited"],
"raw_html": gen["raw_html"],
"outlook_html": gen["outlook_html"],
"plain_text": gen["plain_text"],
"subject": gen["subject"],
"filters": {
"days": days,
"period": period,
"article_type": article_type,
"category": category,
"display": display,
"editor_tip": editor_tip,
"highlights": highlights,
},
"send_logs": db.query(SendLog).order_by(SendLog.created_at.desc()).limit(20).all(),
"wiki_error": gen["wiki_error"],
"send_result": send_result,
}
)
return _render(request, "dashboard.html", context)
@app.post("/admin/smtp") @app.post("/admin/smtp")
@@ -492,6 +482,7 @@ def update_smtp_settings(
username: str = Form(""), username: str = Form(""),
password: str = Form(""), password: str = Form(""),
from_email: str = Form(""), from_email: str = Form(""),
reply_to: str = Form(""),
use_tls: str | None = Form(None), use_tls: str | None = Form(None),
recipients: str = Form(""), recipients: str = Form(""),
schedule_enabled: str | None = Form(None), schedule_enabled: str | None = Form(None),
@@ -513,6 +504,7 @@ def update_smtp_settings(
set_config(db, "smtp.username", username.strip()) set_config(db, "smtp.username", username.strip())
set_config(db, "smtp.password", password) set_config(db, "smtp.password", password)
set_config(db, "smtp.from_email", from_email.strip()) set_config(db, "smtp.from_email", from_email.strip())
set_config(db, "smtp.reply_to", reply_to.strip())
set_config(db, "smtp.use_tls", "true" if use_tls == "true" else "false") set_config(db, "smtp.use_tls", "true" if use_tls == "true" else "false")
set_config(db, "smtp.recipients", recipients.strip()) set_config(db, "smtp.recipients", recipients.strip())

View File

@@ -203,6 +203,7 @@ def create_plain_text(
period_label: str | None = None, period_label: str | None = None,
period: tuple[str, str] | None = None, period: tuple[str, str] | None = None,
article_type: str = "all", article_type: str = "all",
contact_email: str = "",
) -> str: ) -> str:
display = display or DEFAULT_DISPLAY display = display or DEFAULT_DISPLAY
new_articles, edited_articles = split_articles_by_type(articles) new_articles, edited_articles = split_articles_by_type(articles)
@@ -250,7 +251,7 @@ def create_plain_text(
] ]
) )
lines.extend(_plain_editor_tip(editor_tip)) lines.extend(_plain_editor_tip(editor_tip))
lines.append(_plain_footer(new_articles, edited_articles)) lines.append(_plain_footer(new_articles, edited_articles, contact_email))
return "\n".join(lines).strip() return "\n".join(lines).strip()
if show_new: if show_new:
@@ -267,7 +268,7 @@ def create_plain_text(
) )
lines.append("") lines.append("")
lines.extend(_plain_editor_tip(editor_tip)) lines.extend(_plain_editor_tip(editor_tip))
lines.append(_plain_footer(new_articles, edited_articles)) lines.append(_plain_footer(new_articles, edited_articles, contact_email))
return "\n".join(lines).strip() return "\n".join(lines).strip()
@@ -296,6 +297,7 @@ def create_html(
period: tuple[str, str] | None = None, period: tuple[str, str] | None = None,
article_type: str = "all", article_type: str = "all",
month_label: str | None = None, month_label: str | None = None,
contact_email: str = "",
) -> str: ) -> str:
display = display or DEFAULT_DISPLAY display = display or DEFAULT_DISPLAY
new_articles, edited_articles = split_articles_by_type(articles) new_articles, edited_articles = split_articles_by_type(articles)
@@ -334,6 +336,15 @@ def create_html(
f'<strong style="color:{COLOR_HEADING};">Filter:</strong> Kategorie {escape(category.strip())}</p></td></tr>' f'<strong style="color:{COLOR_HEADING};">Filter:</strong> Kategorie {escape(category.strip())}</p></td></tr>'
) )
if contact_email.strip():
ce = escape(contact_email.strip())
contact_html = (
f'Fragen oder ein Themenwunsch? Meldet euch gern beim '
f'<a href="mailto:{ce}" style="color:{COLOR_TK_ORANGE};text-decoration:underline;">Wiki-Team</a>.'
)
else:
contact_html = "Fragen oder ein Themenwunsch? Meldet euch gern beim Wiki-Team."
return f"""<!DOCTYPE html> return f"""<!DOCTYPE html>
<html lang="de" xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office"> <html lang="de" xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head> <head>
@@ -411,7 +422,7 @@ def create_html(
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"> <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
<tr> <tr>
<td style="font-family:{FONT};font-size:15px;line-height:24px;color:{COLOR_TEXT};"> <td style="font-family:{FONT};font-size:15px;line-height:24px;color:{COLOR_TEXT};">
Fragen oder ein Themenwunsch? Meldet euch gern beim Wiki-Team.<br><br> {contact_html}<br><br>
Viele Grüße<br> Viele Grüße<br>
Euer Thomas-Krenn-Wiki Team Euer Thomas-Krenn-Wiki Team
</td> </td>
@@ -679,6 +690,7 @@ def create_outlook_html(
period: tuple[str, str] | None = None, period: tuple[str, str] | None = None,
article_type: str = "all", article_type: str = "all",
month_label: str | None = None, month_label: str | None = None,
contact_email: str = "",
) -> str: ) -> str:
"""Word/Outlook-kompatibles HTML mit font-Tags und bgcolor (überlebt Einfügen in Outlook).""" """Word/Outlook-kompatibles HTML mit font-Tags und bgcolor (überlebt Einfügen in Outlook)."""
display = display or DEFAULT_DISPLAY display = display or DEFAULT_DISPLAY
@@ -726,6 +738,19 @@ def create_outlook_html(
editor_tip_row = _outlook_editor_tip(editor_tip) editor_tip_row = _outlook_editor_tip(editor_tip)
if contact_email.strip():
contact_inner = (
f'{_outlook_font("Fragen oder ein Themenwunsch? Meldet euch gern beim Wiki-Team: ", size="1")}'
f'{_outlook_link(contact_email.strip(), f"mailto:{contact_email.strip()}", color=OUTLOOK_ORANGE, bold=False)}'
)
contact_row = f"""<tr>
<td {_outlook_td_attrs(extra_style="padding:12px 0 0;")}>
{contact_inner}
</td>
</tr>"""
else:
contact_row = ""
return f"""<!DOCTYPE html> return f"""<!DOCTYPE html>
<html lang="de" xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:word"> <html lang="de" xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:word">
<head> <head>
@@ -785,6 +810,7 @@ def create_outlook_html(
{_outlook_cta(WIKI_HOME_URL, "Zum Thomas-Krenn-Wiki →")} {_outlook_cta(WIKI_HOME_URL, "Zum Thomas-Krenn-Wiki →")}
</td> </td>
</tr> </tr>
{contact_row}
<tr> <tr>
<td {_outlook_td_attrs(extra_style=f"padding:16px 0 0;border-top:1px solid {OUTLOOK_BORDER};")}> <td {_outlook_td_attrs(extra_style=f"padding:16px 0 0;border-top:1px solid {OUTLOOK_BORDER};")}>
{_outlook_font(f"Automatisch erstellter interner Newsletter · Nur für den Gebrauch bei {ORG_NAME}", color=OUTLOOK_MUTED, size="1")} {_outlook_font(f"Automatisch erstellter interner Newsletter · Nur für den Gebrauch bei {ORG_NAME}", color=OUTLOOK_MUTED, size="1")}
@@ -932,8 +958,15 @@ def _plain_item(idx: int, item: dict[str, Any], display: DisplayOptions) -> list
return lines return lines
def _plain_footer(new_articles: list[dict[str, Any]], edited_articles: list[dict[str, Any]]) -> str: def _plain_footer(
new_articles: list[dict[str, Any]],
edited_articles: list[dict[str, Any]],
contact_email: str = "",
) -> str:
total = len(new_articles) + len(edited_articles) total = len(new_articles) + len(edited_articles)
contact_lines = []
if contact_email.strip():
contact_lines = [f"Fragen oder ein Themenwunsch? E-Mail an das Wiki-Team: {contact_email.strip()}", ""]
return "\n".join( return "\n".join(
[ [
SUBLINE, SUBLINE,
@@ -945,6 +978,7 @@ def _plain_footer(new_articles: list[dict[str, Any]], edited_articles: list[dict
"", "",
f"Wiki: {WIKI_HOME_URL}", f"Wiki: {WIKI_HOME_URL}",
"", "",
*contact_lines,
SUBLINE, SUBLINE,
"Dieser Newsletter wurde automatisch erstellt und ist nur für den internen", "Dieser Newsletter wurde automatisch erstellt und ist nur für den internen",
f"Gebrauch bei {ORG_NAME} bestimmt.", f"Gebrauch bei {ORG_NAME} bestimmt.",

View File

@@ -18,6 +18,7 @@ def get_smtp_settings(db: Session) -> dict[str, str]:
"username": get_config(db, "smtp.username", ""), "username": get_config(db, "smtp.username", ""),
"password": get_config(db, "smtp.password", ""), "password": get_config(db, "smtp.password", ""),
"from_email": get_config(db, "smtp.from_email", ""), "from_email": get_config(db, "smtp.from_email", ""),
"reply_to": get_config(db, "smtp.reply_to", ""),
"use_tls": get_config(db, "smtp.use_tls", "true"), "use_tls": get_config(db, "smtp.use_tls", "true"),
"recipients": get_config(db, "smtp.recipients", ""), "recipients": get_config(db, "smtp.recipients", ""),
# Zeitplan # Zeitplan
@@ -91,6 +92,8 @@ def send_newsletter(
msg["Subject"] = subject msg["Subject"] = subject
msg["From"] = settings["from_email"] msg["From"] = settings["from_email"]
msg["To"] = ", ".join(recipients) msg["To"] = ", ".join(recipients)
if settings.get("reply_to"):
msg["Reply-To"] = settings["reply_to"]
try: try:
port = int(settings["port"]) port = int(settings["port"])

View File

@@ -253,9 +253,14 @@
<input type="password" name="password" value="{{ smtp.password }}"> <input type="password" name="password" value="{{ smtp.password }}">
</label> </label>
</div> </div>
<div class="form-row">
<label>Absender E-Mail <label>Absender E-Mail
<input type="email" name="from_email" value="{{ smtp.from_email }}"> <input type="email" name="from_email" value="{{ smtp.from_email }}">
</label> </label>
<label>Antwort-Adresse (Reply-To)
<input type="email" name="reply_to" value="{{ smtp.reply_to }}" placeholder="wiki-team@firma.de">
</label>
</div>
<label class="checkbox"> <label class="checkbox">
<input type="checkbox" name="use_tls" value="true" {% if smtp.use_tls == "true" %}checked{% endif %}> <input type="checkbox" name="use_tls" value="true" {% if smtp.use_tls == "true" %}checked{% endif %}>
STARTTLS verwenden STARTTLS verwenden