mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-08-25 06:55:17 +02:00
Add configurable e-mail delivery: direct SMTP send or IMAP drafts
Mahnungs- and SEPA-info-mails could previously only be saved as a local .eml file that still had to be manually imported into Thunderbird. Add a per-store "E-Mail-Versand" configuration (Optionen -> E-Mail-Versand, stored in repository.json alongside the rest of the club's settings, since different stores may use different mailboxes) with four delivery modes: - "Lokal speichern": today's behaviour, unchanged default for existing stores. - "Direkt versenden": sends via SMTP. - "Als Entwurf ablegen": IMAP APPENDs into a configurable drafts folder, so it shows up live in whatever mail client is already watching that account. - "Jedes Mal fragen": prompts once per generation action (not per e-mail -- a SEPA batch can cover dozens of members) with Senden/Entwürfe/Abbrechen. New ccma.services.mail_delivery module (smtplib/imaplib, no new dependency) opens one authenticated connection per batch and reuses it across all messages instead of reconnecting per recipient. Both "Verbindung testen" buttons in Options exercise the same connection path used for real delivery. The archived per-member copy of every generated e-mail is unaffected and still always written regardless of delivery mode. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
b7554478ac
commit
6b0da82b45
@@ -1,4 +1,5 @@
|
||||
import xml.etree.ElementTree as ET
|
||||
from contextlib import contextmanager
|
||||
from datetime import UTC, date, datetime
|
||||
from decimal import Decimal
|
||||
from email.parser import BytesParser
|
||||
@@ -214,6 +215,7 @@ def test_generated_mail_is_exported_archived_and_logged(tmp_path):
|
||||
repository,
|
||||
debits,
|
||||
collection_date=date(2026, 8, 3),
|
||||
delivery_mode="local",
|
||||
output_directory=tmp_path / "mail-export",
|
||||
sender_name="Verwaltung C3MA",
|
||||
sender_email="verwaltung@example.org",
|
||||
@@ -227,3 +229,64 @@ def test_generated_mail_is_exported_archived_and_logged(tmp_path):
|
||||
event = repository.get_events(member.member_id)[-1]
|
||||
assert event.event_type == "sepa_notification_generated"
|
||||
assert event.references["document"].startswith("documents/SEPA/")
|
||||
|
||||
|
||||
def test_generated_mail_send_mode_reuses_one_smtp_connection_for_batch(tmp_path, monkeypatch):
|
||||
import ccma.services.sepa_mail as sepa_mail_module
|
||||
|
||||
repository, member = _repository(tmp_path)
|
||||
organization = repository.get_configuration()["organization"]
|
||||
organization.update(
|
||||
{
|
||||
"name": "Chaos Computer Club Mannheim e.V.",
|
||||
"email": "verwaltung@example.org",
|
||||
"iban": "DE89370400440532013000",
|
||||
"bic": "COBADEFFXXX",
|
||||
"creditor_id": "DE98ZZZ09999999999",
|
||||
}
|
||||
)
|
||||
repository.save_organization(organization)
|
||||
repository.save_email_settings(
|
||||
delivery_mode="send",
|
||||
smtp_host="mail.example.org",
|
||||
smtp_port=587,
|
||||
smtp_security="none",
|
||||
smtp_username="",
|
||||
smtp_password="",
|
||||
imap_host="",
|
||||
imap_port=993,
|
||||
imap_security="ssl",
|
||||
imap_username="",
|
||||
imap_password="",
|
||||
imap_drafts_folder="",
|
||||
)
|
||||
debits, _warnings = pending_direct_debits(repository, due_until=date(2026, 12, 31))
|
||||
|
||||
connections_opened = []
|
||||
|
||||
@contextmanager
|
||||
def fake_smtp_session(settings):
|
||||
connections_opened.append(settings)
|
||||
yield object()
|
||||
|
||||
sent = []
|
||||
monkeypatch.setattr(sepa_mail_module, "smtp_session", fake_smtp_session)
|
||||
monkeypatch.setattr(
|
||||
sepa_mail_module, "send_via_smtp", lambda client, content: sent.append(content)
|
||||
)
|
||||
|
||||
generated, warnings = generate_debit_mails(
|
||||
repository,
|
||||
debits,
|
||||
collection_date=date(2026, 8, 3),
|
||||
delivery_mode="send",
|
||||
sender_name="Verwaltung C3MA",
|
||||
sender_email="verwaltung@example.org",
|
||||
signature="Der Vorstand",
|
||||
)
|
||||
|
||||
assert warnings == []
|
||||
assert generated[0].export_path is None
|
||||
assert generated[0].archive_path.is_file()
|
||||
assert len(connections_opened) == 1, "one connection should be reused for the whole batch"
|
||||
assert len(sent) == len(debits)
|
||||
|
||||
Reference in New Issue
Block a user