4780c18d70
Claude has done a lot of work, but there are still some errors which cause the mails to be distributed to wrong mail boxes. I am now going to let codex and claude compete. They will have their own branches and merge to main as soon as we are somewhere where we can switch AI.
191 lines
6.0 KiB
Python
191 lines
6.0 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Upload en activeer de Sieve-mailrule op australius.nl via ManageSieve (RFC 5804).
|
|
Poort: 4190 met STARTTLS.
|
|
|
|
Gebruik: python3 upload_sieve.py
|
|
"""
|
|
|
|
import socket
|
|
import ssl
|
|
import sys
|
|
import base64
|
|
import config
|
|
from pathlib import Path
|
|
SCRIPT_NAME = "mailrules"
|
|
SCRIPT_FILE = Path(__file__).parent / "mailrules.sieve"
|
|
|
|
# Als fileinto-paden "INBOX." als prefix nodig hebben op deze server:
|
|
# Zet SIEVE_PREFIX op "INBOX." en het script past de paden automatisch aan.
|
|
# Zet op "" als de server paden zonder prefix accepteert (meest gangbaar).
|
|
SIEVE_PREFIX = ""
|
|
|
|
|
|
class ManageSieveClient:
|
|
"""Minimale ManageSieve-client (RFC 5804)."""
|
|
|
|
def __init__(self, host: str, port: int):
|
|
self._sock = None
|
|
self._host = host
|
|
self._port = port
|
|
|
|
def connect(self):
|
|
raw = socket.create_connection((self._host, self._port), timeout=15)
|
|
self._sock = raw
|
|
greeting = self._readline()
|
|
if not greeting.startswith("OK"):
|
|
raise ConnectionError(f"Onverwachte greeting: {greeting!r}")
|
|
# Lees eventuele capability-regels na de greeting
|
|
self._read_capabilities()
|
|
|
|
def starttls(self):
|
|
self._send("STARTTLS")
|
|
resp = self._readline()
|
|
if not resp.startswith("OK"):
|
|
raise ConnectionError(f"STARTTLS geweigerd: {resp!r}")
|
|
context = ssl.create_default_context()
|
|
self._sock = context.wrap_socket(self._sock, server_hostname=self._host)
|
|
# Nieuwe capabilities na TLS
|
|
self._read_capabilities()
|
|
|
|
def authenticate(self, username: str, password: str):
|
|
creds = base64.b64encode(f"\x00{username}\x00{password}".encode()).decode()
|
|
self._send(f'AUTHENTICATE "PLAIN" "{creds}"')
|
|
resp = self._readline()
|
|
if not resp.startswith("OK"):
|
|
raise PermissionError(f"Authenticatie mislukt: {resp!r}")
|
|
|
|
def put_script(self, name: str, content: str):
|
|
encoded = content.encode("utf-8")
|
|
self._send(f'PUTSCRIPT "{name}" {{{len(encoded)}+}}')
|
|
self._sock.sendall(encoded)
|
|
resp = self._readline()
|
|
if not resp.startswith("OK"):
|
|
raise RuntimeError(f"PUTSCRIPT mislukt: {resp!r}")
|
|
|
|
def set_active(self, name: str):
|
|
self._send(f'SETACTIVE "{name}"')
|
|
resp = self._readline()
|
|
if not resp.startswith("OK"):
|
|
raise RuntimeError(f"SETACTIVE mislukt: {resp!r}")
|
|
|
|
def list_scripts(self) -> list[str]:
|
|
self._send("LISTSCRIPTS")
|
|
scripts = []
|
|
while True:
|
|
line = self._readline()
|
|
if line.startswith("OK"):
|
|
break
|
|
scripts.append(line)
|
|
return scripts
|
|
|
|
def logout(self):
|
|
try:
|
|
self._send("LOGOUT")
|
|
except Exception:
|
|
pass
|
|
try:
|
|
self._sock.close()
|
|
except Exception:
|
|
pass
|
|
|
|
def _send(self, cmd: str):
|
|
self._sock.sendall((cmd + "\r\n").encode())
|
|
|
|
def _readline(self) -> str:
|
|
buf = b""
|
|
while not buf.endswith(b"\n"):
|
|
chunk = self._sock.recv(1)
|
|
if not chunk:
|
|
break
|
|
buf += chunk
|
|
return buf.decode("utf-8", errors="replace").strip()
|
|
|
|
def _read_capabilities(self):
|
|
"""Lees en negeer capability-regels tot een lege regel of einde."""
|
|
# Capabilities eindigen met een lege regel of worden gevolgd door authenticatie
|
|
# We lezen tot we iets tegenkomen dat niet op een capability lijkt
|
|
# (dit is een vereenvoudiging; volledige RFC 5804 parsing is complexer)
|
|
pass
|
|
|
|
|
|
def apply_prefix(script: str, prefix: str) -> str:
|
|
"""Voeg SIEVE_PREFIX toe aan alle fileinto-paden als dat nodig is."""
|
|
if not prefix:
|
|
return script
|
|
import re
|
|
def add_prefix(m):
|
|
path = m.group(1)
|
|
if path.startswith(prefix):
|
|
return m.group(0) # al aanwezig
|
|
return f'fileinto "{prefix}{path}"'
|
|
return re.sub(r'fileinto "([^"]+)"', add_prefix, script)
|
|
|
|
|
|
def run():
|
|
if not SCRIPT_FILE.exists():
|
|
print(f"Script niet gevonden: {SCRIPT_FILE}")
|
|
sys.exit(1)
|
|
|
|
script_content = SCRIPT_FILE.read_text(encoding="utf-8")
|
|
script_content = apply_prefix(script_content, SIEVE_PREFIX)
|
|
|
|
cfg = config.load()
|
|
|
|
print(f"Script: {SCRIPT_FILE.name} ({len(script_content)} bytes)")
|
|
print(f"Server: {cfg.sieve_host}:{cfg.sieve_port}")
|
|
print(f"Account:{cfg.username}")
|
|
print(f"Naam: {SCRIPT_NAME}\n")
|
|
|
|
client = ManageSieveClient(cfg.sieve_host, cfg.sieve_port)
|
|
|
|
print("Verbinden ...")
|
|
try:
|
|
client.connect()
|
|
print(" STARTTLS ...")
|
|
client.starttls()
|
|
print(" Authenticeren ...")
|
|
client.authenticate(cfg.username, cfg.password)
|
|
except Exception as e:
|
|
print(f"Verbinding mislukt: {e}")
|
|
sys.exit(1)
|
|
|
|
# Toon bestaande scripts
|
|
scripts = client.list_scripts()
|
|
if scripts:
|
|
print(f"\nBestaande scripts op server:")
|
|
for s in scripts:
|
|
print(f" {s}")
|
|
else:
|
|
print("\nGeen bestaande scripts.")
|
|
|
|
# Bevestiging vragen
|
|
print(f"\nScript '{SCRIPT_NAME}' wordt geüpload en geactiveerd.")
|
|
bevestig = input("Typ JA om door te gaan: ").strip()
|
|
if bevestig != "JA":
|
|
print("Afgebroken.")
|
|
client.logout()
|
|
sys.exit(0)
|
|
|
|
print("\nUploaden ...")
|
|
try:
|
|
client.put_script(SCRIPT_NAME, script_content)
|
|
print(f" ✓ {SCRIPT_NAME} geüpload")
|
|
client.set_active(SCRIPT_NAME)
|
|
print(f" ✓ {SCRIPT_NAME} geactiveerd")
|
|
except Exception as e:
|
|
print(f" ✗ Mislukt: {e}")
|
|
client.logout()
|
|
sys.exit(1)
|
|
|
|
client.logout()
|
|
print("\nKlaar. Nieuwe inkomende mail wordt nu automatisch gesorteerd.")
|
|
print(f"\nAls mail niet aankomt in de verwachte mappen:")
|
|
print(f" 1. Controleer of alle mappen bestaan (maak_mappen.py)")
|
|
print(f" 2. Probeer SIEVE_PREFIX = \"INBOX.\" in dit script")
|
|
print(f" 3. Controleer server-logs via webhosting-paneel")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
run()
|