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'' - f'' - f'' + f'' + f'' + f'' f'\n' ) html += '
{item["time"]}{item["from"]}{item["subject"]}{time}{sender}{subject_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()