plain.email 0.11.1__tar.gz → 0.13.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {plain_email-0.11.1 → plain_email-0.13.0}/.gitignore +5 -3
- {plain_email-0.11.1 → plain_email-0.13.0}/PKG-INFO +1 -1
- {plain_email-0.11.1 → plain_email-0.13.0}/plain/email/CHANGELOG.md +21 -0
- {plain_email-0.11.1 → plain_email-0.13.0}/plain/email/backends/base.py +5 -5
- {plain_email-0.11.1 → plain_email-0.13.0}/plain/email/backends/console.py +7 -3
- {plain_email-0.11.1 → plain_email-0.13.0}/plain/email/backends/filebased.py +10 -6
- {plain_email-0.11.1 → plain_email-0.13.0}/plain/email/backends/smtp.py +2 -1
- {plain_email-0.11.1 → plain_email-0.13.0}/plain/email/message.py +7 -7
- {plain_email-0.11.1 → plain_email-0.13.0}/pyproject.toml +1 -1
- {plain_email-0.11.1 → plain_email-0.13.0}/LICENSE +0 -0
- {plain_email-0.11.1 → plain_email-0.13.0}/README.md +0 -0
- {plain_email-0.11.1 → plain_email-0.13.0}/plain/email/README.md +0 -0
- {plain_email-0.11.1 → plain_email-0.13.0}/plain/email/__init__.py +0 -0
- {plain_email-0.11.1 → plain_email-0.13.0}/plain/email/backends/__init__.py +0 -0
- {plain_email-0.11.1 → plain_email-0.13.0}/plain/email/default_settings.py +0 -0
- {plain_email-0.11.1 → plain_email-0.13.0}/plain/email/utils.py +0 -0
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# plain-email changelog
|
|
2
2
|
|
|
3
|
+
## [0.13.0](https://github.com/dropseed/plain/releases/plain-email@0.13.0) (2025-12-04)
|
|
4
|
+
|
|
5
|
+
### What's changed
|
|
6
|
+
|
|
7
|
+
- Internal typing improvements for better type checker compatibility ([ac1eeb0](https://github.com/dropseed/plain/commit/ac1eeb0ea05b26dfc7e32c50f2a5a5bc7e098ceb))
|
|
8
|
+
|
|
9
|
+
### Upgrade instructions
|
|
10
|
+
|
|
11
|
+
- No changes required
|
|
12
|
+
|
|
13
|
+
## [0.12.0](https://github.com/dropseed/plain/releases/plain-email@0.12.0) (2025-11-12)
|
|
14
|
+
|
|
15
|
+
### What's changed
|
|
16
|
+
|
|
17
|
+
- The filebased email backend now requires `EMAIL_FILE_PATH` to be set and raises `ImproperlyConfigured` if not provided ([f4dbcef](https://github.com/dropseed/plain/commit/f4dbcefa929058be517cb1d4ab35bd73a89f26b8))
|
|
18
|
+
- `BaseEmailBackend` now uses Python's abstract base class with `@abstractmethod` for better type checking ([245b5f4](https://github.com/dropseed/plain/commit/245b5f472c89178b8b764869f1624f8fc885b0f7))
|
|
19
|
+
|
|
20
|
+
### Upgrade instructions
|
|
21
|
+
|
|
22
|
+
- If using the filebased email backend, ensure `EMAIL_FILE_PATH` is configured in your settings or passed when initializing the backend
|
|
23
|
+
|
|
3
24
|
## [0.11.1](https://github.com/dropseed/plain/releases/plain-email@0.11.1) (2025-10-06)
|
|
4
25
|
|
|
5
26
|
### What's changed
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
5
6
|
from typing import TYPE_CHECKING, Any
|
|
6
7
|
|
|
7
8
|
if TYPE_CHECKING:
|
|
@@ -10,7 +11,7 @@ if TYPE_CHECKING:
|
|
|
10
11
|
from ..message import EmailMessage
|
|
11
12
|
|
|
12
13
|
|
|
13
|
-
class BaseEmailBackend:
|
|
14
|
+
class BaseEmailBackend(ABC):
|
|
14
15
|
"""
|
|
15
16
|
Base class for email backend implementations.
|
|
16
17
|
|
|
@@ -27,7 +28,7 @@ class BaseEmailBackend:
|
|
|
27
28
|
def __init__(self, fail_silently: bool = False, **kwargs: Any) -> None:
|
|
28
29
|
self.fail_silently = fail_silently
|
|
29
30
|
|
|
30
|
-
def open(self) -> None:
|
|
31
|
+
def open(self) -> bool | None:
|
|
31
32
|
"""
|
|
32
33
|
Open a network connection.
|
|
33
34
|
|
|
@@ -66,11 +67,10 @@ class BaseEmailBackend:
|
|
|
66
67
|
) -> None:
|
|
67
68
|
self.close()
|
|
68
69
|
|
|
70
|
+
@abstractmethod
|
|
69
71
|
def send_messages(self, email_messages: list[EmailMessage]) -> int:
|
|
70
72
|
"""
|
|
71
73
|
Send one or more EmailMessage objects and return the number of email
|
|
72
74
|
messages sent.
|
|
73
75
|
"""
|
|
74
|
-
|
|
75
|
-
"subclasses of BaseEmailBackend must override send_messages() method"
|
|
76
|
-
)
|
|
76
|
+
...
|
|
@@ -23,9 +23,13 @@ class EmailBackend(BaseEmailBackend):
|
|
|
23
23
|
def write_message(self, message: EmailMessage) -> None:
|
|
24
24
|
msg = message.message()
|
|
25
25
|
msg_data = msg.as_bytes()
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
msg_charset = msg.get_charset()
|
|
27
|
+
if msg_charset is None:
|
|
28
|
+
charset = "utf-8"
|
|
29
|
+
elif isinstance(msg_charset, str):
|
|
30
|
+
charset = msg_charset
|
|
31
|
+
else:
|
|
32
|
+
charset = msg_charset.get_output_charset() or "utf-8"
|
|
29
33
|
msg_data = msg_data.decode(charset)
|
|
30
34
|
self.stream.write(f"{msg_data}\n")
|
|
31
35
|
self.stream.write("-" * 79)
|
|
@@ -16,13 +16,16 @@ if TYPE_CHECKING:
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
class EmailBackend(ConsoleEmailBackend):
|
|
19
|
+
file_path: str # Set during __init__, validated to be non-None
|
|
20
|
+
|
|
19
21
|
def __init__(self, *args: Any, file_path: str | None = None, **kwargs: Any) -> None:
|
|
20
|
-
self._fname = None
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
self._fname: str | None = None
|
|
23
|
+
_file_path: str | None = file_path or getattr(settings, "EMAIL_FILE_PATH", None)
|
|
24
|
+
if not _file_path:
|
|
25
|
+
raise ImproperlyConfigured(
|
|
26
|
+
"EMAIL_FILE_PATH must be set for the filebased email backend"
|
|
27
|
+
)
|
|
28
|
+
self.file_path = os.path.abspath(_file_path)
|
|
26
29
|
try:
|
|
27
30
|
os.makedirs(self.file_path, exist_ok=True)
|
|
28
31
|
except FileExistsError:
|
|
@@ -45,6 +48,7 @@ class EmailBackend(ConsoleEmailBackend):
|
|
|
45
48
|
super().__init__(*args, **kwargs)
|
|
46
49
|
|
|
47
50
|
def write_message(self, message: EmailMessage) -> None:
|
|
51
|
+
assert self.stream is not None, "stream should be opened before writing"
|
|
48
52
|
self.stream.write(message.message().as_bytes() + b"\n")
|
|
49
53
|
self.stream.write(b"-" * 79)
|
|
50
54
|
self.stream.write(b"\n")
|
|
@@ -84,7 +84,7 @@ class EmailBackend(BaseEmailBackend):
|
|
|
84
84
|
|
|
85
85
|
# If local_hostname is not specified, socket.getfqdn() gets used.
|
|
86
86
|
# For performance, we use the cached FQDN for local_hostname.
|
|
87
|
-
connection_params = {"local_hostname": DNS_NAME.get_fqdn()}
|
|
87
|
+
connection_params: dict[str, Any] = {"local_hostname": DNS_NAME.get_fqdn()}
|
|
88
88
|
if self.timeout is not None:
|
|
89
89
|
connection_params["timeout"] = self.timeout
|
|
90
90
|
if self.use_ssl:
|
|
@@ -158,6 +158,7 @@ class EmailBackend(BaseEmailBackend):
|
|
|
158
158
|
sanitize_address(addr, encoding) for addr in email_message.recipients()
|
|
159
159
|
]
|
|
160
160
|
message = email_message.message()
|
|
161
|
+
assert self.connection is not None, "connection should be open before sending"
|
|
161
162
|
try:
|
|
162
163
|
self.connection.sendmail(
|
|
163
164
|
from_email, recipients, message.as_bytes(linesep="\r\n")
|
|
@@ -181,7 +181,7 @@ class SafeMIMEText(MIMEMixin, MIMEText):
|
|
|
181
181
|
name, val = forbid_multi_line_headers(name, val, self.encoding)
|
|
182
182
|
MIMEText.__setitem__(self, name, val)
|
|
183
183
|
|
|
184
|
-
def set_payload(
|
|
184
|
+
def set_payload( # type: ignore[override]
|
|
185
185
|
self, payload: str, charset: str | Charset.Charset | None = None
|
|
186
186
|
) -> None:
|
|
187
187
|
if charset == "utf-8" and not isinstance(charset, Charset.Charset):
|
|
@@ -349,11 +349,10 @@ class EmailMessage:
|
|
|
349
349
|
elif content is None:
|
|
350
350
|
raise ValueError("content must be provided.")
|
|
351
351
|
else:
|
|
352
|
-
mimetype
|
|
353
|
-
mimetype
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
)
|
|
352
|
+
if filename is not None and mimetype is None:
|
|
353
|
+
mimetype = mimetypes.guess_type(filename)[0]
|
|
354
|
+
if mimetype is None:
|
|
355
|
+
mimetype = DEFAULT_ATTACHMENT_MIME_TYPE
|
|
357
356
|
basetype, subtype = mimetype.split("/", 1)
|
|
358
357
|
|
|
359
358
|
if basetype == "text":
|
|
@@ -434,12 +433,13 @@ class EmailMessage:
|
|
|
434
433
|
else:
|
|
435
434
|
# Encode non-text attachments with base64.
|
|
436
435
|
attachment = MIMEBase(basetype, subtype)
|
|
436
|
+
assert isinstance(content, bytes), "Non-text attachments must be bytes"
|
|
437
437
|
attachment.set_payload(content)
|
|
438
438
|
Encoders.encode_base64(attachment)
|
|
439
439
|
return attachment
|
|
440
440
|
|
|
441
441
|
def _create_attachment(
|
|
442
|
-
self, filename: str | None, content: str | bytes, mimetype: str
|
|
442
|
+
self, filename: str | None, content: str | bytes, mimetype: str
|
|
443
443
|
) -> SafeMIMEText | SafeMIMEMessage | MIMEBase:
|
|
444
444
|
"""
|
|
445
445
|
Convert the filename, content, mimetype triple into a MIME attachment
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|