mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-09-06 03:20:49 +02:00
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>
95 lines
3.5 KiB
Python
95 lines
3.5 KiB
Python
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()
|