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:
@@ -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}
|
||||
Reference in New Issue
Block a user