mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-09-06 03:20:49 +02:00
Render every outgoing e-mail from an editable text template
The wording of the dunning and SEPA pre-notification mails was hard-coded in
Python, so adjusting a single sentence required a new release. Both texts now
live in plain text templates that ship as defaults, are copied into the store's
templates/mail/ directory on first start and can be edited there or under
Optionen -> E-Mail-Vorlagen; an existing file is never overwritten and a deleted
one is restored from the shipped default.
A template carries its subject in the first line and the body after a blank
line. Placeholders use the same {{ ... }} syntax as the document templates and
share their member/organization values, so a name means the same thing in a
letter and in the mail that carries it. Unknown placeholders are rejected while
editing instead of during a send run, a line holding nothing but placeholders
that render empty is dropped, and {{#claims}} ... {{/claims}} repeats per entry.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e3807bf7dc
commit
d6cd58a788
@@ -0,0 +1,94 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from email.message import EmailMessage
|
||||
from email.policy import SMTP
|
||||
from email.utils import format_datetime, make_msgid
|
||||
|
||||
from ccma.domain.mail_templates import MailTemplate, MailTemplateError, render_mail_template
|
||||
from ccma.domain.models import Member
|
||||
from ccma.domain.placeholders import base_placeholder_values
|
||||
from ccma.storage.repository import MemberRepository, RepositoryError
|
||||
|
||||
|
||||
def template_values(
|
||||
member: Member,
|
||||
organization: dict | None,
|
||||
*,
|
||||
signature: str,
|
||||
created_at: datetime | None = None,
|
||||
) -> dict[str, str]:
|
||||
values = base_placeholder_values(member, organization, created_at=created_at)
|
||||
# A mail always opens with a salutation, so an empty first name would read as
|
||||
# "Hallo ," -- every other placeholder may legitimately render empty.
|
||||
values["member.first_name"] = member.first_name.strip() or "Mitglied"
|
||||
values["signature"] = signature.strip()
|
||||
return values
|
||||
|
||||
|
||||
def payment_instructions(
|
||||
member: Member,
|
||||
organization: dict | None,
|
||||
*,
|
||||
due_date: str,
|
||||
reference: str,
|
||||
) -> str:
|
||||
"""Ready-made payment paragraph: members with an active mandate are told the
|
||||
money is collected, everyone else gets the club's bank details."""
|
||||
organization = organization if isinstance(organization, dict) else {}
|
||||
if member.mandate_active:
|
||||
mandate = member.mandate_reference.strip()
|
||||
mandate_hint = f" (Mandatsreferenz {mandate})" if mandate else ""
|
||||
return (
|
||||
f"Du hast uns ein SEPA-Lastschriftmandat erteilt{mandate_hint} – wir ziehen den "
|
||||
"Betrag fristgerecht von deinem Konto ein. Du musst also nichts weiter tun; "
|
||||
"vor dem Einzug informieren wir dich rechtzeitig per E-Mail."
|
||||
)
|
||||
deadline = f" bis zum {due_date}" if due_date else ""
|
||||
lines = [f"Bitte überweise den Betrag{deadline} auf unser Vereinskonto:", ""]
|
||||
lines.append(f"IBAN: {str(organization.get('iban', '')).strip()}")
|
||||
bic = str(organization.get("bic", "")).strip()
|
||||
if bic:
|
||||
lines.append(f"BIC: {bic}")
|
||||
if reference.strip():
|
||||
lines.append(f"Verwendungszweck: {reference.strip()}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def render_template(
|
||||
repository: MemberRepository,
|
||||
key: str,
|
||||
values: dict[str, str],
|
||||
repeats: dict[str, list[dict[str, str]]] | None = None,
|
||||
) -> MailTemplate:
|
||||
try:
|
||||
return render_mail_template(repository.get_mail_template(key), values, repeats)
|
||||
except MailTemplateError as exc:
|
||||
raise RepositoryError(str(exc)) from exc
|
||||
|
||||
|
||||
def compose_mail(
|
||||
*,
|
||||
recipient: str,
|
||||
subject: str,
|
||||
body: str,
|
||||
sender_name: str,
|
||||
sender_email: str,
|
||||
created_at: datetime | None = None,
|
||||
) -> bytes:
|
||||
if not recipient.strip():
|
||||
raise RepositoryError("Für das Mitglied ist keine E-Mail-Adresse hinterlegt.")
|
||||
if not sender_email.strip() or "@" not in sender_email:
|
||||
raise RepositoryError("Für den Versand ist eine gültige Absenderadresse erforderlich.")
|
||||
timestamp = created_at or datetime.now().astimezone()
|
||||
message = EmailMessage(policy=SMTP)
|
||||
message["Message-ID"] = make_msgid(domain=sender_email.rsplit("@", 1)[-1])
|
||||
message["Date"] = format_datetime(timestamp)
|
||||
message["From"] = f"{sender_name.strip()} <{sender_email.strip()}>"
|
||||
message["To"] = recipient.strip()
|
||||
message["Subject"] = subject.strip()
|
||||
message["X-Mozilla-Draft-Info"] = (
|
||||
"internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0; attachmentreminder=0"
|
||||
)
|
||||
message.set_content(body.strip() + "\n", charset="utf-8")
|
||||
return message.as_bytes()
|
||||
Reference in New Issue
Block a user