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, template_spec, validate_block_markers, ) 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, expect_transfer: bool = False, ) -> str: """Ready-made payment paragraph: members with an active mandate are told the money is collected, everyone else gets the club's bank details. `expect_transfer` overrides that for a claim the mandate no longer covers -- a dunned claim is expected as a transfer even though the member still has a mandate.""" organization = organization if isinstance(organization, dict) else {} if member.mandate_active and not expect_transfer: 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: template = repository.get_mail_template(key) try: # The templates are plain files in the store and may have been edited outside # CCMA, so the structural check runs again here: refusing to send beats # sending a mail with "{{#claims}}" in its text. validate_block_markers(key, template.subject, template.body) return render_mail_template(template, values, repeats) except MailTemplateError as exc: raise RepositoryError( f"Die E-Mail-Vorlage „{template_spec(key).label}“ ist fehlerhaft: {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()