66 lines
3.1 KiB
Markdown
66 lines
3.1 KiB
Markdown
# Daily Tech Digest — HTML Email with Clickable Headlines
|
|
|
|
The script at `/root/.hermes/scripts/daily-feed-summary.py` collects RSS feeds (XDA, MakeUseOf, How-To Geek, 9to5Mac, HotHardware, Gizmodo, Top Gear) and sends Germaine a daily digest.
|
|
|
|
## Problem: headlines not clickable
|
|
|
|
The original script sent a `MIMEText(body, 'plain')` message. Markdown links `[title](url)` rendered as literal text — the user had to copy-paste URLs manually.
|
|
|
|
## Fix: multipart/alternative with markdown-to-HTML conversion
|
|
|
|
```python
|
|
from email.mime.text import MIMEText
|
|
from email.mime.multipart import MIMEMultipart
|
|
import re
|
|
|
|
def markdown_to_html(text):
|
|
"""Convert markdown links, bold, italic, headers to email-safe HTML."""
|
|
# Links: [text](url) → <a href="url">text</a>
|
|
html = re.sub(
|
|
r'\[([^\]]+)\]\(([^)]+)\)',
|
|
r'<a href="\2" style="color:#2563eb;text-decoration:underline;">\1</a>',
|
|
text
|
|
)
|
|
# Bold: **text** → <strong>text</strong>
|
|
html = re.sub(r'\*\*([^*]+)\*\*', r'<strong>\1</strong>', html)
|
|
# Italic: _text_ → <em>text</em>
|
|
html = re.sub(r'_([^_]+)_', r'<em>\1</em>', html)
|
|
# H2: ## Header
|
|
html = re.sub(r'^## (.+)$', r'<h2 style="font-size:16px;margin:16px 0 4px;color:#1a1a2e;">\1</h2>', html, flags=re.MULTILINE)
|
|
# H1: # Header
|
|
html = re.sub(r'^# (.+)$', r'<h1 style="font-size:20px;margin:0 0 8px;color:#1a1a2e;">\1</h1>', html, flags=re.MULTILINE)
|
|
# HR: ---
|
|
html = re.sub(r'^---+$', r'<hr style="border:none;border-top:1px solid #e2e8f0;margin:16px 0;">', html, flags=re.MULTILINE)
|
|
return html
|
|
|
|
def send_html_digest(plain_body, subject):
|
|
"""Send multipart/alternative digest."""
|
|
html_content = f'''<!DOCTYPE html>
|
|
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width">
|
|
<style>
|
|
body{{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;font-size:13px;line-height:1.6;color:#333;padding:20px;max-width:640px;margin:0 auto;}}
|
|
a{{color:#2563eb;}}p{{margin:4px 0;}}
|
|
</style></head><body>
|
|
<pre style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;font-size:13px;line-height:1.6;white-space:pre-wrap;word-wrap:break-word;">{plain_body}</pre>
|
|
</body></html>'''
|
|
|
|
msg = MIMEMultipart('alternative')
|
|
msg['Subject'] = subject
|
|
msg['From'] = 'g@germainebrown.com'
|
|
msg['To'] = 'g@germainebrown.com'
|
|
msg.attach(MIMEText(plain_body, 'plain', 'utf-8'))
|
|
msg.attach(MIMEText(html_content, 'html', 'utf-8'))
|
|
# ... SMTP send via port 2525
|
|
```
|
|
|
|
## Key design decisions
|
|
|
|
- **multipart/alternative** — includes both text/plain (for clients that can't render HTML) and text/html (for clickable links)
|
|
- **Inline styles** — email clients strip `<style>` blocks, so font, color, and spacing are applied inline via the `<pre>` element
|
|
- **`white-space: pre-wrap`** — preserves the monospaced markdown layout in HTML (indentation, line breaks, table alignment)
|
|
- **Color #2563eb (blue)** — standard link color, renders well in both light and dark email themes
|
|
|
|
## SMTP config
|
|
|
|
Using `mail.germainebrown.com:2525` with STARTTLS. Port 587 is blocked from the netcup VPS.
|