Harden mail move audit logging

The prompts used to arrive at this change since the previous commit

Read restart_prompt.md first, then continue from there.

Any special observations that may be relevant for version management for this version. Be brief.

No mailbox-affecting scripts were run by Codex. A generated dry-run transcript appeared locally as verplaats_bestaand.log and is now ignored via *.log.
This commit is contained in:
2026-07-04 16:33:58 +02:00
parent 33005a7cde
commit f44c363025
4 changed files with 197 additions and 71 deletions
+78 -12
View File
@@ -25,6 +25,7 @@ import sys
import json
import config
from pathlib import Path
from collections import Counter
from imap_utils import list_folders, quote_mailbox
from mail_routes import PREFIX, is_source_folder, quarter_folder, route_from_subject
@@ -32,6 +33,7 @@ from mail_routes import PREFIX, is_source_folder, quarter_folder, route_from_sub
LOG_FILE = Path(__file__).parent / "verplaats_log.json"
DRY_RUN = "--uitvoeren" not in sys.argv
AUDIT = "--audit" in sys.argv
# Mappen die in één keer worden hernoemd/verplaatst (oud → nieuw).
@@ -65,20 +67,56 @@ def list_source_folders(mail) -> list[str]:
return [folder for folder in list_folders(mail) if is_source_folder(folder)]
def move_message(mail, uid: bytes, src: str, dst_full: str, dry: bool) -> bool:
def log_failure(
log: dict,
stap: str,
src: str,
uid: bytes | str,
dst: str | None,
actie: str,
status: str | None,
response,
):
uid_str = uid.decode(errors="replace") if isinstance(uid, bytes) else str(uid)
log.setdefault("fouten", []).append({
"stap": stap,
"bron": src,
"uid": uid_str,
"doel": dst,
"actie": actie,
"status": status,
"response": repr(response),
})
def print_audit(title: str, by_source: Counter, by_destination: Counter):
print(f"\n Audit: {title}")
print(" Per bronmap:")
for folder, count in by_source.most_common():
print(f" {count:5d} {folder}")
print(" Per doelmap:")
for folder, count in by_destination.most_common():
print(f" {count:5d} {folder}")
def move_message(mail, uid: bytes, src: str, dst_full: str, dry: bool):
"""Kopieer bericht naar dst en markeer als verwijderd in src."""
if dry:
return True
return True, None
try:
mail.select(quote_mailbox(src))
res, _ = mail.uid("COPY", uid, quote_mailbox(dst_full))
select_res, select_data = mail.select(quote_mailbox(src))
if select_res != "OK":
return False, ("select_source", select_res, select_data)
res, copy_data = mail.uid("COPY", uid, quote_mailbox(dst_full))
if res != "OK":
return False
store_res, _ = mail.uid("STORE", uid, "+FLAGS", "\\Deleted")
return store_res == "OK"
return False, ("copy", res, copy_data)
store_res, store_data = mail.uid("STORE", uid, "+FLAGS", "\\Deleted")
if store_res != "OK":
return False, ("store_deleted", store_res, store_data)
return True, None
except Exception as e:
print(f" FOUT bij verplaatsen: {e}")
return False
return False, ("exception", None, str(e))
# ── stap 1: vaste mapverplaatsingen ─────────────────────────────────────────
@@ -130,6 +168,8 @@ def stap2_bronmappen_routing(mail, log: dict, dry: bool):
bron_mappen = list_source_folders(mail)
print(f" {len(bron_mappen)} bronmappen")
audit_by_source = Counter()
audit_by_destination = Counter()
totaal_verplaatst = 0
totaal_gepland = 0
totaal_geen_match = 0
@@ -171,6 +211,7 @@ def stap2_bronmappen_routing(mail, log: dict, dry: bool):
status, data = mail.uid("FETCH", uid, "(BODY.PEEK[HEADER.FIELDS (FROM SUBJECT DATE)])")
if status != "OK" or not data or not data[0]:
mislukt += 1
log_failure(log, "stap2", bron, uid, None, "fetch_header", status, data)
continue
msg = email.message_from_bytes(data[0][1])
@@ -193,10 +234,13 @@ def stap2_bronmappen_routing(mail, log: dict, dry: bool):
continue
gepland += 1
audit_by_source[bron] += 1
audit_by_destination[doel] += 1
if dry:
print(f" [DRY {verwerkt}/{len(ids)}] {bron}{doel} | {subject[:60]}")
if not AUDIT:
print(f" [DRY {verwerkt}/{len(ids)}] {bron}{doel} | {subject[:60]}")
else:
ok = move_message(mail, uid, bron, doel, dry=False)
ok, failure = move_message(mail, uid, bron, doel, dry=False)
if ok:
verplaatst += 1
log.setdefault("stap2_verwerkt", []).append(log_key)
@@ -205,6 +249,8 @@ def stap2_bronmappen_routing(mail, log: dict, dry: bool):
_save_log(log)
else:
mislukt += 1
actie, status, response = failure
log_failure(log, "stap2", bron, uid, doel, actie, status, response)
if not dry:
mail.expunge()
@@ -232,6 +278,8 @@ def stap2_bronmappen_routing(mail, log: dict, dry: bool):
f" Totaal gepland: {totaal_gepland} | Totaal geen match: {totaal_geen_match}"
f" | Totaal overgeslagen: {totaal_overgeslagen} | Totaal fouten: {totaal_mislukt}"
)
if AUDIT:
print_audit("stap 2 geplande verplaatsingen", audit_by_source, audit_by_destination)
else:
print(
f" Totaal gepland: {totaal_gepland} | Totaal verplaatst: {totaal_verplaatst}"
@@ -248,6 +296,8 @@ def stap3_facturen(mail, log: dict, dry: bool):
"INBOX.Facturen - te verwerken",
"INBOX.Financieel.Facturen", # als ze er al in zitten
]
audit_by_source = Counter()
audit_by_destination = Counter()
for bron in bron_mappen:
status, data = mail.select(quote_mailbox(bron), readonly=dry)
@@ -272,29 +322,39 @@ def stap3_facturen(mail, log: dict, dry: bool):
status, data = mail.uid("FETCH", uid, "(BODY.PEEK[HEADER.FIELDS (DATE SUBJECT)])")
if status != "OK" or not data or not data[0]:
mislukt += 1
log_failure(log, "stap3", bron, uid, None, "fetch_header", status, data)
continue
msg = email.message_from_bytes(data[0][1])
date_str = msg.get("Date", "")
doel = PREFIX + quarter_folder(date_str)
gepland += 1
audit_by_source[bron] += 1
audit_by_destination[doel] += 1
if dry:
dt_kort = date_str[:16] if date_str else "?"
print(f" [DRY {index}/{len(ids)}] {dt_kort}{doel}")
if not AUDIT:
print(f" [DRY {index}/{len(ids)}] {dt_kort}{doel}")
else:
ok = move_message(mail, uid, bron, doel, dry=False)
ok, failure = move_message(mail, uid, bron, doel, dry=False)
if ok:
verplaatst += 1
else:
mislukt += 1
actie, status, response = failure
log_failure(log, "stap3", bron, uid, doel, actie, status, response)
if not dry:
mail.expunge()
print(f" Gepland: {gepland} | Verplaatst: {verplaatst} | Fouten: {mislukt}")
_save_log(log)
else:
print(f" Gepland: {gepland} | Fouten: {mislukt}")
if dry and AUDIT:
print_audit("stap 3 geplande factuurverplaatsingen", audit_by_source, audit_by_destination)
# ── log ──────────────────────────────────────────────────────────────────────
@@ -314,9 +374,15 @@ def load_log() -> dict:
# ── main ──────────────────────────────────────────────────────────────────────
def run():
if AUDIT and not DRY_RUN:
print("--audit is alleen bedoeld voor dry-runs. Laat --uitvoeren weg.")
sys.exit(1)
if DRY_RUN:
print("DRY RUN — er wordt niets verplaatst.")
print("Voeg --uitvoeren toe om echt uit te voeren.\n")
if AUDIT:
print("Auditmodus: per-bericht dry-runregels worden samengevat per bron en doel.\n")
else:
print("LET OP: dit verplaatst berichten op de echte server.")
bevestig = input("Typ JA om door te gaan: ").strip()