New `myclaude mail` group with: - config: interactive account setup - inbox: list recent messages - read: show message by UID - send: send email with attachments - reply/forward: reply to or forward messages
61 lines
1.9 KiB
Python
61 lines
1.9 KiB
Python
"""Mail utility helpers."""
|
|
|
|
import re
|
|
from datetime import datetime
|
|
from email.header import decode_header
|
|
from email.utils import mktime_tz, parsedate_tz
|
|
from typing import Any
|
|
|
|
|
|
def decode_header_value(value: Any) -> str:
|
|
"""Decode email header value safely."""
|
|
if value is None:
|
|
return ""
|
|
raw = value if isinstance(value, str) else str(value)
|
|
parts = decode_header(raw)
|
|
result: list[str] = []
|
|
for part, charset in parts:
|
|
if isinstance(part, bytes):
|
|
result.append(part.decode(charset or "utf-8", errors="replace"))
|
|
else:
|
|
result.append(part)
|
|
return "".join(result)
|
|
|
|
|
|
def format_date(date_str: str | None) -> str:
|
|
"""Format email date string to local-friendly form."""
|
|
if not date_str:
|
|
return ""
|
|
try:
|
|
tt = parsedate_tz(date_str)
|
|
if tt is None:
|
|
return date_str
|
|
ts = mktime_tz(tt)
|
|
dt = datetime.fromtimestamp(ts)
|
|
return dt.strftime("%Y-%m-%d %H:%M")
|
|
except Exception:
|
|
return date_str
|
|
|
|
|
|
def strip_html_tags(html: str) -> str:
|
|
"""Very basic HTML tag stripping for fallback text extraction."""
|
|
text = re.sub(r"<script[^>]*>.*?</script>", "", html, flags=re.S)
|
|
text = re.sub(r"<style[^>]*>.*?</style>", "", text, flags=re.S)
|
|
# Replace block-level tags with newlines before stripping remaining tags
|
|
text = re.sub(r"</(p|div|h[1-6]|li|tr|blockquote)>", "\n", text, flags=re.I)
|
|
text = re.sub(r"<br\s*/?>", "\n", text, flags=re.I)
|
|
text = re.sub(r"<[^>]+>", "", text)
|
|
text = re.sub(r" ", " ", text)
|
|
text = re.sub(r"<", "<", text)
|
|
text = re.sub(r">", ">", text)
|
|
text = re.sub(r"&", "&", text)
|
|
text = re.sub(r"\n\s*\n+", "\n\n", text)
|
|
return text.strip()
|
|
|
|
|
|
def truncate_text(text: str, max_len: int = 60) -> str:
|
|
"""Truncate text with ellipsis."""
|
|
if len(text) <= max_len:
|
|
return text
|
|
return text[: max_len - 1] + "…"
|