feat: SSH-Key-Auth als primäre Methode, Bitwarden als Fallback

- Neuer Step I (ssh_key_versuch.py): liest SSH-Keys aus DB, testet
  Verbindung per paramiko; erfolgreiche Server landen in server_creds,
  fehlgeschlagene in needs_bitwarden
- Step G (Bitwarden) ist jetzt No-Op wenn alle Server per Key OK
- paramiko.DSSKey in allen 4 Dateien entfernt (nicht in paramiko 4.0)
- failure_module (flow_fehler_handler.py): sendet bei jedem Flow-Fehler
  eine Nextcloud-Talk-Nachricht und bereinigt DB/Session
- Bitwarden-Step überspringt fehlgeschlagene Server statt abzubrechen
- testpause.py als wiederverwendbarer Debug-Helper behalten

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Sebastian Serfling
2026-04-29 21:50:17 +02:00
parent 7d6bec2b4a
commit 4e19c41cd2
19 changed files with 642 additions and 30 deletions
+6
View File
@@ -0,0 +1,6 @@
summary: null
display_name: mail_to_talk
extra_perms:
serfling@itdata-gera.de: true
owners:
- serfling@itdata-gera.de
+8
View File
@@ -0,0 +1,8 @@
description: IMAP Zugangsdaten fuer den Mail-zu-Talk Flow
resource_type: imap
value:
host: mail.stines.de
port: 993
user: sebastianserfling@stines.de
password: c6tzJBxDtNEU84ZAWY39eG8RUM5XR4UyfrDesfgCekEbFhkHDkbn
mailbox: INBOX
@@ -0,0 +1,8 @@
description: IMAP Zugangsdaten fuer das zweite Postfach (itdata-gera.de)
resource_type: imap
value:
host: mail.itdata-gera.de
port: 993
user: serfling@itdata-gera.de
password: ''
mailbox: INBOX
@@ -0,0 +1,85 @@
import imaplib
import email
from email.header import decode_header
from typing import TypedDict, Optional, List
from datetime import datetime
import email.utils
class imap(TypedDict):
host: str
port: int
user: str
password: str
mailbox: Optional[str]
def decode_str(value) -> str:
if value is None:
return ""
parts = decode_header(value)
result = []
for part, charset in parts:
if isinstance(part, bytes):
result.append(part.decode(charset or "utf-8", errors="replace"))
else:
result.append(part)
return "".join(result)
def main(imap_config: imap) -> List[dict]:
host = imap_config["host"]
port = imap_config["port"]
user = imap_config["user"]
password = imap_config["password"]
mailbox = imap_config.get("mailbox") or "INBOX"
if port == 993:
client = imaplib.IMAP4_SSL(host, port)
else:
client = imaplib.IMAP4(host, port)
client.login(user, password)
client.select(mailbox, readonly=False)
# Nur ungelesene E-Mails suchen
status, data = client.search(None, "UNSEEN")
if status != "OK" or not data[0]:
client.logout()
return []
uids = data[0].split()
emails = []
for uid in uids:
status, msg_data = client.fetch(uid, "(BODY.PEEK[HEADER.FIELDS (FROM SUBJECT DATE)])")
if status != "OK":
continue
raw = msg_data[0][1]
msg = email.message_from_bytes(raw)
subject = decode_str(msg.get("Subject", "(Kein Betreff)"))
from_raw = msg.get("From", "Unbekannt")
from_addr = decode_str(from_raw)
date_raw = msg.get("Date", "")
try:
parsed_date = email.utils.parsedate_to_datetime(date_raw)
date_str = parsed_date.strftime("%d.%m.%Y %H:%M:%S")
except Exception:
date_str = date_raw or "Unbekanntes Datum"
# Als gelesen markieren
client.store(uid, "+FLAGS", "\\Seen")
emails.append({
"subject": subject,
"from": from_addr,
"date": date_str,
"uid": int(uid),
"account": user,
})
client.logout()
return emails
@@ -0,0 +1,77 @@
summary: E-Mails zu Nextcloud Talk
description: >
Liest ungelesene E-Mails von zwei IMAP-Konten und sendet Benachrichtigungen
an einen Nextcloud Talk Raum. E-Mails werden als gelesen markiert.
schema:
$schema: "http://json-schema.org/draft-07/schema"
type: object
properties:
imap_config_1:
type: object
format: resource-imap
description: "IMAP Konto 1 (Ressource: f/mail_to_talk/imap_config)"
imap_config_2:
type: object
format: resource-imap
description: "IMAP Konto 2 (Ressource: f/mail_to_talk/imap_config_2)"
nextcloud_config:
type: object
format: resource-nextcloud
description: "Nextcloud Zugangsdaten (Ressource: f/mail_to_talk/nextcloud_talk_config)"
required:
- imap_config_1
- imap_config_2
- nextcloud_config
value:
modules:
- id: fetch_emails_1
summary: Ungelesene E-Mails Konto 1 abrufen
value:
type: rawscript
language: python3
content: "!inline fetch_emails.py"
input_transforms:
imap_config:
type: javascript
expr: flow_input.imap_config_1
stop_after_if:
expr: "false"
skip_if_stopped: false
- id: fetch_emails_2
summary: Ungelesene E-Mails Konto 2 abrufen
value:
type: rawscript
language: python3
content: "!inline fetch_emails.py"
input_transforms:
imap_config:
type: javascript
expr: flow_input.imap_config_2
stop_after_if:
expr: "false"
skip_if_stopped: false
- id: send_to_talk
summary: Jede E-Mail an Nextcloud Talk senden
value:
type: forloopflow
iterator:
type: javascript
expr: "[...results.fetch_emails_1, ...results.fetch_emails_2]"
skip_failures: false
parallel: false
modules:
- id: send_message
summary: Nachricht an Nextcloud Talk senden
value:
type: rawscript
language: python3
content: "!inline send_message.py"
input_transforms:
email:
type: javascript
expr: flow_input.iter.value
nextcloud_config:
type: javascript
expr: flow_input.nextcloud_config
@@ -0,0 +1,49 @@
import urllib.request
import urllib.parse
import json
import base64
from typing import TypedDict
class nextcloud(TypedDict):
baseUrl: str
userId: str
token: str
TALK_TOKEN = "6bdts22w"
def main(email: dict, nextcloud_config: nextcloud) -> dict:
base_url = nextcloud_config["baseUrl"].rstrip("/")
user_id = nextcloud_config["userId"]
token = nextcloud_config["token"]
message = (
f"Neue E-Mail an: {email.get('account', '')}\n"
f"Von: {email['from']}\n"
f"Betreff: {email['subject']}\n"
f"Datum: {email['date']}"
)
url = f"{base_url}/ocs/v2.php/apps/spreed/api/v1/chat/{TALK_TOKEN}"
credentials = base64.b64encode(f"{user_id}:{token}".encode()).decode()
body = json.dumps({"message": message, "replyTo": 0}).encode("utf-8")
req = urllib.request.Request(
url,
data=body,
headers={
"OCS-APIRequest": "true",
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": f"Basic {credentials}",
},
method="POST",
)
with urllib.request.urlopen(req) as resp:
status = resp.status
return {"success": True, "status": status}
@@ -0,0 +1,6 @@
description: Nextcloud Zugangsdaten fuer den Mail-zu-Talk Flow
resource_type: nextcloud
value:
baseUrl: https://cloudstorage.stines.de
userId: reporting
token: "5xw#HLH5kbMDbxNUUVA6iQcstytm4Ss4g9iGy7ZoLCTDTku6GPcXNHgRfSFgci9R"