sync: Windmill-State übernehmen + neue Reporting-Flows

- Dateien nach Windmill-Naming-Konvention umbenannt (ssh-key_aus_db_testen,
  flow-fehler_per_nextcloud_talk_melden, bitwarden_(fallback))
- testpause-Schritt aus flow.yaml entfernt (Debugging abgeschlossen)
- Neue Flows: f/Reporting/exchange_logins, f/Reporting/run_sql_events
- mail_to_talk: Dateinamen nach Windmill-Konvention synchronisiert

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Sebastian Serfling
2026-05-07 13:13:35 +02:00
parent 4e19c41cd2
commit d22ef502ed
40 changed files with 1077 additions and 89 deletions
+41 -37
View File
@@ -1,73 +1,49 @@
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
Liest ungelesene E-Mails von zwei IMAP-Konten und sendet Benachrichtigungen an
einen Nextcloud Talk Raum. E-Mails werden als gelesen markiert.
value:
modules:
- id: fetch_emails_1
summary: Ungelesene E-Mails Konto 1 abrufen
value:
type: rawscript
language: python3
content: "!inline fetch_emails.py"
content: '!inline ungelesene_e-mails_konto_1_abrufen.py'
input_transforms:
imap_config:
type: javascript
expr: flow_input.imap_config_1
lock: '!inline ungelesene_e-mails_konto_1_abrufen.lock'
language: python3
stop_after_if:
expr: "false"
error_message: null
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"
content: '!inline ungelesene_e-mails_konto_2_abrufen.py'
input_transforms:
imap_config:
type: javascript
expr: flow_input.imap_config_2
lock: '!inline ungelesene_e-mails_konto_2_abrufen.lock'
language: python3
stop_after_if:
expr: "false"
error_message: null
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"
content: '!inline nachricht_an_nextcloud_talk_senden.py'
input_transforms:
email:
type: javascript
@@ -75,3 +51,31 @@ value:
nextcloud_config:
type: javascript
expr: flow_input.nextcloud_config
lock: '!inline nachricht_an_nextcloud_talk_senden.lock'
language: python3
iterator:
type: javascript
expr: '[...results.fetch_emails_1, ...results.fetch_emails_2]'
parallel: false
skip_failures: false
schema:
$schema: http://json-schema.org/draft-07/schema
type: object
properties:
imap_config_1:
type: object
description: 'IMAP Konto 1 (Ressource: f/mail_to_talk/imap_config)'
format: resource-imap
imap_config_2:
type: object
description: 'IMAP Konto 2 (Ressource: f/mail_to_talk/imap_config_2)'
format: resource-imap
nextcloud_config:
type: object
description: 'Nextcloud Zugangsdaten (Ressource:
f/mail_to_talk/nextcloud_talk_config)'
format: resource-nextcloud
required:
- imap_config_1
- imap_config_2
- nextcloud_config
@@ -0,0 +1 @@
# py: 3.12
@@ -0,0 +1 @@
# py: 3.12
@@ -0,0 +1 @@
# py: 3.12
@@ -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