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")