v0.143.0
 1from __future__ import annotations
 2
 3import email
 4from email.message import Message
 5from typing import Any
 6
 7from plain.runtime import settings
 8from plain.toolbar import ToolbarItem, register_toolbar_item
 9
10from .backends.preview import EMAIL_DIR
11
12PREVIEW_BACKEND = "plain.email.backends.preview.EmailBackend"
13MAX_MESSAGES = 20
14
15
16@register_toolbar_item
17class EmailToolbarItem(ToolbarItem):
18    name = "Email"
19    panel_template_name = "toolbar/email.html"
20
21    def is_enabled(self) -> bool:
22        return settings.EMAIL_BACKEND == PREVIEW_BACKEND
23
24    def get_template_context(self) -> dict[str, Any]:
25        context = super().get_template_context()
26        context["emails"] = _load_recent_messages()
27        context["email_file_path"] = str(EMAIL_DIR)
28        return context
29
30
31def _load_recent_messages() -> list[dict[str, Any]]:
32    eml_files = sorted(EMAIL_DIR.glob("*.eml"), reverse=True)[:MAX_MESSAGES]
33
34    messages = []
35    for eml_file in eml_files:
36        with eml_file.open("rb") as f:
37            mime = email.message_from_binary_file(f)
38
39        html_body, text_body = _extract_bodies(mime)
40
41        messages.append(
42            {
43                "id": eml_file.stem,
44                "from": mime.get("From", ""),
45                "to": mime.get("To", ""),
46                "cc": mime.get("Cc", ""),
47                "subject": mime.get("Subject", "(no subject)"),
48                "date": mime.get("Date", ""),
49                "html_body": html_body,
50                "text_body": text_body,
51                "kind": "html" if html_body else "text",
52            }
53        )
54    return messages
55
56
57def _extract_bodies(mime: Message) -> tuple[str | None, str | None]:
58    html_body: str | None = None
59    text_body: str | None = None
60
61    for part in mime.walk():
62        if part.is_multipart():
63            continue
64        content_type = part.get_content_type()
65        if content_type == "text/html" and html_body is None:
66            html_body = _decode_part(part)
67        elif content_type == "text/plain" and text_body is None:
68            text_body = _decode_part(part)
69
70    return html_body, text_body
71
72
73def _decode_part(part: Message) -> str:
74    payload = part.get_payload(decode=True)
75    if not isinstance(payload, bytes):
76        return ""
77    charset = part.get_content_charset() or "utf-8"
78    return payload.decode(charset, errors="replace")