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:
+29
-7
@@ -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'<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
@@ -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.
|
||||
|
||||
@@ -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 <admin>", html)
|
||||
self.assertIn("Factuur <juli>", html)
|
||||
self.assertIn("Test & Folder", html)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user