Compare commits

..

13 Commits

Author SHA1 Message Date
mailcat d70112d766 Made the randam locale date optional, default just Dutch 2026-08-17 09:21:11 +02:00
mailcat 0b4e702bac Pre-merge between origin and dagda 2026-07-20 20:56:00 +02:00
mailcat 337378a362 Initial status for developed files on dagda 2026-07-20 20:49:36 +02:00
wienen 0af27236e5 [HCAW] Added random language date 2026-07-11 18:52:43 +02:00
wienen da481678b6 Track Sieve/plan/Roundcube files and update move log
- Add financ.sieve, plan.md, roundcube.txt to version control
- Update verplaats_log.json after backup dry run
- Gitignore codex.session session file
2026-07-11 13:08:15 +02:00
wienen 7b345b8795 Add standalone dynamic invoice Sieve rule
The prompts used to arrive at this change since the previous commit

User reported the Dynamisch kwartaal Roundcube filter is empty and asked for a new file containing a dynamic invoice-quarter filter while keeping the rest of the plan approved.

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

invoice_dynamic_quarter.sieve uses Date header year capture plus regex month tests and compiles with sievec. Existing untracked financ.sieve, codex.session, roundcube.txt, plan.md, and verplaats_log.json were left untouched.
2026-07-05 22:28:36 +02:00
wienen 12a82f570a Reduce mailcat to reporting and reorder tools
The prompts used to arrive at this change since the previous commit

User said the real hans mailbox has been reordered, Roundcube filters are uploaded, the sorter and web rule interface are no longer needed, then asked to keep logs and perform the cleanup while retaining unsubscribe capability.

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

Kept daily report, Sieve policy, folder/reorder scripts, unsubscribe script/data, and log/state files. Removed live sorter, web UI, deploy helpers, old analysis artifacts, backup mirror, and obsolete tests. Verification passed; verplaats_log.json, plan.md, and roundcube.txt remain unstaged/local.
2026-07-05 13:49:53 +02:00
wienen db209ac246 Name and sort Roundcube Sieve rules
The prompts used to arrive at this change since the previous commit

User wanted Roundcube filters to have meaningful names, ordered alphabetically, with financial filters first, while keeping invoice handling as one dynamic variable rule.

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

mailcat_roundcube.sieve now uses # rule names, puts the dynamic invoice rule first, sorts remaining rules by name, and disambiguates duplicate destinations with Mailing-list/Domain suffixes. sievec validation passed locally. roundcube.txt, verplaats_log.json, and plan.md were left unstaged.
2026-07-05 13:03:30 +02:00
wienen 35d78a27aa Add Roundcube Sieve routing file
The prompts used to arrive at this change since the previous commit

User exported a Roundcube test filter, asked for a new Sieve file for the current setup, and requested that Afmelden rules be included while disregarding the existing roundcube.txt filters.

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

Generated mailcat_roundcube.sieve from mailinglist_routes.json and domain_routes.json with INBOX-prefixed fileinto targets and no copy actions. No local Sieve compiler was available; JSON and structural checks passed. roundcube.txt, verplaats_log.json, and plan.md were left unstaged.
2026-07-05 12:33:15 +02:00
wienen 0811a6ca5a Revert "Harden historical sorter header parsing"
This reverts commit edcad4560c.
2026-07-05 11:54:52 +02:00
wienen edcad4560c Harden historical sorter header parsing
The prompts used to arrive at this change since the previous commit

User asked to check the audit log in the current directory before reordering the real mailbox.

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

The inspected log was a partial real backup run, not a hans audit. Header parsing now skips non-bytes FETCH payloads instead of aborting. verplaats_log.json and plan.md were left unstaged.
2026-07-05 11:48:09 +02:00
wienen d0e78e728a Strengthen plain stop wrappers
The prompts used to arrive at this change since the previous commit

User reported that the stop_mailcat script does not stop mailcat.

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

Plain-mode stop_mailcat.sh and stop_web.sh now wait longer after SIGTERM and fall back to SIGKILL. verplaats_log.json and plan.md were left unstaged.
2026-07-05 11:35:44 +02:00
wienen 99cd891b1e Add Apple Mail links to daily report
The prompts used to arrive at this change since the previous commit

User asked whether daily report emails could include links, then chose Apple Mail links as the first implementation.

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

Apple Mail links use Message-ID-based message:// URLs and depend on macOS Mail having the messages synced/indexed locally. verplaats_log.json and plan.md were left unstaged.
2026-07-05 11:22:03 +02:00
73 changed files with 11947 additions and 10971 deletions
Executable
BIN
View File
Binary file not shown.
Executable
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Executable
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Executable
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Executable
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+8
View File
@@ -0,0 +1,8 @@
{
"permissions": {
"allow": [
"Bash(python *)",
"Bash(python3 *)"
]
}
}
+2
View File
@@ -7,3 +7,5 @@ web_config.json
**/*.eml **/*.eml
sort_mail_daemon_state.json sort_mail_daemon_state.json
verplaats_log_*.json verplaats_log_*.json
unsubscribe_log.json
codex.session
+12
View File
@@ -0,0 +1,12 @@
#!/bin/sh
set -eu
APP_DIR="/share/homes/mailcat/mailcat"
ACCOUNT="backup"
PYTHON="/opt/bin/python3"
LOG_DIR="/share/homes/mailcat/mailcat/log"
MAIL_TO="hans@australius.nl"
MKDIR="/bin/mkdir"
"$MKDIR" -p "$LOG_DIR"
cd "$APP_DIR"
"$PYTHON" "$APP_DIR/dagelijks_overzicht.py" --account "$ACCOUNT" --mail-to "$MAIL_TO" >> "$LOG_DIR/daily_report.out" 2>&1
+26
View File
@@ -0,0 +1,26 @@
#!/bin/sh
set -eu
APP_DIR="/share/homes/mailcat/mailcat"
ACCOUNT="backup"
PYTHON="/opt/bin/python3"
STATE_DIR="/share/homes/mailcat/mailcat/state"
LOG_DIR="/share/homes/mailcat/mailcat/log"
RUN_DIR="/share/homes/mailcat/mailcat/run"
PID_FILE="/share/homes/mailcat/mailcat/run/mailcat.pid"
MKDIR="/bin/mkdir"
"$MKDIR" -p "$STATE_DIR" "$LOG_DIR" "$RUN_DIR"
if [ -f "$PID_FILE" ]; then
old_pid=""
IFS= read -r old_pid < "$PID_FILE" || true
if [ -n "$old_pid" ] && kill -0 "$old_pid" 2>/dev/null; then
echo "mailcat already running: $old_pid"
exit 0
fi
fi
cd "$APP_DIR"
"$PYTHON" "$APP_DIR/sort_mail_daemon.py" --account "$ACCOUNT" --folder INBOX --state-file "$STATE_DIR/sort_mail_daemon_state.json" --log-file "$LOG_DIR/sort_mail_daemon.log" >> "$LOG_DIR/daemon.out" 2>&1 &
pid="$!"
echo "$pid" > "$PID_FILE"
echo "mailcat started: $pid"
+32
View File
@@ -0,0 +1,32 @@
#!/bin/sh
set -eu
APP_DIR="/share/homes/mailcat/mailcat"
PYTHON="/opt/bin/python3"
LOG_DIR="/share/homes/mailcat/mailcat/log"
RUN_DIR="/share/homes/mailcat/mailcat/run"
WEB_PID_FILE="/share/homes/mailcat/mailcat/run/mailcat_web.pid"
WEB_HOST="0.0.0.0"
WEB_PORT="4321"
WEB_CONFIG="/share/homes/mailcat/mailcat/web_config.json"
MKDIR="/bin/mkdir"
if [ ! -f "$WEB_CONFIG" ]; then
echo "missing web config: $WEB_CONFIG"
exit 1
fi
"$MKDIR" -p "$LOG_DIR" "$RUN_DIR"
if [ -f "$WEB_PID_FILE" ]; then
old_pid=""
IFS= read -r old_pid < "$WEB_PID_FILE" || true
if [ -n "$old_pid" ] && kill -0 "$old_pid" 2>/dev/null; then
echo "mailcat web already running: $old_pid"
exit 0
fi
fi
cd "$APP_DIR"
"$PYTHON" "$APP_DIR/manage_routes_web.py" --host "$WEB_HOST" --port "$WEB_PORT" --config "$WEB_CONFIG" >> "$LOG_DIR/web.out" 2>&1 &
pid="$!"
echo "$pid" > "$WEB_PID_FILE"
echo "mailcat web started: $pid"
+18
View File
@@ -0,0 +1,18 @@
#!/bin/sh
set -eu
PID_FILE="/share/homes/mailcat/mailcat/run/mailcat.pid"
LOG_DIR="/share/homes/mailcat/mailcat/log"
if [ -f "$PID_FILE" ]; then
pid=""
IFS= read -r pid < "$PID_FILE" || true
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
echo "mailcat running: $pid"
exit 0
fi
echo "mailcat not running: stale pid file ($pid)"
exit 1
fi
echo "mailcat not running"
echo "logs: $LOG_DIR"
exit 1
+21
View File
@@ -0,0 +1,21 @@
#!/bin/sh
set -eu
WEB_PID_FILE="/share/homes/mailcat/mailcat/run/mailcat_web.pid"
WEB_HOST="0.0.0.0"
WEB_PORT="4321"
LOG_DIR="/share/homes/mailcat/mailcat/log"
if [ -f "$WEB_PID_FILE" ]; then
pid=""
IFS= read -r pid < "$WEB_PID_FILE" || true
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
echo "mailcat web running: $pid"
echo "url: http://$WEB_HOST:$WEB_PORT/"
exit 0
fi
echo "mailcat web not running: stale pid file ($pid)"
exit 1
fi
echo "mailcat web not running"
echo "logs: $LOG_DIR/web.out"
exit 1
+41
View File
@@ -0,0 +1,41 @@
#!/bin/sh
set -eu
PID_FILE="/share/homes/mailcat/mailcat/run/mailcat.pid"
RM="/bin/rm"
if [ ! -f "$PID_FILE" ]; then
echo "mailcat not running: no pid file"
exit 0
fi
pid=""
IFS= read -r pid < "$PID_FILE" || true
if [ -z "$pid" ]; then
"$RM" -f "$PID_FILE"
echo "mailcat not running: empty pid file removed"
exit 0
fi
if kill -0 "$pid" 2>/dev/null; then
kill "$pid"
timeout=20
while [ "$timeout" -gt 0 ] && kill -0 "$pid" 2>/dev/null; do
sleep 1
timeout=$((timeout - 1))
done
if kill -0 "$pid" 2>/dev/null; then
echo "mailcat still running after SIGTERM, sending SIGKILL: $pid"
kill -KILL "$pid" 2>/dev/null || true
timeout=5
while [ "$timeout" -gt 0 ] && kill -0 "$pid" 2>/dev/null; do
sleep 1
timeout=$((timeout - 1))
done
if kill -0 "$pid" 2>/dev/null; then
echo "mailcat still running after SIGKILL: $pid"
exit 1
fi
fi
fi
"$RM" -f "$PID_FILE"
echo "mailcat stopped"
+41
View File
@@ -0,0 +1,41 @@
#!/bin/sh
set -eu
WEB_PID_FILE="/share/homes/mailcat/mailcat/run/mailcat_web.pid"
RM="/bin/rm"
if [ ! -f "$WEB_PID_FILE" ]; then
echo "mailcat web not running: no pid file"
exit 0
fi
pid=""
IFS= read -r pid < "$WEB_PID_FILE" || true
if [ -z "$pid" ]; then
"$RM" -f "$WEB_PID_FILE"
echo "mailcat web not running: empty pid file removed"
exit 0
fi
if kill -0 "$pid" 2>/dev/null; then
kill "$pid"
timeout=20
while [ "$timeout" -gt 0 ] && kill -0 "$pid" 2>/dev/null; do
sleep 1
timeout=$((timeout - 1))
done
if kill -0 "$pid" 2>/dev/null; then
echo "mailcat web still running after SIGTERM, sending SIGKILL: $pid"
kill -KILL "$pid" 2>/dev/null || true
timeout=5
while [ "$timeout" -gt 0 ] && kill -0 "$pid" 2>/dev/null; do
sleep 1
timeout=$((timeout - 1))
done
if kill -0 "$pid" 2>/dev/null; then
echo "mailcat web still running after SIGKILL: $pid"
exit 1
fi
fi
fi
"$RM" -f "$WEB_PID_FILE"
echo "mailcat web stopped"
+68 -10
View File
@@ -15,19 +15,21 @@ import imaplib
import email import email
import email.header import email.header
import email.utils import email.utils
import json
import sys import sys
import ssl import ssl
import smtplib import smtplib
import re import re
import html as html_lib
import urllib.parse
import config import config
from datetime import date, timedelta, datetime from datetime import date, timedelta, datetime
from pathlib import Path
from collections import defaultdict from collections import defaultdict
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
from imap_utils import list_folders, quote_mailbox from imap_utils import list_folders, quote_mailbox
from mail_imap_ops import fetch_body from babel.dates import format_date
import babel
import random
# Mappen die NIET in het overzicht komen (systeem + inbox zelf) # Mappen die NIET in het overzicht komen (systeem + inbox zelf)
UITSLUITINGEN = { UITSLUITINGEN = {
@@ -99,6 +101,49 @@ def leesbare_map(imap_naam: str) -> str:
return imap_naam.removeprefix("INBOX.").replace(".", " ") return imap_naam.removeprefix("INBOX.").replace(".", " ")
def apple_mail_url(message_id: str) -> str:
"""Return an Apple Mail message:// URL for a Message-ID header."""
clean = (message_id or "").strip()
if not clean:
return ""
if not (clean.startswith("<") and clean.endswith(">")):
clean = f"<{clean.strip('<>')}>"
return "message://" + urllib.parse.quote(clean, safe="")
def fetch_body(data) -> bytes | None:
"""Return the first bytes payload from an IMAP FETCH response."""
if not data:
return None
for item in data:
if isinstance(item, tuple) and len(item) >= 2 and isinstance(item[1], bytes):
return item[1]
return None
def invalid_locale(code):
return babel.Locale(code).language_name is None
def get_random_locale() -> (str, str, str):
"""Return the locale code, the local name and the english name of a language."""
all_ids = babel.localedata.locale_identifiers()
code = all_ids[random.randrange(0, len(all_ids))]
while (invalid_locale(code)):
code = all_ids[random.randrange(0, len(all_ids))]
return (code, babel.Locale(code).language_name, babel.Locale(code).english_name)
def create_mail_subject(zoekdatum, random = False) -> str:
# Als random = true, toon dan de datum ook in een willekeurige andere taal
# Anders alleen in het Nederlands
datum_nl = format_date(zoekdatum, "EEEE d MMMM yyyy", locale="nl")
if random:
random_locale, random_lang, random_elang = get_random_locale()
datum_bl = format_date(zoekdatum, locale=random_locale)
return f"Mailoverzicht van {datum_bl} ({random_lang} - {random_elang}) of {datum_nl}"
else:
return f"Mailoverzicht van {datum_nl}"
# ── IMAP ophalen ────────────────────────────────────────────────────────────── # ── IMAP ophalen ──────────────────────────────────────────────────────────────
def haal_berichten_op(mail: imaplib.IMAP4_SSL, zoekdatum: date) -> dict[str, list[dict]]: def haal_berichten_op(mail: imaplib.IMAP4_SSL, zoekdatum: date) -> dict[str, list[dict]]:
@@ -138,7 +183,7 @@ def haal_berichten_op(mail: imaplib.IMAP4_SSL, zoekdatum: date) -> dict[str, lis
for uid in ids: for uid in ids:
status, msg_data = mail.fetch( status, msg_data = mail.fetch(
uid, "(BODY.PEEK[HEADER.FIELDS (FROM SUBJECT DATE)])" uid, "(BODY.PEEK[HEADER.FIELDS (FROM SUBJECT DATE MESSAGE-ID)])"
) )
raw_header = fetch_body(msg_data) raw_header = fetch_body(msg_data)
if status != "OK" or raw_header is None: if status != "OK" or raw_header is None:
@@ -148,6 +193,7 @@ def haal_berichten_op(mail: imaplib.IMAP4_SSL, zoekdatum: date) -> dict[str, lis
from_raw = decode_hdr(msg.get("From", "")) from_raw = decode_hdr(msg.get("From", ""))
subject = decode_hdr(msg.get("Subject", "(geen onderwerp)")) subject = decode_hdr(msg.get("Subject", "(geen onderwerp)"))
date_str = msg.get("Date", "") date_str = msg.get("Date", "")
message_id = (msg.get("Message-ID") or "").strip()
# Tijdstip opmaken # Tijdstip opmaken
try: try:
@@ -164,6 +210,7 @@ def haal_berichten_op(mail: imaplib.IMAP4_SSL, zoekdatum: date) -> dict[str, lis
"from": afzender[:40], "from": afzender[:40],
"subject": subject[:80], "subject": subject[:80],
"time": tijdstip, "time": tijdstip,
"message_id": message_id,
}) })
return resultaat return resultaat
@@ -203,15 +250,23 @@ def bouw_html(berichten: dict[str, list[dict]], zoekdatum: date) -> str:
for folder in sorted(berichten.keys()): for folder in sorted(berichten.keys()):
items = berichten[folder] items = berichten[folder]
label = leesbare_map(folder) label = html_lib.escape(leesbare_map(folder))
html += f'<h2>{label} <span style="font-weight:normal;color:#888">({len(items)})</span></h2>\n' html += f'<h2>{label} <span style="font-weight:normal;color:#888">({len(items)})</span></h2>\n'
html += '<table>\n' html += '<table>\n'
for item in items: for item in items:
time = html_lib.escape(item["time"])
sender = html_lib.escape(item["from"])
subject = html_lib.escape(item["subject"])
url = apple_mail_url(item.get("message_id", ""))
if url:
subject_html = f'<a href="{html_lib.escape(url, quote=True)}">{subject}</a>'
else:
subject_html = subject
html += ( html += (
f'<tr>' f'<tr>'
f'<td class="time">{item["time"]}</td>' f'<td class="time">{time}</td>'
f'<td class="from">{item["from"]}</td>' f'<td class="from">{sender}</td>'
f'<td class="subject">{item["subject"]}</td>' f'<td class="subject">{subject_html}</td>'
f'</tr>\n' f'</tr>\n'
) )
html += '</table>\n' html += '</table>\n'
@@ -224,9 +279,12 @@ def bouw_html(berichten: dict[str, list[dict]], zoekdatum: date) -> str:
# ── e-mail versturen ────────────────────────────────────────────────────────── # ── e-mail versturen ──────────────────────────────────────────────────────────
def stuur_overzicht(html: str, zoekdatum: date, config: dict): def stuur_overzicht(html: str, zoekdatum: date, config: dict):
datum_nl = zoekdatum.strftime("%-d %B %Y") # datum_nl = format_date(zoekdatum, "EEEE d MMMM yyyy", locale="nl") # zoekdatum.strftime("%-d %B %Y")
# random_locale, random_lang, random_elang = get_random_locale()
# datum_bl = format_date(zoekdatum, locale=random_locale)
msg = MIMEMultipart("alternative") msg = MIMEMultipart("alternative")
msg["Subject"] = f"Mailbak {datum_nl}" # msg["Subject"] = f"Mailoverzicht van {datum_bl} ({random_lang} - {random_elang}) of {datum_nl}"
msg["Subject"] = create_mail_subject(zoekdatum, False)
msg["From"] = config["from"] msg["From"] = config["from"]
msg["To"] = config["to"] msg["To"] = config["to"]
msg.attach(MIMEText(html, "html", "utf-8")) msg.attach(MIMEText(html, "html", "utf-8"))
+257
View File
@@ -0,0 +1,257 @@
sh: line 1: hans: command not found
Account: hans@australius.nl
Ophalen berichten van 2026-07-04 ...
2 berichten gevonden in 2 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-07-04 ...
2 berichten gevonden in 2 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-07-05 ...
4 berichten gevonden in 3 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-07-05 ...
4 berichten gevonden in 3 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-07-06 ...
6 berichten gevonden in 5 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-07-06 ...
6 berichten gevonden in 5 mappen.
Overzicht verstuurd naar hans@australius.nl
Account 'hand' niet gevonden. Beschikbaar: backup, hans
Account: hans@australius.nl
2026-07-08 10:49:01.846690: Ophalen berichten van 2026-07-07 ...
12 berichten gevonden in 7 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
2026-07-08 13:35:02.222744: Ophalen berichten van 2026-07-07 ...
12 berichten gevonden in 7 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
2026-07-08 13:36:01.725869: Ophalen berichten van 2026-07-07 ...
12 berichten gevonden in 7 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
2026-07-08 13:38:01.565000: Ophalen berichten van 2026-07-07 ...
12 berichten gevonden in 7 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
2026-07-08 13:39:01.933965: Ophalen berichten van 2026-07-07 ...
12 berichten gevonden in 7 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
2026-07-09 06:00:02.179821: Ophalen berichten van 2026-07-08 ...
10 berichten gevonden in 9 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
2026-07-10 06:00:02.302178: Ophalen berichten van 2026-07-09 ...
10 berichten gevonden in 8 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
2026-07-11 06:00:01.530567: Ophalen berichten van 2026-07-10 ...
11 berichten gevonden in 8 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-07-11 ...
4 berichten gevonden in 4 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-07-12 ...
3 berichten gevonden in 2 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-07-13 ...
12 berichten gevonden in 10 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-07-14 ...
12 berichten gevonden in 5 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-07-15 ...
10 berichten gevonden in 8 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-07-16 ...
4 berichten gevonden in 4 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-07-17 ...
4 berichten gevonden in 3 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-07-18 ...
1 berichten gevonden in 1 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-07-19 ...
2 berichten gevonden in 2 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-07-20 ...
7 berichten gevonden in 5 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-07-21 ...
6 berichten gevonden in 6 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-07-22 ...
7 berichten gevonden in 5 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-07-23 ...
5 berichten gevonden in 5 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-07-24 ...
8 berichten gevonden in 6 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-07-25 ...
5 berichten gevonden in 4 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-07-26 ...
1 berichten gevonden in 1 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-07-27 ...
7 berichten gevonden in 5 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-07-28 ...
6 berichten gevonden in 4 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-07-29 ...
3 berichten gevonden in 2 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-07-30 ...
9 berichten gevonden in 8 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-07-31 ...
4 berichten gevonden in 3 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-08-01 ...
2 berichten gevonden in 2 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-08-02 ...
4 berichten gevonden in 2 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-08-03 ...
5 berichten gevonden in 4 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-08-04 ...
4 berichten gevonden in 4 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-08-05 ...
4 berichten gevonden in 3 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-08-06 ...
7 berichten gevonden in 6 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-08-07 ...
5 berichten gevonden in 4 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-08-08 ...
6 berichten gevonden in 5 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-08-09 ...
3 berichten gevonden in 3 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-08-10 ...
4 berichten gevonden in 3 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-08-11 ...
5 berichten gevonden in 4 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-08-12 ...
7 berichten gevonden in 6 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-08-13 ...
7 berichten gevonden in 5 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-08-14 ...
6 berichten gevonden in 3 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-08-15 ...
1 berichten gevonden in 1 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
Ophalen berichten van 2026-08-16 ...
3 berichten gevonden in 2 mappen.
Overzicht verstuurd naar hans@australius.nl
+30 -4
View File
@@ -184,12 +184,25 @@ fi
if kill -0 "\$pid" 2>/dev/null; then if kill -0 "\$pid" 2>/dev/null; then
kill "\$pid" kill "\$pid"
sleep 2 timeout=20
while [ "\$timeout" -gt 0 ] && kill -0 "\$pid" 2>/dev/null; do
sleep 1
timeout=\$((timeout - 1))
done
if kill -0 "\$pid" 2>/dev/null; then if kill -0 "\$pid" 2>/dev/null; then
echo "mailcat still running after SIGTERM: \$pid" echo "mailcat still running after SIGTERM, sending SIGKILL: \$pid"
kill -KILL "\$pid" 2>/dev/null || true
timeout=5
while [ "\$timeout" -gt 0 ] && kill -0 "\$pid" 2>/dev/null; do
sleep 1
timeout=\$((timeout - 1))
done
if kill -0 "\$pid" 2>/dev/null; then
echo "mailcat still running after SIGKILL: \$pid"
exit 1 exit 1
fi fi
fi fi
fi
"\$RM" -f "\$PID_FILE" "\$RM" -f "\$PID_FILE"
echo "mailcat stopped" echo "mailcat stopped"
STOP_SCRIPT STOP_SCRIPT
@@ -286,12 +299,25 @@ fi
if kill -0 "\$pid" 2>/dev/null; then if kill -0 "\$pid" 2>/dev/null; then
kill "\$pid" kill "\$pid"
sleep 2 timeout=20
while [ "\$timeout" -gt 0 ] && kill -0 "\$pid" 2>/dev/null; do
sleep 1
timeout=\$((timeout - 1))
done
if kill -0 "\$pid" 2>/dev/null; then if kill -0 "\$pid" 2>/dev/null; then
echo "mailcat web still running after SIGTERM: \$pid" echo "mailcat web still running after SIGTERM, sending SIGKILL: \$pid"
kill -KILL "\$pid" 2>/dev/null || true
timeout=5
while [ "\$timeout" -gt 0 ] && kill -0 "\$pid" 2>/dev/null; do
sleep 1
timeout=\$((timeout - 1))
done
if kill -0 "\$pid" 2>/dev/null; then
echo "mailcat web still running after SIGKILL: \$pid"
exit 1 exit 1
fi fi
fi fi
fi
"\$RM" -f "\$WEB_PID_FILE" "\$RM" -f "\$WEB_PID_FILE"
echo "mailcat web stopped" echo "mailcat web stopped"
WEB_STOP_SCRIPT WEB_STOP_SCRIPT
+10
View File
@@ -0,0 +1,10 @@
Account: hans@australius.nl
2026-07-08 10:50:02.099347: Ophalen berichten van 2026-07-07 ...
12 berichten gevonden in 7 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: hans@australius.nl
2026-07-08 13:35:09.360753: Ophalen berichten van 2026-07-07 ...
12 berichten gevonden in 7 mappen.
Overzicht verstuurd naar hans@australius.nl
+26
View File
@@ -0,0 +1,26 @@
# mailcat_roundcube.sieve
# Generated from domain_routes.json and mailinglist_routes.json.
# Intended for Roundcube/Managesieve import for hans@australius.nl.
# Do not use :copy: these rules sort incoming mail into destination folders.
# Historical mailbox reordering still requires verplaats_bestaand.py.
require ["fileinto", "date", "variables"];
# Financial rules are first. The invoice rule stays dynamic, so individual quarters are not separately ordered.
# rule:[Financieel / Facturen / Dynamisch kwartaal]
if header :contains "Subject" ["factuur", "invoice", "rekening", "nota", "betaalverzoek", "receipt", "orderbevestiging", "order confirmation", "betaling ontvangen", "payment received", "uw factuur", "your invoice", "je factuur"] {
if currentdate :matches "year" "*" {
set "jaar" "${1}";
}
if anyof (currentdate :is "month" "1", currentdate :is "month" "2", currentdate :is "month" "3") {
fileinto "INBOX.Financieel.Facturen.${jaar}.Q1";
} elsif anyof (currentdate :is "month" "4", currentdate :is "month" "5", currentdate :is "month" "6") {
fileinto "INBOX.Financieel.Facturen.${jaar}.Q2";
} elsif anyof (currentdate :is "month" "7", currentdate :is "month" "8", currentdate :is "month" "9") {
fileinto "INBOX.Financieel.Facturen.${jaar}.Q3";
} else {
fileinto "INBOX.Financieel.Facturen.${jaar}.Q4";
}
stop;
}
+40
View File
@@ -0,0 +1,40 @@
# invoice_dynamic_quarter.sieve
# Single Roundcube/Managesieve rule for dynamic invoice quarter routing.
# Import this as a standalone filter set or copy the rule into an existing set.
require ["fileinto", "date", "variables", "regex"];
# rule:[Financieel / Facturen / Dynamisch kwartaal]
if allof (
header :contains "Subject" [
"factuur",
"invoice",
"rekening",
"nota",
"betaalverzoek",
"receipt",
"orderbevestiging",
"order confirmation",
"betaling ontvangen",
"payment received",
"uw factuur",
"your invoice",
"je factuur"
],
date :matches "Date" "year" "*"
) {
set "jaar" "${1}";
if date :regex "Date" "month" "^(0?[123])$" {
set "kwartaal" "Q1";
} elsif date :regex "Date" "month" "^(0?[456])$" {
set "kwartaal" "Q2";
} elsif date :regex "Date" "month" "^(0?[789])$" {
set "kwartaal" "Q3";
} else {
set "kwartaal" "Q4";
}
fileinto "INBOX.Financieel.Facturen.${jaar}.${kwartaal}";
stop;
}
+1593
View File
File diff suppressed because it is too large Load Diff
+10
View File
@@ -0,0 +1,10 @@
Account: backup@australius.nl
Ophalen berichten van 2026-07-03 ...
10 berichten gevonden in 7 mappen.
Overzicht verstuurd naar hans@australius.nl
Account: backup@australius.nl
Ophalen berichten van 2026-07-04 ...
3 berichten gevonden in 3 mappen.
Overzicht verstuurd naar hans@australius.nl
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+32
View File
@@ -0,0 +1,32 @@
---
Plan
Fase 0 — Download (jij voert uit)
Draai python download_mailbox.py in de projectmap. Het script:
- vraagt om je e-mailadres + wachtwoord (lokaal, gaat niet via mij)
- downloadt alle mappen en mails naar mailbox/<mapnaam>/<id>.eml
- is herstart-safe: al gedownloade mails worden overgeslagen
:q
Fase 1 — Overzichten (ik maak na download)
1. Mailinglijsten — via List-ID / List-Unsubscribe headers; per lijst: volume, laatste mail, advies (afmelden / houden)
2. Facturen in mailbox — PDF-bijlagen + mails met factuurtermen; per afzender gesorteerd
3. Factuurlinks — mails die verwijzen naar een portaal ("bekijk je factuur op..."); per website
4. Prullenbak-kandidaten — auto-notificaties, lege threads, doublures, oude nieuwsbrieven
Fase 2 — Mappenindeling + mailrules (ik stel voor na analyse)
Op basis van wat ik in de mailbox aantref, maak ik een voorstel voor:
- een logische, intuïtieve mappenstructuur (bijv. Financieel / Projecten / Abonnementen / Archief)
- server-side mailrules (Sieve-formaat, het meest gangbare bij IMAP-providers) om inkomende mail automatisch te sorteren
Fase 3 — Scripts (ik schrijf, jij voert uit op echte mailbox)
- Unsubscribe-script — per mailinglijst: mailto of HTTP List-Unsubscribe aanroepen
- Opruim-script — geselecteerde mails via IMAP naar Trash verplaatsen
- Factuur-plan — per website een aanpak voor geautomatiseerd ophalen (login-flow, download-URL)
- Mailrules-implementatie — instructies of script om de Sieve-rules op de server te zetten
---
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+133 -7
View File
@@ -2,6 +2,17 @@
You are continuing the `mailcat` project in `/Users/hanswienen/Documents/Development/Vibes/mailcat`. You are continuing the `mailcat` project in `/Users/hanswienen/Documents/Development/Vibes/mailcat`.
## Current Goal
The real `hans` mailbox has been completely reordered, and Roundcube/Managesieve filters have been uploaded for ongoing incoming-mail sorting. The Python live sorter and web route-management interface are no longer needed.
The repository is intentionally reduced to the minimum tooling needed to:
1. Repeat folder creation and historical reordering if needed.
2. Keep/generate the Roundcube Sieve policy.
3. Continue unsubscribe work.
4. Run the daily report.
## Project Goal ## Project Goal
Build a reliable mailbox cleanup and automation toolkit for `australius.nl`. Build a reliable mailbox cleanup and automation toolkit for `australius.nl`.
@@ -13,10 +24,69 @@ Do not run mailbox-affecting scripts yourself unless the user explicitly authori
## Collaboration Rules ## Collaboration Rules
- Follow the git workflow from `AGENTS.md`. - Follow the git workflow from `AGENTS.md`.
- Before making file, code, or configuration changes, present a plan and wait for explicit approval.
- Work on branch `codex` unless the user says otherwise. - Work on branch `codex` unless the user says otherwise.
- Before every commit, rewrite this file from scratch as the current reconstruction prompt. - Before every commit, rewrite this file from scratch as the current reconstruction prompt.
- Commit after each user-prompted code change using the required three-section commit message format. - Commit after each user-prompted code change using the required three-section commit message format.
- The user asked: first provide a plan and wait for approval before doing future work. - Keep runtime logs/state files unless the user explicitly says to delete them.
- Do not commit credentials.
## Kept Files
Core configuration and IMAP helpers:
- `config.py`
- `imap_utils.py`
- `config.json` is local-only and ignored; it contains account credentials.
Routing/reorder policy:
- `mail_routes.py`
- `domain_routes.json`
- `mailinglist_routes.json`
- `maak_mappen.py`
- `verplaats_bestaand.py`
Roundcube/Sieve:
- `mailcat_roundcube.sieve`
- `roundcube.txt` is local-only user export/test evidence and should not be treated as production policy.
Unsubscribe:
- `unsubscribe.py`
- `decisions.json`
- `unsubscribe_log.json` is runtime state and should not be committed unless the user explicitly asks.
Daily report:
- `dagelijks_overzicht.py`
- `tests/test_dagelijks_overzicht.py`
Logs/state kept:
- `backup_log.json`
- `verplaats_log.json`
- `verplaats_log_voor_aiwijzigingen.json`
- `verplaats_bestaand.log`
- `unsubscribe_log.json`
## Current Behavior
Roundcube/Sieve:
- `mailcat_roundcube.sieve` is the generated Roundcube/Managesieve Sieve file for the current routing setup.
- `invoice_dynamic_quarter.sieve` is a standalone Roundcube/Managesieve Sieve file containing only the dynamic invoice-quarter rule. It uses the message `Date` header to set `jaar`, regex month tests to set `kwartaal`, then files into `INBOX.Financieel.Facturen.${jaar}.${kwartaal}`.
- It uses `INBOX.`-prefixed mailbox names because the user's Roundcube export used `fileinto :copy "INBOX.Notes";`.
- It includes `Afmelden.*` routes and uses `fileinto` without `:copy`, so matching incoming messages are sorted instead of duplicated.
- Rules have Roundcube-compatible `# rule:[...]` names.
- Financial rules are first; the invoice rule remains one dynamic quarter rule, then remaining financial rules and all other rules are alphabetically ordered by rule name.
- Duplicate destination names are disambiguated with `/ Mailing-list` or `/ Domain` suffixes.
=======
- Work on branch `codex` unless the user says otherwise.
- Before every commit, rewrite this file from scratch as the current reconstruction prompt.
- Commit after each user-prompted code change using the required three-section commit message format.
- The user asked: first provide a plan and wait for approval before future broad work. For narrow requested edits, implement directly when the request is explicit.
- Do not stage local runtime output such as `verplaats_log.json` unless the user explicitly asks. - Do not stage local runtime output such as `verplaats_log.json` unless the user explicitly asks.
## Local Data And Secrets ## Local Data And Secrets
@@ -108,8 +178,58 @@ Daily report:
- `dagelijks_overzicht.py` builds an HTML digest for a selected date, defaulting to yesterday. - `dagelijks_overzicht.py` builds an HTML digest for a selected date, defaulting to yesterday.
- It supports `--account` and `--mail-to`. - It supports `--account` and `--mail-to`.
- It searches sorted folders, excluding inbox/system/archive/technical folders.
- It sends an HTML email via SMTP.
- Report rows fetch `Message-ID` and render the subject as an Apple Mail `message://` link when a `Message-ID` is present.
- Apple Mail links are macOS-oriented and depend on Apple Mail having the message synced/indexed locally.
- Report HTML escapes folder labels, senders, subjects, and link attributes.
Historical reorder:
- `verplaats_bestaand.py` defaults to dry-run. Real moves require `--uitvoeren` and interactive `JA`.
- Account selection is explicit with `--account <naam>`, for example `--account hans`.
- `--audit` is available in dry-run mode.
- It logs to `verplaats_log.json`.
Unsubscribe:
- `unsubscribe.py` reads `decisions.json`.
- It processes entries with `keuze == "a"` and a `List-Unsubscribe` value.
- It logs progress to `unsubscribe_log.json`.
- It may perform HTTP unsubscribe requests or send mailto unsubscribe messages via SMTP, so the user should run it personally.
## NAS Daily Report
For the current NAS setup, only the daily report needs scheduling. A simple cron entry is enough:
```cron
0 6 * * * cd /share/homes/mailcat/mailcat && /opt/bin/python3 dagelijks_overzicht.py --account hans --mail-to hans@australius.nl >> /share/homes/mailcat/mailcat/daily_report.out 2>&1
```
Use the NAS-specific crontab mechanism if QNAP overwrites normal crontabs after reboot.
## Verification Commands
Latest local verification should include:
```sh
python3 -m py_compile config.py imap_utils.py mail_routes.py maak_mappen.py verplaats_bestaand.py dagelijks_overzicht.py unsubscribe.py
python3 -m unittest discover -s tests
sievec -c /tmp/mailcat-sieve-test/dovecot.conf mailcat_roundcube.sieve /tmp/mailcat_roundcube.svbin
git diff --check
```
If `/tmp/mailcat-sieve-test/dovecot.conf` does not exist, recreate it for Dovecot 2.4.0:
```sh
mkdir -p /tmp/mailcat-sieve-test
printf 'dovecot_config_version = 2.4.0\ndovecot_storage_version = 2.4.0\nssl = no\n' > /tmp/mailcat-sieve-test/dovecot.conf
=======
- The NAS daily report target is `hans@australius.nl`. - The NAS daily report target is `hans@australius.nl`.
- It sends a report even when there are zero messages. - It sends a report even when there are zero messages.
- Report rows fetch `Message-ID` and render the subject as an Apple Mail `message://` link when a `Message-ID` is present.
- Apple Mail links are macOS-oriented. They depend on Apple Mail having the message synced/indexed locally; iOS Mail should not be treated as reliable for these links.
- Report HTML escapes folder labels, senders, subjects, and link attributes.
## Web Route Management ## Web Route Management
@@ -145,6 +265,8 @@ Security note:
## NAS Deployment ## NAS Deployment
The files have already been deployed to the NAS and the webserver is running.
`deploy_vps.sh` is host-neutral despite its historical filename. `deploy_vps.sh` is host-neutral despite its historical filename.
For the QNAP-like NAS, use: For the QNAP-like NAS, use:
@@ -188,6 +310,7 @@ In `SERVICE_MANAGER=plain` mode:
- The sorter appends wrapper stdout/stderr to `$LOG_DIR/daemon.out`. - The sorter appends wrapper stdout/stderr to `$LOG_DIR/daemon.out`.
- The web UI appends stdout/stderr to `$LOG_DIR/web.out`. - The web UI appends stdout/stderr to `$LOG_DIR/web.out`.
- The daily report wrapper appends stdout/stderr to `$LOG_DIR/daily_report.out`. - The daily report wrapper appends stdout/stderr to `$LOG_DIR/daily_report.out`.
- `stop_mailcat.sh` and `stop_web.sh` in plain mode now send SIGTERM, wait up to 20 seconds, then fall back to SIGKILL and wait up to 5 seconds before reporting failure.
Before starting services on the NAS: Before starting services on the NAS:
@@ -224,15 +347,18 @@ Treat that as a successful backup historical-sorter run with no immediate recove
## Verification Commands ## Verification Commands
Latest local verification should include: Latest local verification included:
```sh ```sh
python3 -m unittest tests.test_dagelijks_overzicht
python3 -m py_compile *.py tests/*.py python3 -m py_compile *.py tests/*.py
bash -n deploy_vps.sh bash -n deploy_vps.sh
python3 -m unittest discover -s tests python3 -m unittest discover -s tests
git diff --check git diff --check
``` ```
After the plain-mode stop wrapper change, `bash -n deploy_vps.sh` was rerun successfully.
Useful routing sanity check: Useful routing sanity check:
```sh ```sh
@@ -249,8 +375,8 @@ PY
Likely next steps: Likely next steps:
1. Deploy the updated code to the NAS in `SERVICE_MANAGER=plain` mode. 1. Deploy the Apple Mail link update and strengthened stop wrappers to the NAS.
2. Create remote `web_config.json`. 2. If an already-running sorter will not stop with the old wrapper, stop it manually by reading `$RUN_DIR/mailcat.pid` and sending TERM/KILL to that PID, then remove the stale PID file.
3. Start the web UI with `/share/homes/mailcat/mailcat/bin/start_web.sh`. 3. Run `/share/homes/mailcat/mailcat/bin/run_daily_report.sh` manually on the NAS to send a test digest.
4. Open `http://<nas-host-or-ip>:4321/` from the internal network and verify Basic Auth. 4. Open the digest on macOS Mail and verify subject links open local Apple Mail messages.
5. Make a harmless route edit, verify `domain_routes.json` and `domain_routes.json.bak`, then restart the sorter if the live daemon is running. 5. If iOS/web reliability is needed later, add Roundcube links using mailbox plus IMAP UID, or add a read-only Mailcat message viewer.
+12
View File
@@ -0,0 +1,12 @@
require ["copy","date","fileinto","regex"];
# rule:[kwartaalmail]
if allof (date :regex "Date" "month" "[789]", date :is "Date" "year" "2026")
{
fileinto :copy "INBOX.Notes";
}
# rule:[zuiderzee]
if allof (header :contains "from" "zuiderzee")
{
fileinto :copy "INBOX.Notes";
}
+1
View File
@@ -0,0 +1 @@
12747
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+41
View File
@@ -0,0 +1,41 @@
import unittest
from dagelijks_overzicht import apple_mail_url, bouw_html
class DagelijksOverzichtTests(unittest.TestCase):
def test_apple_mail_url_encodes_message_id(self):
self.assertEqual(
apple_mail_url("<abc.123@example.com>"),
"message://%3Cabc.123%40example.com%3E",
)
def test_apple_mail_url_wraps_bare_message_id(self):
self.assertEqual(
apple_mail_url("abc.123@example.com"),
"message://%3Cabc.123%40example.com%3E",
)
def test_bouw_html_links_subject_and_escapes_headers(self):
html = bouw_html(
{
"INBOX.Test & Folder": [
{
"time": "08:15",
"from": "Alice <admin>",
"subject": "Factuur <juli>",
"message_id": "<msg@example.com>",
}
]
},
__import__("datetime").date(2026, 7, 5),
)
self.assertIn("message://%3Cmsg%40example.com%3E", html)
self.assertIn("Alice &lt;admin&gt;", html)
self.assertIn("Factuur &lt;juli&gt;", html)
self.assertIn("Test &amp; Folder", html)
if __name__ == "__main__":
unittest.main()
+7977 -3182
View File
File diff suppressed because it is too large Load Diff