Treat only processed invoices as already sorted
Prompts used since the previous commit: - User said they do not want final destination folders in Archief. - User clarified: only treat INBOX.Facturen - verwerkt as already sorted; treat all other mailboxes as sources except Sent, Drafts, Trash and Spam. Special observations: - Updated source-folder selection accordingly. - Removed Archief final destination creation and added Technisch destination folders. - Verified Python compilation and route/folder consistency.
This commit is contained in:
+3
-5
@@ -185,11 +185,9 @@ def mappen() -> list[str]:
|
|||||||
"Bestellingen.Sligro",
|
"Bestellingen.Sligro",
|
||||||
"Bestellingen.Smartphonehoesjes",
|
"Bestellingen.Smartphonehoesjes",
|
||||||
"Bestellingen.Viking",
|
"Bestellingen.Viking",
|
||||||
# Archief nieuwe jaren
|
# Technisch
|
||||||
"Archief.2023",
|
"Technisch",
|
||||||
"Archief.2024",
|
"Technisch.DMARC",
|
||||||
"Archief.2025",
|
|
||||||
"Archief.2026",
|
|
||||||
]
|
]
|
||||||
return folders
|
return folders
|
||||||
|
|
||||||
|
|||||||
+141
-81
@@ -2,13 +2,15 @@
|
|||||||
"""
|
"""
|
||||||
Herindeling van bestaande mail naar de nieuwe mappenstructuur.
|
Herindeling van bestaande mail naar de nieuwe mappenstructuur.
|
||||||
|
|
||||||
Stap 1 – Vaste mapverplaatsingen (hele mappen in één keer):
|
Stap 1 – Vaste mapverplaatsingen:
|
||||||
Mailinglists/*, Kennis/Coursera, Archief/*/inkomend, enz.
|
Uitgeschakeld. Alle niet-uitgesloten mappen worden als bron behandeld.
|
||||||
|
|
||||||
Stap 2 – Per-bericht routing (INBOX + overige mappen):
|
Stap 2 – Per-bericht routing:
|
||||||
Op basis van afzenderdomein en onderwerpkeywords.
|
Op basis van afzenderdomein en onderwerpkeywords.
|
||||||
|
Bronmappen: alles behalve Sent, Drafts, Trash, Spam en
|
||||||
|
INBOX.Facturen - verwerkt.
|
||||||
|
|
||||||
Stap 3 – Facturen per kwartaal sorteren (Facturen - te verwerken / verwerkt).
|
Stap 3 – Facturen per kwartaal sorteren (Facturen - te verwerken).
|
||||||
|
|
||||||
Standaard: DRY RUN (laat zien wat er zou gebeuren, verplaatst niets).
|
Standaard: DRY RUN (laat zien wat er zou gebeuren, verplaatst niets).
|
||||||
Echt uitvoeren: python3 verplaats_bestaand.py --uitvoeren
|
Echt uitvoeren: python3 verplaats_bestaand.py --uitvoeren
|
||||||
@@ -63,6 +65,45 @@ def to_mutf7(s: str) -> str:
|
|||||||
)
|
)
|
||||||
res.append(f"&{b64}-")
|
res.append(f"&{b64}-")
|
||||||
return "".join(res)
|
return "".join(res)
|
||||||
|
|
||||||
|
|
||||||
|
def from_mutf7(s: str) -> str:
|
||||||
|
"""Decodeer een mUTF-7-mapnaam (van server) terug naar Unicode."""
|
||||||
|
res = []
|
||||||
|
i = 0
|
||||||
|
while i < len(s):
|
||||||
|
if s[i] == "&":
|
||||||
|
j = s.index("-", i + 1)
|
||||||
|
if j == i + 1:
|
||||||
|
res.append("&")
|
||||||
|
else:
|
||||||
|
encoded = s[i + 1 : j].replace(",", "/")
|
||||||
|
pad = (4 - len(encoded) % 4) % 4
|
||||||
|
res.append(
|
||||||
|
base64.b64decode(encoded + "=" * pad).decode("utf-16-be")
|
||||||
|
)
|
||||||
|
i = j + 1
|
||||||
|
else:
|
||||||
|
res.append(s[i])
|
||||||
|
i += 1
|
||||||
|
return "".join(res)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_folder_name(raw: bytes) -> str | None:
|
||||||
|
"""Haal mapnaam op uit IMAP LIST-response en decodeer mUTF-7."""
|
||||||
|
decoded = raw.decode("ascii", errors="replace")
|
||||||
|
match = re.match(r'\(.*?\)\s+".*?"\s+(.*)', decoded)
|
||||||
|
if not match:
|
||||||
|
return None
|
||||||
|
name = match.group(1).strip().strip('"')
|
||||||
|
if not name:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return from_mutf7(name)
|
||||||
|
except Exception:
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
LOG_FILE = Path(__file__).parent / "verplaats_log.json"
|
LOG_FILE = Path(__file__).parent / "verplaats_log.json"
|
||||||
|
|
||||||
DRY_RUN = "--uitvoeren" not in sys.argv
|
DRY_RUN = "--uitvoeren" not in sys.argv
|
||||||
@@ -178,31 +219,24 @@ FACTUUR_KEYWORDS = [
|
|||||||
"uw factuur", "your invoice", "je factuur",
|
"uw factuur", "your invoice", "je factuur",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Mappen die in één keer worden hernoemd/verplaatst (oud → nieuw)
|
# Mappen die in één keer worden hernoemd/verplaatst (oud → nieuw).
|
||||||
|
#
|
||||||
|
# Bewust leeg: oude mappen, archiefmappen en mailinglistmappen zijn bronnen.
|
||||||
|
# Alleen Sent, Drafts, Trash, Spam en INBOX.Facturen - verwerkt worden
|
||||||
|
# uitgesloten van routering.
|
||||||
MAP_RENAMES: list[tuple[str, str]] = [
|
MAP_RENAMES: list[tuple[str, str]] = [
|
||||||
("INBOX.Mailinglists.Ben Tiggelaar", "INBOX.Nieuwsbrieven.Leren.Ben Tiggelaar"),
|
|
||||||
("INBOX.Mailinglists.Brian Keating", "INBOX.Nieuwsbrieven.Leren.Brian Keating"),
|
|
||||||
("INBOX.Mailinglists.Eva Keiffenheim", "INBOX.Nieuwsbrieven.Leren.Eva Keiffenheim"),
|
|
||||||
("INBOX.Mailinglists.Lendahand", "INBOX.Nieuwsbrieven.Overig.Lendahand"),
|
|
||||||
("INBOX.Mailinglists.LYT - Nick Milo", "INBOX.Nieuwsbrieven.Leren.Nick Milo"),
|
|
||||||
("INBOX.Mailinglists.MIT Technology Review", "INBOX.Nieuwsbrieven.Leren.MIT Technology Review"),
|
|
||||||
("INBOX.Mailinglists.Storytelling with Data", "INBOX.Nieuwsbrieven.Leren.Storytelling with Data"),
|
|
||||||
("INBOX.Kennis.Coursera", "INBOX.Nieuwsbrieven.Leren.Coursera"),
|
|
||||||
# Archief: verwijder inkomend/verzonden laag
|
|
||||||
("INBOX.Archief.2020.inkomend", "INBOX.Archief.2020"),
|
|
||||||
("INBOX.Archief.2021.inkomend", "INBOX.Archief.2021"),
|
|
||||||
("INBOX.Archief.2022.inkomend", "INBOX.Archief.2022"),
|
|
||||||
# Verzonden archief ook samenvoegen
|
|
||||||
("INBOX.Archief.2020.verzonden", "INBOX.Archief.2020"),
|
|
||||||
("INBOX.Archief.2021.verzonden", "INBOX.Archief.2021"),
|
|
||||||
("INBOX.Archief.2022.verzonden", "INBOX.Archief.2022"),
|
|
||||||
# Facturen omzetten (per-kwartaal via stap 3)
|
|
||||||
("INBOX.Facturen - te verwerken", "__FACTUREN__"),
|
|
||||||
("INBOX.Facturen - verwerkt", "__FACTUREN__"),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# Mappen waarvan de inhoud per bericht gerouteerd moet worden
|
EXCLUDED_SOURCE_FOLDERS = {
|
||||||
ROUTE_FOLDERS = ["INBOX"]
|
"INBOX.Sent", "Sent",
|
||||||
|
"INBOX.Drafts", "Drafts",
|
||||||
|
"INBOX.Trash", "Trash",
|
||||||
|
"INBOX.Spam", "Spam",
|
||||||
|
}
|
||||||
|
|
||||||
|
ALREADY_SORTED_FOLDERS = {
|
||||||
|
"INBOX.Facturen - verwerkt",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# ── helpers ──────────────────────────────────────────────────────────────────
|
# ── helpers ──────────────────────────────────────────────────────────────────
|
||||||
@@ -253,6 +287,19 @@ def route_from_subject(from_addr: str, subject: str) -> str | None:
|
|||||||
return DOMAIN_LOOKUP.get(domain)
|
return DOMAIN_LOOKUP.get(domain)
|
||||||
|
|
||||||
|
|
||||||
|
def is_source_folder(folder: str) -> bool:
|
||||||
|
"""True als deze map als bron voor routering gebruikt mag worden."""
|
||||||
|
return folder not in EXCLUDED_SOURCE_FOLDERS and folder not in ALREADY_SORTED_FOLDERS
|
||||||
|
|
||||||
|
|
||||||
|
def list_source_folders(mail) -> list[str]:
|
||||||
|
status, raw_folders = mail.list()
|
||||||
|
if status != "OK" or not raw_folders:
|
||||||
|
return []
|
||||||
|
folders = [parse_folder_name(item) for item in raw_folders if item]
|
||||||
|
return sorted(f for f in folders if f and is_source_folder(f))
|
||||||
|
|
||||||
|
|
||||||
def move_message(mail, uid: bytes, src: str, dst_full: str, dry: bool) -> bool:
|
def move_message(mail, uid: bytes, src: str, dst_full: str, dry: bool) -> bool:
|
||||||
"""Kopieer bericht naar dst en markeer als verwijderd in src."""
|
"""Kopieer bericht naar dst en markeer als verwijderd in src."""
|
||||||
if dry:
|
if dry:
|
||||||
@@ -260,9 +307,10 @@ def move_message(mail, uid: bytes, src: str, dst_full: str, dry: bool) -> bool:
|
|||||||
try:
|
try:
|
||||||
mail.select(f'"{to_mutf7(src)}"')
|
mail.select(f'"{to_mutf7(src)}"')
|
||||||
res, _ = mail.uid("COPY", uid, f'"{to_mutf7(dst_full)}"')
|
res, _ = mail.uid("COPY", uid, f'"{to_mutf7(dst_full)}"')
|
||||||
if res == "OK":
|
if res != "OK":
|
||||||
mail.uid("STORE", uid, "+FLAGS", "\\Deleted")
|
return False
|
||||||
return res == "OK"
|
store_res, _ = mail.uid("STORE", uid, "+FLAGS", "\\Deleted")
|
||||||
|
return store_res == "OK"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" FOUT bij verplaatsen: {e}")
|
print(f" FOUT bij verplaatsen: {e}")
|
||||||
return False
|
return False
|
||||||
@@ -312,69 +360,82 @@ def stap1_mapverplaatsingen(mail, log: dict, dry: bool):
|
|||||||
|
|
||||||
# ── stap 2: per-bericht routing (INBOX) ─────────────────────────────────────
|
# ── stap 2: per-bericht routing (INBOX) ─────────────────────────────────────
|
||||||
|
|
||||||
def stap2_inbox_routing(mail, log: dict, dry: bool):
|
def stap2_bronmappen_routing(mail, log: dict, dry: bool):
|
||||||
print("\n── Stap 2: INBOX per-bericht routing ──")
|
print("\n── Stap 2: bronmappen per-bericht routing ──")
|
||||||
status, data = mail.select('"INBOX"', readonly=dry)
|
bron_mappen = list_source_folders(mail)
|
||||||
if status != "OK":
|
print(f" {len(bron_mappen)} bronmappen")
|
||||||
print(" Kan INBOX niet openen.")
|
|
||||||
return
|
|
||||||
|
|
||||||
n = int(data[0])
|
|
||||||
print(f" {n} berichten in INBOX")
|
|
||||||
|
|
||||||
# UID-gebaseerde search: stabiel ook na expunge
|
|
||||||
status, data = mail.uid("SEARCH", "ALL")
|
|
||||||
if status != "OK" or not data or not data[0]:
|
|
||||||
print(" Geen berichten gevonden (search mislukt of leeg).")
|
|
||||||
return
|
|
||||||
ids = [uid for uid in data[0].split() if uid]
|
|
||||||
|
|
||||||
verplaatst = 0
|
|
||||||
geen_match = 0
|
|
||||||
|
|
||||||
|
totaal_verplaatst = 0
|
||||||
|
totaal_geen_match = 0
|
||||||
al_verwerkt = set(log.get("stap2_verwerkt", []))
|
al_verwerkt = set(log.get("stap2_verwerkt", []))
|
||||||
|
|
||||||
for uid in ids:
|
for bron in bron_mappen:
|
||||||
uid_str = uid.decode()
|
status, data = mail.select(f'"{to_mutf7(bron)}"', readonly=dry)
|
||||||
if uid_str in al_verwerkt:
|
if status != "OK":
|
||||||
|
print(f" Kan map niet openen: {bron}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Haal alleen headers op via UID
|
n = int(data[0])
|
||||||
status, data = mail.uid("FETCH", uid, "(BODY.PEEK[HEADER.FIELDS (FROM SUBJECT DATE)])")
|
if n == 0:
|
||||||
|
continue
|
||||||
|
print(f" {bron}: {n} berichten")
|
||||||
|
|
||||||
|
status, data = mail.uid("SEARCH", "ALL")
|
||||||
if status != "OK" or not data or not data[0]:
|
if status != "OK" or not data or not data[0]:
|
||||||
continue
|
continue
|
||||||
|
ids = [uid for uid in data[0].split() if uid]
|
||||||
|
|
||||||
msg = email.message_from_bytes(data[0][1])
|
verplaatst = 0
|
||||||
from_raw = decode_hdr(msg.get("From", ""))
|
geen_match = 0
|
||||||
subject = decode_hdr(msg.get("Subject", ""))
|
|
||||||
date_str = msg.get("Date", "")
|
|
||||||
|
|
||||||
doel = route_from_subject(from_raw, subject)
|
for uid in ids:
|
||||||
|
uid_str = uid.decode()
|
||||||
|
log_key = f"{bron}:{uid_str}"
|
||||||
|
if log_key in al_verwerkt:
|
||||||
|
continue
|
||||||
|
|
||||||
if doel == "__FACTUUR_DATE__":
|
status, data = mail.uid("FETCH", uid, "(BODY.PEEK[HEADER.FIELDS (FROM SUBJECT DATE)])")
|
||||||
doel = PREFIX + quarter_folder(date_str)
|
if status != "OK" or not data or not data[0]:
|
||||||
elif doel:
|
continue
|
||||||
doel = PREFIX + doel
|
|
||||||
else:
|
|
||||||
geen_match += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
if dry:
|
msg = email.message_from_bytes(data[0][1])
|
||||||
print(f" [DRY] → {doel} | {subject[:60]}")
|
from_raw = decode_hdr(msg.get("From", ""))
|
||||||
else:
|
subject = decode_hdr(msg.get("Subject", ""))
|
||||||
ok = move_message(mail, uid, "INBOX", doel, dry=False)
|
date_str = msg.get("Date", "")
|
||||||
if ok:
|
|
||||||
verplaatst += 1
|
|
||||||
log.setdefault("stap2_verwerkt", []).append(uid_str)
|
|
||||||
if verplaatst % 50 == 0:
|
|
||||||
mail.expunge()
|
|
||||||
_save_log(log)
|
|
||||||
|
|
||||||
if not dry:
|
doel = route_from_subject(from_raw, subject)
|
||||||
mail.expunge()
|
|
||||||
_save_log(log)
|
|
||||||
|
|
||||||
print(f" Verplaatst: {verplaatst} | Geen match (blijft in INBOX): {geen_match}")
|
if doel == "__FACTUUR_DATE__":
|
||||||
|
doel = PREFIX + quarter_folder(date_str)
|
||||||
|
elif doel:
|
||||||
|
doel = PREFIX + doel
|
||||||
|
else:
|
||||||
|
geen_match += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if bron == doel:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if dry:
|
||||||
|
print(f" [DRY] {bron} → {doel} | {subject[:60]}")
|
||||||
|
else:
|
||||||
|
ok = move_message(mail, uid, bron, doel, dry=False)
|
||||||
|
if ok:
|
||||||
|
verplaatst += 1
|
||||||
|
log.setdefault("stap2_verwerkt", []).append(log_key)
|
||||||
|
if verplaatst % 50 == 0:
|
||||||
|
mail.expunge()
|
||||||
|
_save_log(log)
|
||||||
|
|
||||||
|
if not dry:
|
||||||
|
mail.expunge()
|
||||||
|
_save_log(log)
|
||||||
|
|
||||||
|
totaal_verplaatst += verplaatst
|
||||||
|
totaal_geen_match += geen_match
|
||||||
|
print(f" Verplaatst: {verplaatst} | Geen match: {geen_match}")
|
||||||
|
|
||||||
|
print(f" Totaal verplaatst: {totaal_verplaatst} | Totaal geen match: {totaal_geen_match}")
|
||||||
|
|
||||||
|
|
||||||
# ── stap 3: facturen per kwartaal ────────────────────────────────────────────
|
# ── stap 3: facturen per kwartaal ────────────────────────────────────────────
|
||||||
@@ -383,7 +444,6 @@ def stap3_facturen(mail, log: dict, dry: bool):
|
|||||||
print("\n── Stap 3: facturen per kwartaal ──")
|
print("\n── Stap 3: facturen per kwartaal ──")
|
||||||
bron_mappen = [
|
bron_mappen = [
|
||||||
"INBOX.Facturen - te verwerken",
|
"INBOX.Facturen - te verwerken",
|
||||||
"INBOX.Facturen - verwerkt",
|
|
||||||
"INBOX.Financieel.Facturen", # als ze er al in zitten
|
"INBOX.Financieel.Facturen", # als ze er al in zitten
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -468,7 +528,7 @@ def run():
|
|||||||
log = load_log()
|
log = load_log()
|
||||||
|
|
||||||
stap1_mapverplaatsingen(mail, log, DRY_RUN)
|
stap1_mapverplaatsingen(mail, log, DRY_RUN)
|
||||||
stap2_inbox_routing(mail, log, DRY_RUN)
|
stap2_bronmappen_routing(mail, log, DRY_RUN)
|
||||||
stap3_facturen(mail, log, DRY_RUN)
|
stap3_facturen(mail, log, DRY_RUN)
|
||||||
|
|
||||||
mail.logout()
|
mail.logout()
|
||||||
|
|||||||
Reference in New Issue
Block a user