plain.email 0.13.0__py3-none-any.whl → 0.15.0__py3-none-any.whl
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/CHANGELOG.md +21 -0
- plain/email/README.md +264 -3
- plain/email/__init__.py +0 -11
- plain/email/backends/smtp.py +5 -5
- plain/email/default_settings.py +3 -1
- plain/email/message.py +29 -24
- plain/email/utils.py +3 -1
- plain_email-0.15.0.dist-info/METADATA +295 -0
- plain_email-0.15.0.dist-info/RECORD +15 -0
- plain_email-0.13.0.dist-info/METADATA +0 -34
- plain_email-0.13.0.dist-info/RECORD +0 -15
- {plain_email-0.13.0.dist-info → plain_email-0.15.0.dist-info}/WHEEL +0 -0
- {plain_email-0.13.0.dist-info → plain_email-0.15.0.dist-info}/licenses/LICENSE +0 -0
plain/email/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# plain-email changelog
|
|
2
2
|
|
|
3
|
+
## [0.15.0](https://github.com/dropseed/plain/releases/plain-email@0.15.0) (2026-01-15)
|
|
4
|
+
|
|
5
|
+
### What's changed
|
|
6
|
+
|
|
7
|
+
- `EMAIL_HOST_PASSWORD` is now marked as a `Secret` type, ensuring the password is masked when displaying settings in CLI output ([7666190](https://github.com/dropseed/plain/commit/7666190305e13ebd1fc9b536e6415e863c2c0b25))
|
|
8
|
+
|
|
9
|
+
### Upgrade instructions
|
|
10
|
+
|
|
11
|
+
- No changes required
|
|
12
|
+
|
|
13
|
+
## [0.14.0](https://github.com/dropseed/plain/releases/plain-email@0.14.0) (2026-01-13)
|
|
14
|
+
|
|
15
|
+
### What's changed
|
|
16
|
+
|
|
17
|
+
- Simplified public API exports to only include user-facing classes and functions ([28f4849](https://github.com/dropseed/plain/commit/28f4849ca5692acc1dbef97f1590ddbd2f5afe96))
|
|
18
|
+
- Improved README documentation with comprehensive usage examples and installation instructions ([da37a78](https://github.com/dropseed/plain/commit/da37a78fbb8a683c65863f4d0b7af9af5b16feec))
|
|
19
|
+
|
|
20
|
+
### Upgrade instructions
|
|
21
|
+
|
|
22
|
+
- No changes required
|
|
23
|
+
|
|
3
24
|
## [0.13.0](https://github.com/dropseed/plain/releases/plain-email@0.13.0) (2025-12-04)
|
|
4
25
|
|
|
5
26
|
### What's changed
|
plain/email/README.md
CHANGED
|
@@ -1,18 +1,263 @@
|
|
|
1
1
|
# plain.email
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Send emails from your Plain application using SMTP, console output, or file-based backends.**
|
|
4
4
|
|
|
5
|
+
- [Overview](#overview)
|
|
6
|
+
- [Sending a simple email](#sending-a-simple-email)
|
|
7
|
+
- [Sending HTML emails](#sending-html-emails)
|
|
8
|
+
- [Template-based emails](#template-based-emails)
|
|
9
|
+
- [Attachments](#attachments)
|
|
10
|
+
- [Configuration](#configuration)
|
|
11
|
+
- [SMTP settings](#smtp-settings)
|
|
12
|
+
- [Email backends](#email-backends)
|
|
13
|
+
- [SMTP backend](#smtp-backend)
|
|
14
|
+
- [Console backend](#console-backend)
|
|
15
|
+
- [File-based backend](#file-based-backend)
|
|
16
|
+
- [FAQs](#faqs)
|
|
5
17
|
- [Installation](#installation)
|
|
6
18
|
|
|
19
|
+
## Overview
|
|
20
|
+
|
|
21
|
+
You can send emails using the `send_mail` function for simple cases, or use the `EmailMessage` and `EmailMultiAlternatives` classes for more control. For template-based emails, the `TemplateEmail` class renders HTML templates automatically.
|
|
22
|
+
|
|
23
|
+
### Sending a simple email
|
|
24
|
+
|
|
25
|
+
The `send_mail` function is the easiest way to send an email.
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from plain.email import send_mail
|
|
29
|
+
|
|
30
|
+
send_mail(
|
|
31
|
+
subject="Welcome!",
|
|
32
|
+
message="Thanks for signing up.",
|
|
33
|
+
from_email="hello@example.com",
|
|
34
|
+
recipient_list=["user@example.com"],
|
|
35
|
+
)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
To include an HTML version along with the plain text:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
send_mail(
|
|
42
|
+
subject="Welcome!",
|
|
43
|
+
message="Thanks for signing up.",
|
|
44
|
+
from_email="hello@example.com",
|
|
45
|
+
recipient_list=["user@example.com"],
|
|
46
|
+
html_message="<h1>Thanks for signing up!</h1>",
|
|
47
|
+
)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Sending HTML emails
|
|
51
|
+
|
|
52
|
+
For more control over multipart emails, use `EmailMultiAlternatives`.
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from plain.email import EmailMultiAlternatives
|
|
56
|
+
|
|
57
|
+
email = EmailMultiAlternatives(
|
|
58
|
+
subject="Your order confirmation",
|
|
59
|
+
body="Your order #123 has been confirmed.",
|
|
60
|
+
from_email="orders@example.com",
|
|
61
|
+
to=["customer@example.com"],
|
|
62
|
+
)
|
|
63
|
+
email.attach_alternative("<h1>Order #123 Confirmed</h1>", "text/html")
|
|
64
|
+
email.send()
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Template-based emails
|
|
68
|
+
|
|
69
|
+
The `TemplateEmail` class renders emails from template files. You provide a template name, and it looks for corresponding files in your `templates/email/` directory:
|
|
70
|
+
|
|
71
|
+
- `email/{template}.html` - HTML content (required)
|
|
72
|
+
- `email/{template}.txt` - Plain text content (optional, falls back to stripping HTML tags)
|
|
73
|
+
- `email/{template}.subject.txt` - Subject line (optional)
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from plain.email import TemplateEmail
|
|
77
|
+
|
|
78
|
+
email = TemplateEmail(
|
|
79
|
+
template="welcome",
|
|
80
|
+
context={"user_name": "Alice"},
|
|
81
|
+
to=["alice@example.com"],
|
|
82
|
+
)
|
|
83
|
+
email.send()
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
With these template files:
|
|
87
|
+
|
|
88
|
+
```html
|
|
89
|
+
<!-- templates/email/welcome.html -->
|
|
90
|
+
<h1>Welcome, {{ user_name }}!</h1>
|
|
91
|
+
<p>We're glad you're here.</p>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
{# templates/email/welcome.subject.txt #}
|
|
96
|
+
Welcome to our app, {{ user_name }}!
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
You can subclass `TemplateEmail` to customize the template context by overriding `get_template_context()`.
|
|
100
|
+
|
|
101
|
+
### Attachments
|
|
102
|
+
|
|
103
|
+
You can attach files to any email message.
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from plain.email import EmailMessage
|
|
107
|
+
|
|
108
|
+
email = EmailMessage(
|
|
109
|
+
subject="Your report",
|
|
110
|
+
body="Please find your report attached.",
|
|
111
|
+
to=["user@example.com"],
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Attach content directly
|
|
115
|
+
email.attach("report.csv", csv_content, "text/csv")
|
|
116
|
+
|
|
117
|
+
# Or attach a file from disk
|
|
118
|
+
email.attach_file("/path/to/report.pdf")
|
|
119
|
+
|
|
120
|
+
email.send()
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Configuration
|
|
124
|
+
|
|
125
|
+
Configure email settings in your `settings.py`:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
# settings.py
|
|
129
|
+
|
|
130
|
+
# Required: The backend to use for sending emails
|
|
131
|
+
EMAIL_BACKEND = "plain.email.backends.smtp.EmailBackend"
|
|
132
|
+
|
|
133
|
+
# Required: Default "From" address for outgoing emails
|
|
134
|
+
EMAIL_DEFAULT_FROM = "noreply@example.com"
|
|
135
|
+
|
|
136
|
+
# Optional: Default "Reply-To" addresses
|
|
137
|
+
EMAIL_DEFAULT_REPLY_TO = ["support@example.com"]
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### SMTP settings
|
|
141
|
+
|
|
142
|
+
When using the SMTP backend, configure your mail server:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
# settings.py
|
|
146
|
+
|
|
147
|
+
EMAIL_HOST = "smtp.example.com"
|
|
148
|
+
EMAIL_PORT = 587
|
|
149
|
+
EMAIL_HOST_USER = "your-username"
|
|
150
|
+
EMAIL_HOST_PASSWORD = "your-password"
|
|
151
|
+
EMAIL_USE_TLS = True # Use STARTTLS
|
|
152
|
+
EMAIL_USE_SSL = False # Use implicit SSL (mutually exclusive with TLS)
|
|
153
|
+
|
|
154
|
+
# Optional settings
|
|
155
|
+
EMAIL_TIMEOUT = 10 # Connection timeout in seconds
|
|
156
|
+
EMAIL_SSL_CERTFILE = None # Path to SSL certificate file
|
|
157
|
+
EMAIL_SSL_KEYFILE = None # Path to SSL key file
|
|
158
|
+
EMAIL_USE_LOCALTIME = False # Use local time in Date header (default: UTC)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Email backends
|
|
162
|
+
|
|
163
|
+
The `EMAIL_BACKEND` setting controls how emails are sent. Plain includes three backends.
|
|
164
|
+
|
|
165
|
+
### SMTP backend
|
|
166
|
+
|
|
167
|
+
The default backend sends emails via SMTP.
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
EMAIL_BACKEND = "plain.email.backends.smtp.EmailBackend"
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Console backend
|
|
174
|
+
|
|
175
|
+
Prints emails to the console instead of sending them. Useful during development.
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
EMAIL_BACKEND = "plain.email.backends.console.EmailBackend"
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### File-based backend
|
|
182
|
+
|
|
183
|
+
Writes emails to files in a directory. Useful for testing and debugging.
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
EMAIL_BACKEND = "plain.email.backends.filebased.EmailBackend"
|
|
187
|
+
EMAIL_FILE_PATH = "/path/to/email-output"
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Each email is saved to a timestamped `.log` file in the specified directory.
|
|
191
|
+
|
|
192
|
+
## FAQs
|
|
193
|
+
|
|
194
|
+
#### How do I send to multiple recipients efficiently?
|
|
195
|
+
|
|
196
|
+
Use `send_mass_mail` to send multiple messages over a single connection:
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
from plain.email import send_mass_mail
|
|
200
|
+
|
|
201
|
+
messages = (
|
|
202
|
+
("Subject 1", "Body 1", "from@example.com", ["to1@example.com"]),
|
|
203
|
+
("Subject 2", "Body 2", "from@example.com", ["to2@example.com"]),
|
|
204
|
+
)
|
|
205
|
+
send_mass_mail(messages)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
#### How do I reuse a connection for multiple emails?
|
|
209
|
+
|
|
210
|
+
Use the backend as a context manager:
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
from plain.email import get_connection, EmailMessage
|
|
214
|
+
|
|
215
|
+
with get_connection() as connection:
|
|
216
|
+
for user in users:
|
|
217
|
+
email = EmailMessage(
|
|
218
|
+
subject="Hello",
|
|
219
|
+
body="Hi there!",
|
|
220
|
+
to=[user.email],
|
|
221
|
+
connection=connection,
|
|
222
|
+
)
|
|
223
|
+
email.send()
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
#### How do I add custom headers?
|
|
227
|
+
|
|
228
|
+
Pass a `headers` dict to any email class:
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
email = EmailMessage(
|
|
232
|
+
subject="Hello",
|
|
233
|
+
body="Content",
|
|
234
|
+
to=["user@example.com"],
|
|
235
|
+
headers={"X-Custom-Header": "value", "Reply-To": "reply@example.com"},
|
|
236
|
+
)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
#### How do I create a custom email backend?
|
|
240
|
+
|
|
241
|
+
Subclass [`BaseEmailBackend`](./backends/base.py#BaseEmailBackend) and implement the `send_messages` method:
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
from plain.email.backends.base import BaseEmailBackend
|
|
245
|
+
|
|
246
|
+
class MyBackend(BaseEmailBackend):
|
|
247
|
+
def send_messages(self, email_messages):
|
|
248
|
+
# Your sending logic here
|
|
249
|
+
return len(email_messages)
|
|
250
|
+
```
|
|
251
|
+
|
|
7
252
|
## Installation
|
|
8
253
|
|
|
9
|
-
Install the `plain.email` package from
|
|
254
|
+
Install the `plain.email` package from PyPI:
|
|
10
255
|
|
|
11
256
|
```bash
|
|
12
257
|
uv add plain.email
|
|
13
258
|
```
|
|
14
259
|
|
|
15
|
-
Add `plain.email` to your `INSTALLED_PACKAGES
|
|
260
|
+
Add `plain.email` to your `INSTALLED_PACKAGES` and configure the required settings:
|
|
16
261
|
|
|
17
262
|
```python
|
|
18
263
|
# settings.py
|
|
@@ -20,4 +265,20 @@ INSTALLED_PACKAGES = [
|
|
|
20
265
|
# ...
|
|
21
266
|
"plain.email",
|
|
22
267
|
]
|
|
268
|
+
|
|
269
|
+
EMAIL_BACKEND = "plain.email.backends.smtp.EmailBackend"
|
|
270
|
+
EMAIL_DEFAULT_FROM = "noreply@example.com"
|
|
271
|
+
|
|
272
|
+
# For SMTP (adjust for your mail provider)
|
|
273
|
+
EMAIL_HOST = "smtp.example.com"
|
|
274
|
+
EMAIL_PORT = 587
|
|
275
|
+
EMAIL_HOST_USER = "your-username"
|
|
276
|
+
EMAIL_HOST_PASSWORD = "your-password"
|
|
277
|
+
EMAIL_USE_TLS = True
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
For local development, use the console backend to see emails in your terminal:
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
EMAIL_BACKEND = "plain.email.backends.console.EmailBackend"
|
|
23
284
|
```
|
plain/email/__init__.py
CHANGED
|
@@ -13,30 +13,19 @@ if TYPE_CHECKING:
|
|
|
13
13
|
from .backends.base import BaseEmailBackend
|
|
14
14
|
|
|
15
15
|
from .message import (
|
|
16
|
-
DEFAULT_ATTACHMENT_MIME_TYPE,
|
|
17
16
|
BadHeaderError,
|
|
18
17
|
EmailMessage,
|
|
19
18
|
EmailMultiAlternatives,
|
|
20
|
-
SafeMIMEMultipart,
|
|
21
|
-
SafeMIMEText,
|
|
22
19
|
TemplateEmail,
|
|
23
|
-
forbid_multi_line_headers,
|
|
24
20
|
make_msgid,
|
|
25
21
|
)
|
|
26
|
-
from .utils import DNS_NAME, CachedDnsName
|
|
27
22
|
|
|
28
23
|
__all__ = [
|
|
29
|
-
"CachedDnsName",
|
|
30
|
-
"DNS_NAME",
|
|
31
24
|
"EmailMessage",
|
|
32
25
|
"EmailMultiAlternatives",
|
|
33
26
|
"TemplateEmail",
|
|
34
|
-
"SafeMIMEText",
|
|
35
|
-
"SafeMIMEMultipart",
|
|
36
|
-
"DEFAULT_ATTACHMENT_MIME_TYPE",
|
|
37
27
|
"make_msgid",
|
|
38
28
|
"BadHeaderError",
|
|
39
|
-
"forbid_multi_line_headers",
|
|
40
29
|
"get_connection",
|
|
41
30
|
"send_mail",
|
|
42
31
|
"send_mass_mail",
|
plain/email/backends/smtp.py
CHANGED
|
@@ -11,8 +11,8 @@ from typing import TYPE_CHECKING, Any
|
|
|
11
11
|
from plain.runtime import settings
|
|
12
12
|
|
|
13
13
|
from ..backends.base import BaseEmailBackend
|
|
14
|
-
from ..message import
|
|
15
|
-
from ..utils import
|
|
14
|
+
from ..message import _sanitize_address
|
|
15
|
+
from ..utils import _DNS_NAME
|
|
16
16
|
|
|
17
17
|
if TYPE_CHECKING:
|
|
18
18
|
from ..message import EmailMessage
|
|
@@ -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: dict[str, Any] = {"local_hostname":
|
|
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:
|
|
@@ -153,9 +153,9 @@ class EmailBackend(BaseEmailBackend):
|
|
|
153
153
|
if not email_message.recipients():
|
|
154
154
|
return False
|
|
155
155
|
encoding = email_message.encoding or settings.DEFAULT_CHARSET
|
|
156
|
-
from_email =
|
|
156
|
+
from_email = _sanitize_address(email_message.from_email, encoding)
|
|
157
157
|
recipients = [
|
|
158
|
-
|
|
158
|
+
_sanitize_address(addr, encoding) for addr in email_message.recipients()
|
|
159
159
|
]
|
|
160
160
|
message = email_message.message()
|
|
161
161
|
assert self.connection is not None, "connection should be open before sending"
|
plain/email/default_settings.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from plain.runtime import Secret
|
|
2
|
+
|
|
1
3
|
# The email backend to use. For possible shortcuts see plain.email.
|
|
2
4
|
# The default is to use the SMTP backend.
|
|
3
5
|
# Third-party backends can be specified by providing a Python path
|
|
@@ -19,7 +21,7 @@ EMAIL_USE_LOCALTIME: bool = False
|
|
|
19
21
|
|
|
20
22
|
# Optional SMTP authentication information for EMAIL_HOST.
|
|
21
23
|
EMAIL_HOST_USER: str = ""
|
|
22
|
-
EMAIL_HOST_PASSWORD: str = ""
|
|
24
|
+
EMAIL_HOST_PASSWORD: Secret[str] = "" # type: ignore[assignment]
|
|
23
25
|
EMAIL_USE_TLS: bool = True
|
|
24
26
|
EMAIL_USE_SSL: bool = False
|
|
25
27
|
EMAIL_SSL_CERTFILE: str | None = None
|
plain/email/message.py
CHANGED
|
@@ -20,12 +20,13 @@ from io import BytesIO, StringIO
|
|
|
20
20
|
from pathlib import Path
|
|
21
21
|
from typing import TYPE_CHECKING, Any
|
|
22
22
|
|
|
23
|
+
from plain.internal import internalcode
|
|
23
24
|
from plain.runtime import settings
|
|
24
25
|
from plain.templates import Template, TemplateFileMissing
|
|
25
26
|
from plain.utils.encoding import force_str, punycode
|
|
26
27
|
from plain.utils.html import strip_tags
|
|
27
28
|
|
|
28
|
-
from .utils import
|
|
29
|
+
from .utils import _DNS_NAME
|
|
29
30
|
|
|
30
31
|
if TYPE_CHECKING:
|
|
31
32
|
from os import PathLike
|
|
@@ -34,16 +35,16 @@ if TYPE_CHECKING:
|
|
|
34
35
|
|
|
35
36
|
# Don't BASE64-encode UTF-8 messages so that we avoid unwanted attention from
|
|
36
37
|
# some spam filters.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
_utf8_charset = Charset.Charset("utf-8")
|
|
39
|
+
_utf8_charset.body_encoding = None # type: ignore[assignment] # Python defaults to BASE64
|
|
40
|
+
_utf8_charset_qp = Charset.Charset("utf-8")
|
|
41
|
+
_utf8_charset_qp.body_encoding = Charset.QP
|
|
41
42
|
|
|
42
43
|
# Default MIME type to use on attachments (if it is not explicitly given
|
|
43
44
|
# and cannot be guessed).
|
|
44
|
-
|
|
45
|
+
_DEFAULT_ATTACHMENT_MIME_TYPE = "application/octet-stream"
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
_RFC5322_EMAIL_LINE_LENGTH_LIMIT = 998
|
|
47
48
|
|
|
48
49
|
|
|
49
50
|
class BadHeaderError(ValueError):
|
|
@@ -51,7 +52,7 @@ class BadHeaderError(ValueError):
|
|
|
51
52
|
|
|
52
53
|
|
|
53
54
|
# Header names that contain structured address data (RFC 5322).
|
|
54
|
-
|
|
55
|
+
_ADDRESS_HEADERS = {
|
|
55
56
|
"from",
|
|
56
57
|
"sender",
|
|
57
58
|
"reply-to",
|
|
@@ -66,7 +67,7 @@ ADDRESS_HEADERS = {
|
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
|
|
69
|
-
def
|
|
70
|
+
def _forbid_multi_line_headers(
|
|
70
71
|
name: str, val: str, encoding: str | None
|
|
71
72
|
) -> tuple[str, str]:
|
|
72
73
|
"""Forbid multi-line headers to prevent header injection."""
|
|
@@ -79,9 +80,9 @@ def forbid_multi_line_headers(
|
|
|
79
80
|
try:
|
|
80
81
|
val.encode("ascii")
|
|
81
82
|
except UnicodeEncodeError:
|
|
82
|
-
if name.lower() in
|
|
83
|
+
if name.lower() in _ADDRESS_HEADERS:
|
|
83
84
|
val = ", ".join(
|
|
84
|
-
|
|
85
|
+
_sanitize_address(addr, encoding) for addr in getaddresses((val,))
|
|
85
86
|
)
|
|
86
87
|
else:
|
|
87
88
|
val = Header(val, encoding).encode()
|
|
@@ -91,7 +92,7 @@ def forbid_multi_line_headers(
|
|
|
91
92
|
return name, val
|
|
92
93
|
|
|
93
94
|
|
|
94
|
-
def
|
|
95
|
+
def _sanitize_address(addr: str | tuple[str, str], encoding: str) -> str:
|
|
95
96
|
"""
|
|
96
97
|
Format a pair of (name, address) or an email address string.
|
|
97
98
|
"""
|
|
@@ -135,6 +136,7 @@ def sanitize_address(addr: str | tuple[str, str], encoding: str) -> str:
|
|
|
135
136
|
return formataddr((nm, parsed_address.addr_spec))
|
|
136
137
|
|
|
137
138
|
|
|
139
|
+
@internalcode
|
|
138
140
|
class MIMEMixin:
|
|
139
141
|
def as_string(self, unixfrom: bool = False, linesep: str = "\n") -> str:
|
|
140
142
|
"""Return the entire formatted message as a string.
|
|
@@ -163,13 +165,15 @@ class MIMEMixin:
|
|
|
163
165
|
return fp.getvalue()
|
|
164
166
|
|
|
165
167
|
|
|
168
|
+
@internalcode
|
|
166
169
|
class SafeMIMEMessage(MIMEMixin, MIMEMessage):
|
|
167
170
|
def __setitem__(self, name: str, val: str) -> None:
|
|
168
171
|
# message/rfc822 attachments must be ASCII
|
|
169
|
-
name, val =
|
|
172
|
+
name, val = _forbid_multi_line_headers(name, val, "ascii")
|
|
170
173
|
MIMEMessage.__setitem__(self, name, val)
|
|
171
174
|
|
|
172
175
|
|
|
176
|
+
@internalcode
|
|
173
177
|
class SafeMIMEText(MIMEMixin, MIMEText):
|
|
174
178
|
def __init__(
|
|
175
179
|
self, _text: str, _subtype: str = "plain", _charset: str | None = None
|
|
@@ -178,7 +182,7 @@ class SafeMIMEText(MIMEMixin, MIMEText):
|
|
|
178
182
|
MIMEText.__init__(self, _text, _subtype=_subtype, _charset=_charset)
|
|
179
183
|
|
|
180
184
|
def __setitem__(self, name: str, val: str) -> None:
|
|
181
|
-
name, val =
|
|
185
|
+
name, val = _forbid_multi_line_headers(name, val, self.encoding)
|
|
182
186
|
MIMEText.__setitem__(self, name, val)
|
|
183
187
|
|
|
184
188
|
def set_payload( # type: ignore[override]
|
|
@@ -186,15 +190,16 @@ class SafeMIMEText(MIMEMixin, MIMEText):
|
|
|
186
190
|
) -> None:
|
|
187
191
|
if charset == "utf-8" and not isinstance(charset, Charset.Charset):
|
|
188
192
|
has_long_lines = any(
|
|
189
|
-
len(line.encode()) >
|
|
193
|
+
len(line.encode()) > _RFC5322_EMAIL_LINE_LENGTH_LIMIT
|
|
190
194
|
for line in payload.splitlines()
|
|
191
195
|
)
|
|
192
196
|
# Quoted-Printable encoding has the side effect of shortening long
|
|
193
197
|
# lines, if any (#22561).
|
|
194
|
-
charset =
|
|
198
|
+
charset = _utf8_charset_qp if has_long_lines else _utf8_charset
|
|
195
199
|
MIMEText.set_payload(self, payload, charset=charset)
|
|
196
200
|
|
|
197
201
|
|
|
202
|
+
@internalcode
|
|
198
203
|
class SafeMIMEMultipart(MIMEMixin, MIMEMultipart):
|
|
199
204
|
def __init__(
|
|
200
205
|
self,
|
|
@@ -208,7 +213,7 @@ class SafeMIMEMultipart(MIMEMixin, MIMEMultipart):
|
|
|
208
213
|
MIMEMultipart.__init__(self, _subtype, boundary, _subparts, **_params)
|
|
209
214
|
|
|
210
215
|
def __setitem__(self, name: str, val: str) -> None:
|
|
211
|
-
name, val =
|
|
216
|
+
name, val = _forbid_multi_line_headers(name, val, self.encoding)
|
|
212
217
|
MIMEMultipart.__setitem__(self, name, val)
|
|
213
218
|
|
|
214
219
|
|
|
@@ -300,8 +305,8 @@ class EmailMessage:
|
|
|
300
305
|
# will get picked up by formatdate().
|
|
301
306
|
msg["Date"] = formatdate(localtime=settings.EMAIL_USE_LOCALTIME)
|
|
302
307
|
if "message-id" not in header_names:
|
|
303
|
-
# Use cached
|
|
304
|
-
msg["Message-ID"] = make_msgid(domain=str(
|
|
308
|
+
# Use cached _DNS_NAME for performance
|
|
309
|
+
msg["Message-ID"] = make_msgid(domain=str(_DNS_NAME))
|
|
305
310
|
for name, value in self.extra_headers.items():
|
|
306
311
|
if name.lower() != "from": # From is already handled
|
|
307
312
|
msg[name] = value
|
|
@@ -337,7 +342,7 @@ class EmailMessage:
|
|
|
337
342
|
|
|
338
343
|
For a text/* mimetype (guessed or specified), when a bytes object is
|
|
339
344
|
specified as content, decode it as UTF-8. If that fails, set the
|
|
340
|
-
mimetype to
|
|
345
|
+
mimetype to _DEFAULT_ATTACHMENT_MIME_TYPE and don't decode the content.
|
|
341
346
|
"""
|
|
342
347
|
if isinstance(filename, MIMEBase):
|
|
343
348
|
if content is not None or mimetype is not None:
|
|
@@ -352,7 +357,7 @@ class EmailMessage:
|
|
|
352
357
|
if filename is not None and mimetype is None:
|
|
353
358
|
mimetype = mimetypes.guess_type(filename)[0]
|
|
354
359
|
if mimetype is None:
|
|
355
|
-
mimetype =
|
|
360
|
+
mimetype = _DEFAULT_ATTACHMENT_MIME_TYPE
|
|
356
361
|
basetype, subtype = mimetype.split("/", 1)
|
|
357
362
|
|
|
358
363
|
if basetype == "text":
|
|
@@ -362,7 +367,7 @@ class EmailMessage:
|
|
|
362
367
|
except UnicodeDecodeError:
|
|
363
368
|
# If mimetype suggests the file is text but it's
|
|
364
369
|
# actually binary, read() raises a UnicodeDecodeError.
|
|
365
|
-
mimetype =
|
|
370
|
+
mimetype = _DEFAULT_ATTACHMENT_MIME_TYPE
|
|
366
371
|
|
|
367
372
|
self.attachments.append((filename, content, mimetype))
|
|
368
373
|
|
|
@@ -372,12 +377,12 @@ class EmailMessage:
|
|
|
372
377
|
"""
|
|
373
378
|
Attach a file from the filesystem.
|
|
374
379
|
|
|
375
|
-
Set the mimetype to
|
|
380
|
+
Set the mimetype to _DEFAULT_ATTACHMENT_MIME_TYPE if it isn't specified
|
|
376
381
|
and cannot be guessed.
|
|
377
382
|
|
|
378
383
|
For a text/* mimetype (guessed or specified), decode the file's content
|
|
379
384
|
as UTF-8. If that fails, set the mimetype to
|
|
380
|
-
|
|
385
|
+
_DEFAULT_ATTACHMENT_MIME_TYPE and don't decode the content.
|
|
381
386
|
"""
|
|
382
387
|
path = Path(path)
|
|
383
388
|
with path.open("rb") as file:
|
plain/email/utils.py
CHANGED
|
@@ -6,11 +6,13 @@ from __future__ import annotations
|
|
|
6
6
|
|
|
7
7
|
import socket
|
|
8
8
|
|
|
9
|
+
from plain.internal import internalcode
|
|
9
10
|
from plain.utils.encoding import punycode
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
# Cache the hostname, but do it lazily: socket.getfqdn() can take a couple of
|
|
13
14
|
# seconds, which slows down the restart of the server.
|
|
15
|
+
@internalcode
|
|
14
16
|
class CachedDnsName:
|
|
15
17
|
def __str__(self) -> str:
|
|
16
18
|
return self.get_fqdn()
|
|
@@ -21,4 +23,4 @@ class CachedDnsName:
|
|
|
21
23
|
return self._fqdn
|
|
22
24
|
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
_DNS_NAME = CachedDnsName()
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: plain.email
|
|
3
|
+
Version: 0.15.0
|
|
4
|
+
Summary: Everything you need to send email in Plain.
|
|
5
|
+
Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.13
|
|
9
|
+
Requires-Dist: plain<1.0.0
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# plain.email
|
|
13
|
+
|
|
14
|
+
**Send emails from your Plain application using SMTP, console output, or file-based backends.**
|
|
15
|
+
|
|
16
|
+
- [Overview](#overview)
|
|
17
|
+
- [Sending a simple email](#sending-a-simple-email)
|
|
18
|
+
- [Sending HTML emails](#sending-html-emails)
|
|
19
|
+
- [Template-based emails](#template-based-emails)
|
|
20
|
+
- [Attachments](#attachments)
|
|
21
|
+
- [Configuration](#configuration)
|
|
22
|
+
- [SMTP settings](#smtp-settings)
|
|
23
|
+
- [Email backends](#email-backends)
|
|
24
|
+
- [SMTP backend](#smtp-backend)
|
|
25
|
+
- [Console backend](#console-backend)
|
|
26
|
+
- [File-based backend](#file-based-backend)
|
|
27
|
+
- [FAQs](#faqs)
|
|
28
|
+
- [Installation](#installation)
|
|
29
|
+
|
|
30
|
+
## Overview
|
|
31
|
+
|
|
32
|
+
You can send emails using the `send_mail` function for simple cases, or use the `EmailMessage` and `EmailMultiAlternatives` classes for more control. For template-based emails, the `TemplateEmail` class renders HTML templates automatically.
|
|
33
|
+
|
|
34
|
+
### Sending a simple email
|
|
35
|
+
|
|
36
|
+
The `send_mail` function is the easiest way to send an email.
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from plain.email import send_mail
|
|
40
|
+
|
|
41
|
+
send_mail(
|
|
42
|
+
subject="Welcome!",
|
|
43
|
+
message="Thanks for signing up.",
|
|
44
|
+
from_email="hello@example.com",
|
|
45
|
+
recipient_list=["user@example.com"],
|
|
46
|
+
)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
To include an HTML version along with the plain text:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
send_mail(
|
|
53
|
+
subject="Welcome!",
|
|
54
|
+
message="Thanks for signing up.",
|
|
55
|
+
from_email="hello@example.com",
|
|
56
|
+
recipient_list=["user@example.com"],
|
|
57
|
+
html_message="<h1>Thanks for signing up!</h1>",
|
|
58
|
+
)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Sending HTML emails
|
|
62
|
+
|
|
63
|
+
For more control over multipart emails, use `EmailMultiAlternatives`.
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from plain.email import EmailMultiAlternatives
|
|
67
|
+
|
|
68
|
+
email = EmailMultiAlternatives(
|
|
69
|
+
subject="Your order confirmation",
|
|
70
|
+
body="Your order #123 has been confirmed.",
|
|
71
|
+
from_email="orders@example.com",
|
|
72
|
+
to=["customer@example.com"],
|
|
73
|
+
)
|
|
74
|
+
email.attach_alternative("<h1>Order #123 Confirmed</h1>", "text/html")
|
|
75
|
+
email.send()
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Template-based emails
|
|
79
|
+
|
|
80
|
+
The `TemplateEmail` class renders emails from template files. You provide a template name, and it looks for corresponding files in your `templates/email/` directory:
|
|
81
|
+
|
|
82
|
+
- `email/{template}.html` - HTML content (required)
|
|
83
|
+
- `email/{template}.txt` - Plain text content (optional, falls back to stripping HTML tags)
|
|
84
|
+
- `email/{template}.subject.txt` - Subject line (optional)
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from plain.email import TemplateEmail
|
|
88
|
+
|
|
89
|
+
email = TemplateEmail(
|
|
90
|
+
template="welcome",
|
|
91
|
+
context={"user_name": "Alice"},
|
|
92
|
+
to=["alice@example.com"],
|
|
93
|
+
)
|
|
94
|
+
email.send()
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
With these template files:
|
|
98
|
+
|
|
99
|
+
```html
|
|
100
|
+
<!-- templates/email/welcome.html -->
|
|
101
|
+
<h1>Welcome, {{ user_name }}!</h1>
|
|
102
|
+
<p>We're glad you're here.</p>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```text
|
|
106
|
+
{# templates/email/welcome.subject.txt #}
|
|
107
|
+
Welcome to our app, {{ user_name }}!
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
You can subclass `TemplateEmail` to customize the template context by overriding `get_template_context()`.
|
|
111
|
+
|
|
112
|
+
### Attachments
|
|
113
|
+
|
|
114
|
+
You can attach files to any email message.
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from plain.email import EmailMessage
|
|
118
|
+
|
|
119
|
+
email = EmailMessage(
|
|
120
|
+
subject="Your report",
|
|
121
|
+
body="Please find your report attached.",
|
|
122
|
+
to=["user@example.com"],
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Attach content directly
|
|
126
|
+
email.attach("report.csv", csv_content, "text/csv")
|
|
127
|
+
|
|
128
|
+
# Or attach a file from disk
|
|
129
|
+
email.attach_file("/path/to/report.pdf")
|
|
130
|
+
|
|
131
|
+
email.send()
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Configuration
|
|
135
|
+
|
|
136
|
+
Configure email settings in your `settings.py`:
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
# settings.py
|
|
140
|
+
|
|
141
|
+
# Required: The backend to use for sending emails
|
|
142
|
+
EMAIL_BACKEND = "plain.email.backends.smtp.EmailBackend"
|
|
143
|
+
|
|
144
|
+
# Required: Default "From" address for outgoing emails
|
|
145
|
+
EMAIL_DEFAULT_FROM = "noreply@example.com"
|
|
146
|
+
|
|
147
|
+
# Optional: Default "Reply-To" addresses
|
|
148
|
+
EMAIL_DEFAULT_REPLY_TO = ["support@example.com"]
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### SMTP settings
|
|
152
|
+
|
|
153
|
+
When using the SMTP backend, configure your mail server:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
# settings.py
|
|
157
|
+
|
|
158
|
+
EMAIL_HOST = "smtp.example.com"
|
|
159
|
+
EMAIL_PORT = 587
|
|
160
|
+
EMAIL_HOST_USER = "your-username"
|
|
161
|
+
EMAIL_HOST_PASSWORD = "your-password"
|
|
162
|
+
EMAIL_USE_TLS = True # Use STARTTLS
|
|
163
|
+
EMAIL_USE_SSL = False # Use implicit SSL (mutually exclusive with TLS)
|
|
164
|
+
|
|
165
|
+
# Optional settings
|
|
166
|
+
EMAIL_TIMEOUT = 10 # Connection timeout in seconds
|
|
167
|
+
EMAIL_SSL_CERTFILE = None # Path to SSL certificate file
|
|
168
|
+
EMAIL_SSL_KEYFILE = None # Path to SSL key file
|
|
169
|
+
EMAIL_USE_LOCALTIME = False # Use local time in Date header (default: UTC)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Email backends
|
|
173
|
+
|
|
174
|
+
The `EMAIL_BACKEND` setting controls how emails are sent. Plain includes three backends.
|
|
175
|
+
|
|
176
|
+
### SMTP backend
|
|
177
|
+
|
|
178
|
+
The default backend sends emails via SMTP.
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
EMAIL_BACKEND = "plain.email.backends.smtp.EmailBackend"
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Console backend
|
|
185
|
+
|
|
186
|
+
Prints emails to the console instead of sending them. Useful during development.
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
EMAIL_BACKEND = "plain.email.backends.console.EmailBackend"
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### File-based backend
|
|
193
|
+
|
|
194
|
+
Writes emails to files in a directory. Useful for testing and debugging.
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
EMAIL_BACKEND = "plain.email.backends.filebased.EmailBackend"
|
|
198
|
+
EMAIL_FILE_PATH = "/path/to/email-output"
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Each email is saved to a timestamped `.log` file in the specified directory.
|
|
202
|
+
|
|
203
|
+
## FAQs
|
|
204
|
+
|
|
205
|
+
#### How do I send to multiple recipients efficiently?
|
|
206
|
+
|
|
207
|
+
Use `send_mass_mail` to send multiple messages over a single connection:
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
from plain.email import send_mass_mail
|
|
211
|
+
|
|
212
|
+
messages = (
|
|
213
|
+
("Subject 1", "Body 1", "from@example.com", ["to1@example.com"]),
|
|
214
|
+
("Subject 2", "Body 2", "from@example.com", ["to2@example.com"]),
|
|
215
|
+
)
|
|
216
|
+
send_mass_mail(messages)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
#### How do I reuse a connection for multiple emails?
|
|
220
|
+
|
|
221
|
+
Use the backend as a context manager:
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
from plain.email import get_connection, EmailMessage
|
|
225
|
+
|
|
226
|
+
with get_connection() as connection:
|
|
227
|
+
for user in users:
|
|
228
|
+
email = EmailMessage(
|
|
229
|
+
subject="Hello",
|
|
230
|
+
body="Hi there!",
|
|
231
|
+
to=[user.email],
|
|
232
|
+
connection=connection,
|
|
233
|
+
)
|
|
234
|
+
email.send()
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
#### How do I add custom headers?
|
|
238
|
+
|
|
239
|
+
Pass a `headers` dict to any email class:
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
email = EmailMessage(
|
|
243
|
+
subject="Hello",
|
|
244
|
+
body="Content",
|
|
245
|
+
to=["user@example.com"],
|
|
246
|
+
headers={"X-Custom-Header": "value", "Reply-To": "reply@example.com"},
|
|
247
|
+
)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
#### How do I create a custom email backend?
|
|
251
|
+
|
|
252
|
+
Subclass [`BaseEmailBackend`](./backends/base.py#BaseEmailBackend) and implement the `send_messages` method:
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
from plain.email.backends.base import BaseEmailBackend
|
|
256
|
+
|
|
257
|
+
class MyBackend(BaseEmailBackend):
|
|
258
|
+
def send_messages(self, email_messages):
|
|
259
|
+
# Your sending logic here
|
|
260
|
+
return len(email_messages)
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Installation
|
|
264
|
+
|
|
265
|
+
Install the `plain.email` package from PyPI:
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
uv add plain.email
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Add `plain.email` to your `INSTALLED_PACKAGES` and configure the required settings:
|
|
272
|
+
|
|
273
|
+
```python
|
|
274
|
+
# settings.py
|
|
275
|
+
INSTALLED_PACKAGES = [
|
|
276
|
+
# ...
|
|
277
|
+
"plain.email",
|
|
278
|
+
]
|
|
279
|
+
|
|
280
|
+
EMAIL_BACKEND = "plain.email.backends.smtp.EmailBackend"
|
|
281
|
+
EMAIL_DEFAULT_FROM = "noreply@example.com"
|
|
282
|
+
|
|
283
|
+
# For SMTP (adjust for your mail provider)
|
|
284
|
+
EMAIL_HOST = "smtp.example.com"
|
|
285
|
+
EMAIL_PORT = 587
|
|
286
|
+
EMAIL_HOST_USER = "your-username"
|
|
287
|
+
EMAIL_HOST_PASSWORD = "your-password"
|
|
288
|
+
EMAIL_USE_TLS = True
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
For local development, use the console backend to see emails in your terminal:
|
|
292
|
+
|
|
293
|
+
```python
|
|
294
|
+
EMAIL_BACKEND = "plain.email.backends.console.EmailBackend"
|
|
295
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
plain/email/CHANGELOG.md,sha256=5o266slsieqY_0Ci5-GeE93uTESuZU5xR63tpxyUb3o,3289
|
|
2
|
+
plain/email/README.md,sha256=JRL1Q3S0KAnZSe2u7xp56kgLPq3zFUDeckYWOT9igr8,7024
|
|
3
|
+
plain/email/__init__.py,sha256=_wQC03wth7dgSqKaFG3IGkdJN5yhXfDH7M7pLNYm7IQ,3391
|
|
4
|
+
plain/email/default_settings.py,sha256=dZlKwsLBAJSZ8cRt117Db3qPf97xbPUaGEHbUmlv2CU,881
|
|
5
|
+
plain/email/message.py,sha256=Wlb0qyxmIow3dWIvFEfDuj-G041x-L-aa7GU0YiSr64,23144
|
|
6
|
+
plain/email/utils.py,sha256=b9DzVVvI6BRwIVWSud_sRRchanBztH1p6qCy5-g9-ns,610
|
|
7
|
+
plain/email/backends/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
plain/email/backends/base.py,sha256=7XDlJv07IMSES10pduu_ayCaznD2kwIyo3iAHWg9uQs,2009
|
|
9
|
+
plain/email/backends/console.py,sha256=M7xW13XMjrozwnkkfTv2RxkV0vKp20EvTpIQpIfCGek,1745
|
|
10
|
+
plain/email/backends/filebased.py,sha256=mEqxazVXHihkndshgkD56qMoXww5CQpZOpsoPh0tQEw,2733
|
|
11
|
+
plain/email/backends/smtp.py,sha256=IqpYXXsf1LTh1uqHR9_gGyYTHwuRdtdX2kUtaRQY01Q,6258
|
|
12
|
+
plain_email-0.15.0.dist-info/METADATA,sha256=Voan-CdvnmoWHGxf7EW_vrl36ZGLXZcoBxT3IFCSpHM,7337
|
|
13
|
+
plain_email-0.15.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
14
|
+
plain_email-0.15.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
|
|
15
|
+
plain_email-0.15.0.dist-info/RECORD,,
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: plain.email
|
|
3
|
-
Version: 0.13.0
|
|
4
|
-
Summary: Everything you need to send email in Plain.
|
|
5
|
-
Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
|
|
6
|
-
License-Expression: BSD-3-Clause
|
|
7
|
-
License-File: LICENSE
|
|
8
|
-
Requires-Python: >=3.13
|
|
9
|
-
Requires-Dist: plain<1.0.0
|
|
10
|
-
Description-Content-Type: text/markdown
|
|
11
|
-
|
|
12
|
-
# plain.email
|
|
13
|
-
|
|
14
|
-
**Everything you need to send email in Plain.**
|
|
15
|
-
|
|
16
|
-
- [Installation](#installation)
|
|
17
|
-
|
|
18
|
-
## Installation
|
|
19
|
-
|
|
20
|
-
Install the `plain.email` package from [PyPI](https://pypi.org/project/plain.email/):
|
|
21
|
-
|
|
22
|
-
```bash
|
|
23
|
-
uv add plain.email
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
Add `plain.email` to your `INSTALLED_PACKAGES`:
|
|
27
|
-
|
|
28
|
-
```python
|
|
29
|
-
# settings.py
|
|
30
|
-
INSTALLED_PACKAGES = [
|
|
31
|
-
# ...
|
|
32
|
-
"plain.email",
|
|
33
|
-
]
|
|
34
|
-
```
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
plain/email/CHANGELOG.md,sha256=XwhhOVmTMUu0nPLCJTX9pPJtpV0p5SAmeJHnluXJ7b4,2379
|
|
2
|
-
plain/email/README.md,sha256=F8R2Mu_U-jOEa2HVTzVpODGD-Ml6jbn9KYnJLl8p6lI,364
|
|
3
|
-
plain/email/__init__.py,sha256=CceyW_Lf4OC6VIkPPvstwNT6Z-RtIkyym_k_s8HMw5E,3691
|
|
4
|
-
plain/email/default_settings.py,sha256=B2V9UOq4MDG5VQM9VKTRiBqQ6ppUyoRFq9j7Qi9bO_M,811
|
|
5
|
-
plain/email/message.py,sha256=hEpEEuw6K9z6zfAbG3nMujaJvaNNkmin51SWkA6iOgQ,23023
|
|
6
|
-
plain/email/utils.py,sha256=1B12SBVCpbLVN5bSaewiEpcDdz-7KM2SpXPC33Dcef8,555
|
|
7
|
-
plain/email/backends/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
plain/email/backends/base.py,sha256=7XDlJv07IMSES10pduu_ayCaznD2kwIyo3iAHWg9uQs,2009
|
|
9
|
-
plain/email/backends/console.py,sha256=M7xW13XMjrozwnkkfTv2RxkV0vKp20EvTpIQpIfCGek,1745
|
|
10
|
-
plain/email/backends/filebased.py,sha256=mEqxazVXHihkndshgkD56qMoXww5CQpZOpsoPh0tQEw,2733
|
|
11
|
-
plain/email/backends/smtp.py,sha256=PmBO3EeYDZLjcBvTCSlWq0THYn4Nv2joGTS1-21Bft0,6253
|
|
12
|
-
plain_email-0.13.0.dist-info/METADATA,sha256=KruBYNqIQ_6ndZ-dqNVYArq4KX61C6GCcYpkOqI9Hbk,677
|
|
13
|
-
plain_email-0.13.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
14
|
-
plain_email-0.13.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
|
|
15
|
-
plain_email-0.13.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|