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.
This commit is contained in:
2026-07-05 11:22:03 +02:00
parent ae0813d519
commit 99cd891b1e
3 changed files with 82 additions and 14 deletions
+27 -5
View File
@@ -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:
@@ -164,6 +177,7 @@ def haal_berichten_op(mail: imaplib.IMAP4_SSL, zoekdatum: date) -> dict[str, lis
"from": afzender[:40],
"subject": subject[:80],
"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'<h2>{label} <span style="font-weight:normal;color:#888">({len(items)})</span></h2>\n'
html += '<table>\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'<a href="{html_lib.escape(url, quote=True)}">{subject}</a>'
else:
subject_html = subject
html += (
f'<tr>'
f'<td class="time">{item["time"]}</td>'
f'<td class="from">{item["from"]}</td>'
f'<td class="subject">{item["subject"]}</td>'
f'<td class="time">{time}</td>'
f'<td class="from">{sender}</td>'
f'<td class="subject">{subject_html}</td>'
f'</tr>\n'
)
html += '</table>\n'
+12 -7
View File
@@ -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://<nas-host-or-ip>: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.
+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()