diff --git a/dagelijks_overzicht.py b/dagelijks_overzicht.py
index 14463e3..33c0608 100644
--- a/dagelijks_overzicht.py
+++ b/dagelijks_overzicht.py
@@ -20,6 +20,8 @@ import sys
import ssl
import smtplib
import re
+import html as html_lib
+import urllib.parse
import config
from datetime import date, timedelta, datetime
from pathlib import Path
@@ -99,6 +101,16 @@ def leesbare_map(imap_naam: str) -> str:
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="")
+
+
# ── IMAP ophalen ──────────────────────────────────────────────────────────────
def haal_berichten_op(mail: imaplib.IMAP4_SSL, zoekdatum: date) -> dict[str, list[dict]]:
@@ -138,7 +150,7 @@ def haal_berichten_op(mail: imaplib.IMAP4_SSL, zoekdatum: date) -> dict[str, lis
for uid in ids:
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)
if status != "OK" or raw_header is None:
@@ -148,6 +160,7 @@ def haal_berichten_op(mail: imaplib.IMAP4_SSL, zoekdatum: date) -> dict[str, lis
from_raw = decode_hdr(msg.get("From", ""))
subject = decode_hdr(msg.get("Subject", "(geen onderwerp)"))
date_str = msg.get("Date", "")
+ message_id = (msg.get("Message-ID") or "").strip()
# Tijdstip opmaken
try:
@@ -161,9 +174,10 @@ def haal_berichten_op(mail: imaplib.IMAP4_SSL, zoekdatum: date) -> dict[str, lis
afzender = m.group(1).strip() if m else from_raw.split("@")[0]
resultaat[folder].append({
- "from": afzender[:40],
+ "from": afzender[:40],
"subject": subject[:80],
- "time": tijdstip,
+ "time": tijdstip,
+ "message_id": message_id,
})
return resultaat
@@ -203,15 +217,23 @@ def bouw_html(berichten: dict[str, list[dict]], zoekdatum: date) -> str:
for folder in sorted(berichten.keys()):
items = berichten[folder]
- label = leesbare_map(folder)
+ label = html_lib.escape(leesbare_map(folder))
html += f'
{label} ({len(items)})
\n'
html += '\n'
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'{subject}'
+ else:
+ subject_html = subject
html += (
f''
- f'| {item["time"]} | '
- f'{item["from"]} | '
- f'{item["subject"]} | '
+ f'{time} | '
+ f'{sender} | '
+ f'{subject_html} | '
f'
\n'
)
html += '
\n'
diff --git a/restart_prompt.md b/restart_prompt.md
index cbd8390..7372e1c 100644
--- a/restart_prompt.md
+++ b/restart_prompt.md
@@ -16,7 +16,7 @@ Do not run mailbox-affecting scripts yourself unless the user explicitly authori
- 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 doing future work.
+- 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.
## Local Data And Secrets
@@ -110,6 +110,9 @@ Daily report:
- It supports `--account` and `--mail-to`.
- The NAS daily report target is `hans@australius.nl`.
- 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
@@ -145,6 +148,8 @@ Security note:
## 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.
For the QNAP-like NAS, use:
@@ -224,9 +229,10 @@ Treat that as a successful backup historical-sorter run with no immediate recove
## Verification Commands
-Latest local verification should include:
+Latest local verification included:
```sh
+python3 -m unittest tests.test_dagelijks_overzicht
python3 -m py_compile *.py tests/*.py
bash -n deploy_vps.sh
python3 -m unittest discover -s tests
@@ -249,8 +255,7 @@ PY
Likely next steps:
-1. Deploy the updated code to the NAS in `SERVICE_MANAGER=plain` mode.
-2. Create remote `web_config.json`.
-3. Start the web UI with `/share/homes/mailcat/mailcat/bin/start_web.sh`.
-4. Open `http://:4321/` from the internal network and verify Basic Auth.
-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.
+1. Deploy the Apple Mail link update to the NAS if the user wants it live there.
+2. Run `/share/homes/mailcat/mailcat/bin/run_daily_report.sh` manually on the NAS to send a test digest.
+3. Open the digest on macOS Mail and verify subject links open local Apple Mail messages.
+4. If iOS/web reliability is needed later, add Roundcube links using mailbox plus IMAP UID, or add a read-only Mailcat message viewer.
diff --git a/tests/test_dagelijks_overzicht.py b/tests/test_dagelijks_overzicht.py
new file mode 100644
index 0000000..b892277
--- /dev/null
+++ b/tests/test_dagelijks_overzicht.py
@@ -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(""),
+ "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 ",
+ "subject": "Factuur ",
+ "message_id": "",
+ }
+ ]
+ },
+ __import__("datetime").date(2026, 7, 5),
+ )
+
+ self.assertIn("message://%3Cmsg%40example.com%3E", html)
+ self.assertIn("Alice <admin>", html)
+ self.assertIn("Factuur <juli>", html)
+ self.assertIn("Test & Folder", html)
+
+
+if __name__ == "__main__":
+ unittest.main()