feat: Postfaecher einrichten und Zahlungs-Mails abholen (Etappe B, Teil 2)
- Verschluesselung fuer Zugangsdaten: Ein IMAP-Passwort laesst sich nicht hashen, Certain muss es dem Mailserver vorlegen. Es liegt deshalb mit Fernet verschluesselt in der Datenbank, der Schluessel wird per HKDF aus session_secret abgeleitet und nirgends gespeichert. Wird das Geheimnis getauscht, meldet Certain das verstaendlich statt mit einem Stacktrace. - IMAP-Abruf ueber imaplib/email (Standardbibliothek). Ordnerliste, Verbindungstest mit klarer Fehlermeldung, Absender-Zuordnung auf Konten, danach als gelesen markieren oder verschieben. - Wiedererkennung ueber die Message-ID statt der UID – die vergibt der Server beim Verschieben neu. - Nichts wird stillschweigend gebucht: Eine Mail wird zum Eingangs-Eintrag. Fehlt das Zielkonto oder ist ein Feld unsicher, gilt sie als unklar. - Hintergrund-Thread beim Start, nach dem Muster des EZB-Kursabrufs: ein langsames Postfach darf den Dienst nie blockieren. Getestet gegen einen IMAP-Doppelgaenger, damit die Suite ohne Netz laeuft. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -684,3 +684,56 @@ class InboxItem(Base):
|
||||
__table_args__ = (
|
||||
UniqueConstraint("owner_id", "source", "external_id", name="uq_inbox_owner_source_external"),
|
||||
)
|
||||
|
||||
|
||||
class MailSource(Base):
|
||||
"""Ein Postfach, aus dem Certain Zahlungs-Mails abholt."""
|
||||
|
||||
__tablename__ = "mail_sources"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
owner_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
name: Mapped[str] = mapped_column(String(80), nullable=False, default="")
|
||||
host: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
port: Mapped[int] = mapped_column(Integer, nullable=False, default=993)
|
||||
use_ssl: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
||||
username: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
# Verschlüsselt, nicht gehasht: Certain muss es dem Mailserver vorlegen.
|
||||
password_enc: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
folder: Mapped[str] = mapped_column(String(255), nullable=False, default="INBOX")
|
||||
# Nach dem Auswerten: dorthin verschieben (leer = nicht verschieben).
|
||||
done_folder: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||
mark_seen: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
||||
poll_minutes: Mapped[int] = mapped_column(Integer, nullable=False, default=15)
|
||||
last_poll_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
last_error: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now)
|
||||
|
||||
|
||||
class MailSender(Base):
|
||||
"""Welcher Absender auf welches Konto bucht.
|
||||
|
||||
Ohne Zuordnung weiss Certain nicht, wohin eine erkannte Zahlung gehört –
|
||||
dieselbe Mailadresse kann je nach Konto etwas anderes bedeuten.
|
||||
"""
|
||||
|
||||
__tablename__ = "mail_senders"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
source_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("mail_sources.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
owner_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
name: Mapped[str] = mapped_column(String(80), nullable=False, default="")
|
||||
# Teilstrings, gegen Absender und Betreff geprüft. Leer = jede Mail.
|
||||
from_contains: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||
subject_contains: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||
account_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("accounts.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now)
|
||||
|
||||
Reference in New Issue
Block a user