postiee 0.1.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.
- postiee-0.1.0/LICENSE +21 -0
- postiee-0.1.0/MANIFEST.in +2 -0
- postiee-0.1.0/PKG-INFO +209 -0
- postiee-0.1.0/README.md +181 -0
- postiee-0.1.0/postie/__init__.py +10 -0
- postiee-0.1.0/postie/client.py +196 -0
- postiee-0.1.0/postie/django_backend.py +64 -0
- postiee-0.1.0/postie/exceptions.py +15 -0
- postiee-0.1.0/postiee.egg-info/PKG-INFO +209 -0
- postiee-0.1.0/postiee.egg-info/SOURCES.txt +13 -0
- postiee-0.1.0/postiee.egg-info/dependency_links.txt +1 -0
- postiee-0.1.0/postiee.egg-info/requires.txt +1 -0
- postiee-0.1.0/postiee.egg-info/top_level.txt +1 -0
- postiee-0.1.0/pyproject.toml +40 -0
- postiee-0.1.0/setup.cfg +4 -0
postiee-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nomadic Influence
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
postiee-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: postiee
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A minimal Python client for the Postmark API
|
|
5
|
+
Author-email: Nomadic Influence <support@nomadicinfluence.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/roddy-devs/postie
|
|
8
|
+
Project-URL: Repository, https://github.com/roddy-devs/postie
|
|
9
|
+
Project-URL: Issues, https://github.com/roddy-devs/postie/issues
|
|
10
|
+
Keywords: postmark,email,api,transactional
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Communications :: Email
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Requires-Python: >=3.7
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: requests>=2.25.0
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# Postie
|
|
30
|
+
|
|
31
|
+
A minimal Python client for the Postmark API.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install postie
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from postie import PostieClient
|
|
43
|
+
|
|
44
|
+
# Initialize client
|
|
45
|
+
client = PostieClient(server_token="your-postmark-server-token")
|
|
46
|
+
|
|
47
|
+
# Send a simple email
|
|
48
|
+
response = client.send_email(
|
|
49
|
+
from_email="support@nomadicinfluence.com",
|
|
50
|
+
to="user@example.com",
|
|
51
|
+
subject="Hello from Postie",
|
|
52
|
+
html_body="<h1>Hello!</h1><p>This is a test email.</p>",
|
|
53
|
+
text_body="Hello! This is a test email."
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
print(response)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Features
|
|
60
|
+
|
|
61
|
+
- ✅ Send single emails
|
|
62
|
+
- ✅ Send batch emails (up to 500)
|
|
63
|
+
- ✅ Send template-based emails
|
|
64
|
+
- ✅ HTML and plain text support
|
|
65
|
+
- ✅ CC, BCC, Reply-To
|
|
66
|
+
- ✅ Email tagging and metadata
|
|
67
|
+
- ✅ Open and link tracking
|
|
68
|
+
- ✅ Type hints
|
|
69
|
+
- ✅ Minimal dependencies (only `requests`)
|
|
70
|
+
|
|
71
|
+
## Usage
|
|
72
|
+
|
|
73
|
+
### Send Email
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
client.send_email(
|
|
77
|
+
from_email="support@nomadicinfluence.com",
|
|
78
|
+
to="user@example.com",
|
|
79
|
+
subject="Welcome!",
|
|
80
|
+
html_body="<h1>Welcome to our platform</h1>",
|
|
81
|
+
text_body="Welcome to our platform",
|
|
82
|
+
tag="onboarding",
|
|
83
|
+
track_opens=True
|
|
84
|
+
)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Send Batch Emails
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
emails = [
|
|
91
|
+
{
|
|
92
|
+
"From": "support@nomadicinfluence.com",
|
|
93
|
+
"To": "user1@example.com",
|
|
94
|
+
"Subject": "Hello User 1",
|
|
95
|
+
"HtmlBody": "<p>Hello User 1</p>"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"From": "support@nomadicinfluence.com",
|
|
99
|
+
"To": "user2@example.com",
|
|
100
|
+
"Subject": "Hello User 2",
|
|
101
|
+
"HtmlBody": "<p>Hello User 2</p>"
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
responses = client.send_batch(emails)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Send Template Email
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
client.send_template_email(
|
|
112
|
+
from_email="support@nomadicinfluence.com",
|
|
113
|
+
to="user@example.com",
|
|
114
|
+
template_id=123456,
|
|
115
|
+
template_model={
|
|
116
|
+
"name": "John Doe",
|
|
117
|
+
"action_url": "https://example.com/verify"
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Django Integration
|
|
123
|
+
|
|
124
|
+
### Option 1: Use Django Email Backend (Recommended)
|
|
125
|
+
|
|
126
|
+
Configure Postie as your Django email backend:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
# settings.py
|
|
130
|
+
EMAIL_BACKEND = 'postie.django_backend.PostieEmailBackend'
|
|
131
|
+
POSTMARK_SERVER_TOKEN = os.getenv('POSTMARK_SERVER_TOKEN')
|
|
132
|
+
DEFAULT_FROM_EMAIL = 'support@nomadicinfluence.com'
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Then use Django's standard email functions:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from django.core.mail import send_mail, EmailMultiAlternatives
|
|
139
|
+
|
|
140
|
+
# Simple email
|
|
141
|
+
send_mail(
|
|
142
|
+
subject='Welcome!',
|
|
143
|
+
message='Welcome to our platform',
|
|
144
|
+
from_email='support@nomadicinfluence.com',
|
|
145
|
+
recipient_list=['user@example.com'],
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# HTML email
|
|
149
|
+
email = EmailMultiAlternatives(
|
|
150
|
+
subject='Welcome!',
|
|
151
|
+
body='Welcome to our platform',
|
|
152
|
+
from_email='support@nomadicinfluence.com',
|
|
153
|
+
to=['user@example.com'],
|
|
154
|
+
)
|
|
155
|
+
email.attach_alternative('<h1>Welcome!</h1>', "text/html")
|
|
156
|
+
email.send()
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Option 2: Use PostieClient Directly
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
# utils/email.py
|
|
163
|
+
from postie import PostieClient
|
|
164
|
+
from django.conf import settings
|
|
165
|
+
|
|
166
|
+
postmark = PostieClient(server_token=settings.POSTMARK_SERVER_TOKEN)
|
|
167
|
+
|
|
168
|
+
def send_welcome_email(user_email, user_name):
|
|
169
|
+
postmark.send_email(
|
|
170
|
+
from_email="support@nomadicinfluence.com",
|
|
171
|
+
to=user_email,
|
|
172
|
+
subject=f"Welcome {user_name}!",
|
|
173
|
+
html_body=f"<h1>Welcome {user_name}!</h1>",
|
|
174
|
+
text_body=f"Welcome {user_name}!"
|
|
175
|
+
)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Error Handling
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
from postie import PostieClient, PostieAPIError, PostieError
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
client.send_email(...)
|
|
185
|
+
except PostieAPIError as e:
|
|
186
|
+
print(f"API Error: {e}")
|
|
187
|
+
print(f"Error Code: {e.error_code}")
|
|
188
|
+
print(f"Status Code: {e.status_code}")
|
|
189
|
+
except PostieError as e:
|
|
190
|
+
print(f"Client Error: {e}")
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Requirements
|
|
194
|
+
|
|
195
|
+
- Python 3.7+
|
|
196
|
+
- requests
|
|
197
|
+
|
|
198
|
+
## License
|
|
199
|
+
|
|
200
|
+
MIT
|
|
201
|
+
|
|
202
|
+
## Contributing
|
|
203
|
+
|
|
204
|
+
Contributions welcome! Please open an issue or PR.
|
|
205
|
+
|
|
206
|
+
## Links
|
|
207
|
+
|
|
208
|
+
- [Postmark API Documentation](https://postmarkapp.com/developer)
|
|
209
|
+
- [GitHub Repository](https://github.com/roddy-devs/postie)
|
postiee-0.1.0/README.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Postie
|
|
2
|
+
|
|
3
|
+
A minimal Python client for the Postmark API.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install postie
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from postie import PostieClient
|
|
15
|
+
|
|
16
|
+
# Initialize client
|
|
17
|
+
client = PostieClient(server_token="your-postmark-server-token")
|
|
18
|
+
|
|
19
|
+
# Send a simple email
|
|
20
|
+
response = client.send_email(
|
|
21
|
+
from_email="support@nomadicinfluence.com",
|
|
22
|
+
to="user@example.com",
|
|
23
|
+
subject="Hello from Postie",
|
|
24
|
+
html_body="<h1>Hello!</h1><p>This is a test email.</p>",
|
|
25
|
+
text_body="Hello! This is a test email."
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
print(response)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
|
|
33
|
+
- ✅ Send single emails
|
|
34
|
+
- ✅ Send batch emails (up to 500)
|
|
35
|
+
- ✅ Send template-based emails
|
|
36
|
+
- ✅ HTML and plain text support
|
|
37
|
+
- ✅ CC, BCC, Reply-To
|
|
38
|
+
- ✅ Email tagging and metadata
|
|
39
|
+
- ✅ Open and link tracking
|
|
40
|
+
- ✅ Type hints
|
|
41
|
+
- ✅ Minimal dependencies (only `requests`)
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
### Send Email
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
client.send_email(
|
|
49
|
+
from_email="support@nomadicinfluence.com",
|
|
50
|
+
to="user@example.com",
|
|
51
|
+
subject="Welcome!",
|
|
52
|
+
html_body="<h1>Welcome to our platform</h1>",
|
|
53
|
+
text_body="Welcome to our platform",
|
|
54
|
+
tag="onboarding",
|
|
55
|
+
track_opens=True
|
|
56
|
+
)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Send Batch Emails
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
emails = [
|
|
63
|
+
{
|
|
64
|
+
"From": "support@nomadicinfluence.com",
|
|
65
|
+
"To": "user1@example.com",
|
|
66
|
+
"Subject": "Hello User 1",
|
|
67
|
+
"HtmlBody": "<p>Hello User 1</p>"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"From": "support@nomadicinfluence.com",
|
|
71
|
+
"To": "user2@example.com",
|
|
72
|
+
"Subject": "Hello User 2",
|
|
73
|
+
"HtmlBody": "<p>Hello User 2</p>"
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
responses = client.send_batch(emails)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Send Template Email
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
client.send_template_email(
|
|
84
|
+
from_email="support@nomadicinfluence.com",
|
|
85
|
+
to="user@example.com",
|
|
86
|
+
template_id=123456,
|
|
87
|
+
template_model={
|
|
88
|
+
"name": "John Doe",
|
|
89
|
+
"action_url": "https://example.com/verify"
|
|
90
|
+
}
|
|
91
|
+
)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Django Integration
|
|
95
|
+
|
|
96
|
+
### Option 1: Use Django Email Backend (Recommended)
|
|
97
|
+
|
|
98
|
+
Configure Postie as your Django email backend:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
# settings.py
|
|
102
|
+
EMAIL_BACKEND = 'postie.django_backend.PostieEmailBackend'
|
|
103
|
+
POSTMARK_SERVER_TOKEN = os.getenv('POSTMARK_SERVER_TOKEN')
|
|
104
|
+
DEFAULT_FROM_EMAIL = 'support@nomadicinfluence.com'
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Then use Django's standard email functions:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from django.core.mail import send_mail, EmailMultiAlternatives
|
|
111
|
+
|
|
112
|
+
# Simple email
|
|
113
|
+
send_mail(
|
|
114
|
+
subject='Welcome!',
|
|
115
|
+
message='Welcome to our platform',
|
|
116
|
+
from_email='support@nomadicinfluence.com',
|
|
117
|
+
recipient_list=['user@example.com'],
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# HTML email
|
|
121
|
+
email = EmailMultiAlternatives(
|
|
122
|
+
subject='Welcome!',
|
|
123
|
+
body='Welcome to our platform',
|
|
124
|
+
from_email='support@nomadicinfluence.com',
|
|
125
|
+
to=['user@example.com'],
|
|
126
|
+
)
|
|
127
|
+
email.attach_alternative('<h1>Welcome!</h1>', "text/html")
|
|
128
|
+
email.send()
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Option 2: Use PostieClient Directly
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
# utils/email.py
|
|
135
|
+
from postie import PostieClient
|
|
136
|
+
from django.conf import settings
|
|
137
|
+
|
|
138
|
+
postmark = PostieClient(server_token=settings.POSTMARK_SERVER_TOKEN)
|
|
139
|
+
|
|
140
|
+
def send_welcome_email(user_email, user_name):
|
|
141
|
+
postmark.send_email(
|
|
142
|
+
from_email="support@nomadicinfluence.com",
|
|
143
|
+
to=user_email,
|
|
144
|
+
subject=f"Welcome {user_name}!",
|
|
145
|
+
html_body=f"<h1>Welcome {user_name}!</h1>",
|
|
146
|
+
text_body=f"Welcome {user_name}!"
|
|
147
|
+
)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Error Handling
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from postie import PostieClient, PostieAPIError, PostieError
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
client.send_email(...)
|
|
157
|
+
except PostieAPIError as e:
|
|
158
|
+
print(f"API Error: {e}")
|
|
159
|
+
print(f"Error Code: {e.error_code}")
|
|
160
|
+
print(f"Status Code: {e.status_code}")
|
|
161
|
+
except PostieError as e:
|
|
162
|
+
print(f"Client Error: {e}")
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Requirements
|
|
166
|
+
|
|
167
|
+
- Python 3.7+
|
|
168
|
+
- requests
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
MIT
|
|
173
|
+
|
|
174
|
+
## Contributing
|
|
175
|
+
|
|
176
|
+
Contributions welcome! Please open an issue or PR.
|
|
177
|
+
|
|
178
|
+
## Links
|
|
179
|
+
|
|
180
|
+
- [Postmark API Documentation](https://postmarkapp.com/developer)
|
|
181
|
+
- [GitHub Repository](https://github.com/roddy-devs/postie)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Postie - A minimal Python client for Postmark API"""
|
|
2
|
+
|
|
3
|
+
from .client import PostieClient
|
|
4
|
+
from .exceptions import PostieError, PostieAPIError
|
|
5
|
+
|
|
6
|
+
__version__ = "0.1.0"
|
|
7
|
+
__all__ = ["PostieClient", "PostieError", "PostieAPIError"]
|
|
8
|
+
|
|
9
|
+
# Django backend is imported separately to avoid Django dependency
|
|
10
|
+
# from postie.django_backend import PostieEmailBackend
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Postie client for Postmark API"""
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
from typing import Optional, Dict, Any, List
|
|
5
|
+
from .exceptions import PostieAPIError, PostieError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PostieClient:
|
|
9
|
+
"""Minimal client for Postmark API"""
|
|
10
|
+
|
|
11
|
+
BASE_URL = "https://api.postmarkapp.com"
|
|
12
|
+
|
|
13
|
+
def __init__(self, server_token: str, timeout: int = 30):
|
|
14
|
+
"""
|
|
15
|
+
Initialize Postie client
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
server_token: Postmark server API token
|
|
19
|
+
timeout: Request timeout in seconds (default: 30)
|
|
20
|
+
"""
|
|
21
|
+
if not server_token:
|
|
22
|
+
raise PostieError("server_token is required")
|
|
23
|
+
|
|
24
|
+
self.server_token = server_token
|
|
25
|
+
self.timeout = timeout
|
|
26
|
+
self.session = requests.Session()
|
|
27
|
+
self.session.headers.update({
|
|
28
|
+
"Accept": "application/json",
|
|
29
|
+
"Content-Type": "application/json",
|
|
30
|
+
"X-Postmark-Server-Token": server_token
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
def _request(self, method: str, endpoint: str, data: Optional[Dict] = None) -> Dict[str, Any]:
|
|
34
|
+
"""Make API request"""
|
|
35
|
+
url = f"{self.BASE_URL}{endpoint}"
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
response = self.session.request(
|
|
39
|
+
method=method,
|
|
40
|
+
url=url,
|
|
41
|
+
json=data,
|
|
42
|
+
timeout=self.timeout
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
if response.status_code >= 400:
|
|
46
|
+
error_data = response.json() if response.content else {}
|
|
47
|
+
raise PostieAPIError(
|
|
48
|
+
message=error_data.get("Message", "Unknown error"),
|
|
49
|
+
error_code=error_data.get("ErrorCode"),
|
|
50
|
+
status_code=response.status_code
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
return response.json()
|
|
54
|
+
|
|
55
|
+
except requests.RequestException as e:
|
|
56
|
+
raise PostieError(f"Request failed: {str(e)}")
|
|
57
|
+
|
|
58
|
+
def send_email(
|
|
59
|
+
self,
|
|
60
|
+
from_email: str,
|
|
61
|
+
to: str,
|
|
62
|
+
subject: str,
|
|
63
|
+
html_body: Optional[str] = None,
|
|
64
|
+
text_body: Optional[str] = None,
|
|
65
|
+
cc: Optional[str] = None,
|
|
66
|
+
bcc: Optional[str] = None,
|
|
67
|
+
reply_to: Optional[str] = None,
|
|
68
|
+
tag: Optional[str] = None,
|
|
69
|
+
metadata: Optional[Dict[str, str]] = None,
|
|
70
|
+
track_opens: Optional[bool] = None,
|
|
71
|
+
track_links: Optional[str] = None,
|
|
72
|
+
) -> Dict[str, Any]:
|
|
73
|
+
"""
|
|
74
|
+
Send a single email
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
from_email: Sender email address
|
|
78
|
+
to: Recipient email address
|
|
79
|
+
subject: Email subject
|
|
80
|
+
html_body: HTML email body
|
|
81
|
+
text_body: Plain text email body
|
|
82
|
+
cc: CC recipients (comma-separated)
|
|
83
|
+
bcc: BCC recipients (comma-separated)
|
|
84
|
+
reply_to: Reply-To address
|
|
85
|
+
tag: Tag for categorizing emails
|
|
86
|
+
metadata: Custom metadata dictionary
|
|
87
|
+
track_opens: Enable open tracking
|
|
88
|
+
track_links: Link tracking ("None", "HtmlAndText", "HtmlOnly", "TextOnly")
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
API response dictionary
|
|
92
|
+
"""
|
|
93
|
+
if not html_body and not text_body:
|
|
94
|
+
raise PostieError("Either html_body or text_body is required")
|
|
95
|
+
|
|
96
|
+
payload = {
|
|
97
|
+
"From": from_email,
|
|
98
|
+
"To": to,
|
|
99
|
+
"Subject": subject,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if html_body:
|
|
103
|
+
payload["HtmlBody"] = html_body
|
|
104
|
+
if text_body:
|
|
105
|
+
payload["TextBody"] = text_body
|
|
106
|
+
if cc:
|
|
107
|
+
payload["Cc"] = cc
|
|
108
|
+
if bcc:
|
|
109
|
+
payload["Bcc"] = bcc
|
|
110
|
+
if reply_to:
|
|
111
|
+
payload["ReplyTo"] = reply_to
|
|
112
|
+
if tag:
|
|
113
|
+
payload["Tag"] = tag
|
|
114
|
+
if metadata:
|
|
115
|
+
payload["Metadata"] = metadata
|
|
116
|
+
if track_opens is not None:
|
|
117
|
+
payload["TrackOpens"] = track_opens
|
|
118
|
+
if track_links:
|
|
119
|
+
payload["TrackLinks"] = track_links
|
|
120
|
+
|
|
121
|
+
return self._request("POST", "/email", payload)
|
|
122
|
+
|
|
123
|
+
def send_batch(self, emails: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
124
|
+
"""
|
|
125
|
+
Send multiple emails in a batch
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
emails: List of email dictionaries (same format as send_email args)
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
List of API response dictionaries
|
|
132
|
+
"""
|
|
133
|
+
if not emails:
|
|
134
|
+
raise PostieError("emails list cannot be empty")
|
|
135
|
+
|
|
136
|
+
if len(emails) > 500:
|
|
137
|
+
raise PostieError("Maximum 500 emails per batch")
|
|
138
|
+
|
|
139
|
+
return self._request("POST", "/email/batch", emails)
|
|
140
|
+
|
|
141
|
+
def send_template_email(
|
|
142
|
+
self,
|
|
143
|
+
from_email: str,
|
|
144
|
+
to: str,
|
|
145
|
+
template_id: int,
|
|
146
|
+
template_model: Dict[str, Any],
|
|
147
|
+
cc: Optional[str] = None,
|
|
148
|
+
bcc: Optional[str] = None,
|
|
149
|
+
reply_to: Optional[str] = None,
|
|
150
|
+
tag: Optional[str] = None,
|
|
151
|
+
metadata: Optional[Dict[str, str]] = None,
|
|
152
|
+
track_opens: Optional[bool] = None,
|
|
153
|
+
track_links: Optional[str] = None,
|
|
154
|
+
) -> Dict[str, Any]:
|
|
155
|
+
"""
|
|
156
|
+
Send email using a Postmark template
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
from_email: Sender email address
|
|
160
|
+
to: Recipient email address
|
|
161
|
+
template_id: Postmark template ID
|
|
162
|
+
template_model: Template variables dictionary
|
|
163
|
+
cc: CC recipients (comma-separated)
|
|
164
|
+
bcc: BCC recipients (comma-separated)
|
|
165
|
+
reply_to: Reply-To address
|
|
166
|
+
tag: Tag for categorizing emails
|
|
167
|
+
metadata: Custom metadata dictionary
|
|
168
|
+
track_opens: Enable open tracking
|
|
169
|
+
track_links: Link tracking ("None", "HtmlAndText", "HtmlOnly", "TextOnly")
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
API response dictionary
|
|
173
|
+
"""
|
|
174
|
+
payload = {
|
|
175
|
+
"From": from_email,
|
|
176
|
+
"To": to,
|
|
177
|
+
"TemplateId": template_id,
|
|
178
|
+
"TemplateModel": template_model,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if cc:
|
|
182
|
+
payload["Cc"] = cc
|
|
183
|
+
if bcc:
|
|
184
|
+
payload["Bcc"] = bcc
|
|
185
|
+
if reply_to:
|
|
186
|
+
payload["ReplyTo"] = reply_to
|
|
187
|
+
if tag:
|
|
188
|
+
payload["Tag"] = tag
|
|
189
|
+
if metadata:
|
|
190
|
+
payload["Metadata"] = metadata
|
|
191
|
+
if track_opens is not None:
|
|
192
|
+
payload["TrackOpens"] = track_opens
|
|
193
|
+
if track_links:
|
|
194
|
+
payload["TrackLinks"] = track_links
|
|
195
|
+
|
|
196
|
+
return self._request("POST", "/email/withTemplate", payload)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Django email backend for Postie"""
|
|
2
|
+
|
|
3
|
+
from django.conf import settings
|
|
4
|
+
from django.core.mail.backends.base import BaseEmailBackend
|
|
5
|
+
from django.core.mail import EmailMultiAlternatives
|
|
6
|
+
from .client import PostieClient
|
|
7
|
+
from .exceptions import PostieError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PostieEmailBackend(BaseEmailBackend):
|
|
11
|
+
"""Django email backend using Postie/Postmark"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, fail_silently=False, **kwargs):
|
|
14
|
+
super().__init__(fail_silently=fail_silently, **kwargs)
|
|
15
|
+
|
|
16
|
+
server_token = getattr(settings, 'POSTMARK_SERVER_TOKEN', None)
|
|
17
|
+
if not server_token:
|
|
18
|
+
raise PostieError("POSTMARK_SERVER_TOKEN setting is required")
|
|
19
|
+
|
|
20
|
+
self.client = PostieClient(server_token=server_token)
|
|
21
|
+
|
|
22
|
+
def send_messages(self, email_messages):
|
|
23
|
+
"""Send one or more EmailMessage objects"""
|
|
24
|
+
if not email_messages:
|
|
25
|
+
return 0
|
|
26
|
+
|
|
27
|
+
sent_count = 0
|
|
28
|
+
|
|
29
|
+
for message in email_messages:
|
|
30
|
+
try:
|
|
31
|
+
self._send(message)
|
|
32
|
+
sent_count += 1
|
|
33
|
+
except Exception as e:
|
|
34
|
+
if not self.fail_silently:
|
|
35
|
+
raise
|
|
36
|
+
|
|
37
|
+
return sent_count
|
|
38
|
+
|
|
39
|
+
def _send(self, message):
|
|
40
|
+
"""Send a single email message"""
|
|
41
|
+
# Get HTML body if available
|
|
42
|
+
html_body = None
|
|
43
|
+
if isinstance(message, EmailMultiAlternatives):
|
|
44
|
+
for content, mimetype in message.alternatives:
|
|
45
|
+
if mimetype == 'text/html':
|
|
46
|
+
html_body = content
|
|
47
|
+
break
|
|
48
|
+
|
|
49
|
+
# Prepare recipients
|
|
50
|
+
to = ', '.join(message.to) if message.to else None
|
|
51
|
+
cc = ', '.join(message.cc) if message.cc else None
|
|
52
|
+
bcc = ', '.join(message.bcc) if message.bcc else None
|
|
53
|
+
|
|
54
|
+
# Send via Postie
|
|
55
|
+
self.client.send_email(
|
|
56
|
+
from_email=message.from_email,
|
|
57
|
+
to=to,
|
|
58
|
+
subject=message.subject,
|
|
59
|
+
html_body=html_body,
|
|
60
|
+
text_body=message.body if message.body else None,
|
|
61
|
+
cc=cc,
|
|
62
|
+
bcc=bcc,
|
|
63
|
+
reply_to=message.reply_to[0] if message.reply_to else None,
|
|
64
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Postie exceptions"""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class PostieError(Exception):
|
|
5
|
+
"""Base exception for Postie"""
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PostieAPIError(PostieError):
|
|
10
|
+
"""Raised when Postmark API returns an error"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, message, error_code=None, status_code=None):
|
|
13
|
+
super().__init__(message)
|
|
14
|
+
self.error_code = error_code
|
|
15
|
+
self.status_code = status_code
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: postiee
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A minimal Python client for the Postmark API
|
|
5
|
+
Author-email: Nomadic Influence <support@nomadicinfluence.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/roddy-devs/postie
|
|
8
|
+
Project-URL: Repository, https://github.com/roddy-devs/postie
|
|
9
|
+
Project-URL: Issues, https://github.com/roddy-devs/postie/issues
|
|
10
|
+
Keywords: postmark,email,api,transactional
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Communications :: Email
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Requires-Python: >=3.7
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: requests>=2.25.0
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# Postie
|
|
30
|
+
|
|
31
|
+
A minimal Python client for the Postmark API.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install postie
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from postie import PostieClient
|
|
43
|
+
|
|
44
|
+
# Initialize client
|
|
45
|
+
client = PostieClient(server_token="your-postmark-server-token")
|
|
46
|
+
|
|
47
|
+
# Send a simple email
|
|
48
|
+
response = client.send_email(
|
|
49
|
+
from_email="support@nomadicinfluence.com",
|
|
50
|
+
to="user@example.com",
|
|
51
|
+
subject="Hello from Postie",
|
|
52
|
+
html_body="<h1>Hello!</h1><p>This is a test email.</p>",
|
|
53
|
+
text_body="Hello! This is a test email."
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
print(response)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Features
|
|
60
|
+
|
|
61
|
+
- ✅ Send single emails
|
|
62
|
+
- ✅ Send batch emails (up to 500)
|
|
63
|
+
- ✅ Send template-based emails
|
|
64
|
+
- ✅ HTML and plain text support
|
|
65
|
+
- ✅ CC, BCC, Reply-To
|
|
66
|
+
- ✅ Email tagging and metadata
|
|
67
|
+
- ✅ Open and link tracking
|
|
68
|
+
- ✅ Type hints
|
|
69
|
+
- ✅ Minimal dependencies (only `requests`)
|
|
70
|
+
|
|
71
|
+
## Usage
|
|
72
|
+
|
|
73
|
+
### Send Email
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
client.send_email(
|
|
77
|
+
from_email="support@nomadicinfluence.com",
|
|
78
|
+
to="user@example.com",
|
|
79
|
+
subject="Welcome!",
|
|
80
|
+
html_body="<h1>Welcome to our platform</h1>",
|
|
81
|
+
text_body="Welcome to our platform",
|
|
82
|
+
tag="onboarding",
|
|
83
|
+
track_opens=True
|
|
84
|
+
)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Send Batch Emails
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
emails = [
|
|
91
|
+
{
|
|
92
|
+
"From": "support@nomadicinfluence.com",
|
|
93
|
+
"To": "user1@example.com",
|
|
94
|
+
"Subject": "Hello User 1",
|
|
95
|
+
"HtmlBody": "<p>Hello User 1</p>"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"From": "support@nomadicinfluence.com",
|
|
99
|
+
"To": "user2@example.com",
|
|
100
|
+
"Subject": "Hello User 2",
|
|
101
|
+
"HtmlBody": "<p>Hello User 2</p>"
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
responses = client.send_batch(emails)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Send Template Email
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
client.send_template_email(
|
|
112
|
+
from_email="support@nomadicinfluence.com",
|
|
113
|
+
to="user@example.com",
|
|
114
|
+
template_id=123456,
|
|
115
|
+
template_model={
|
|
116
|
+
"name": "John Doe",
|
|
117
|
+
"action_url": "https://example.com/verify"
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Django Integration
|
|
123
|
+
|
|
124
|
+
### Option 1: Use Django Email Backend (Recommended)
|
|
125
|
+
|
|
126
|
+
Configure Postie as your Django email backend:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
# settings.py
|
|
130
|
+
EMAIL_BACKEND = 'postie.django_backend.PostieEmailBackend'
|
|
131
|
+
POSTMARK_SERVER_TOKEN = os.getenv('POSTMARK_SERVER_TOKEN')
|
|
132
|
+
DEFAULT_FROM_EMAIL = 'support@nomadicinfluence.com'
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Then use Django's standard email functions:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from django.core.mail import send_mail, EmailMultiAlternatives
|
|
139
|
+
|
|
140
|
+
# Simple email
|
|
141
|
+
send_mail(
|
|
142
|
+
subject='Welcome!',
|
|
143
|
+
message='Welcome to our platform',
|
|
144
|
+
from_email='support@nomadicinfluence.com',
|
|
145
|
+
recipient_list=['user@example.com'],
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# HTML email
|
|
149
|
+
email = EmailMultiAlternatives(
|
|
150
|
+
subject='Welcome!',
|
|
151
|
+
body='Welcome to our platform',
|
|
152
|
+
from_email='support@nomadicinfluence.com',
|
|
153
|
+
to=['user@example.com'],
|
|
154
|
+
)
|
|
155
|
+
email.attach_alternative('<h1>Welcome!</h1>', "text/html")
|
|
156
|
+
email.send()
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Option 2: Use PostieClient Directly
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
# utils/email.py
|
|
163
|
+
from postie import PostieClient
|
|
164
|
+
from django.conf import settings
|
|
165
|
+
|
|
166
|
+
postmark = PostieClient(server_token=settings.POSTMARK_SERVER_TOKEN)
|
|
167
|
+
|
|
168
|
+
def send_welcome_email(user_email, user_name):
|
|
169
|
+
postmark.send_email(
|
|
170
|
+
from_email="support@nomadicinfluence.com",
|
|
171
|
+
to=user_email,
|
|
172
|
+
subject=f"Welcome {user_name}!",
|
|
173
|
+
html_body=f"<h1>Welcome {user_name}!</h1>",
|
|
174
|
+
text_body=f"Welcome {user_name}!"
|
|
175
|
+
)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Error Handling
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
from postie import PostieClient, PostieAPIError, PostieError
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
client.send_email(...)
|
|
185
|
+
except PostieAPIError as e:
|
|
186
|
+
print(f"API Error: {e}")
|
|
187
|
+
print(f"Error Code: {e.error_code}")
|
|
188
|
+
print(f"Status Code: {e.status_code}")
|
|
189
|
+
except PostieError as e:
|
|
190
|
+
print(f"Client Error: {e}")
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Requirements
|
|
194
|
+
|
|
195
|
+
- Python 3.7+
|
|
196
|
+
- requests
|
|
197
|
+
|
|
198
|
+
## License
|
|
199
|
+
|
|
200
|
+
MIT
|
|
201
|
+
|
|
202
|
+
## Contributing
|
|
203
|
+
|
|
204
|
+
Contributions welcome! Please open an issue or PR.
|
|
205
|
+
|
|
206
|
+
## Links
|
|
207
|
+
|
|
208
|
+
- [Postmark API Documentation](https://postmarkapp.com/developer)
|
|
209
|
+
- [GitHub Repository](https://github.com/roddy-devs/postie)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
postie/__init__.py
|
|
6
|
+
postie/client.py
|
|
7
|
+
postie/django_backend.py
|
|
8
|
+
postie/exceptions.py
|
|
9
|
+
postiee.egg-info/PKG-INFO
|
|
10
|
+
postiee.egg-info/SOURCES.txt
|
|
11
|
+
postiee.egg-info/dependency_links.txt
|
|
12
|
+
postiee.egg-info/requires.txt
|
|
13
|
+
postiee.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
requests>=2.25.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
postie
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "postiee"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A minimal Python client for the Postmark API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{name = "Nomadic Influence", email = "support@nomadicinfluence.com"}
|
|
12
|
+
]
|
|
13
|
+
license = {text = "MIT"}
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.7",
|
|
20
|
+
"Programming Language :: Python :: 3.8",
|
|
21
|
+
"Programming Language :: Python :: 3.9",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Topic :: Communications :: Email",
|
|
26
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
27
|
+
]
|
|
28
|
+
keywords = ["postmark", "email", "api", "transactional"]
|
|
29
|
+
requires-python = ">=3.7"
|
|
30
|
+
dependencies = [
|
|
31
|
+
"requests>=2.25.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://github.com/roddy-devs/postie"
|
|
36
|
+
Repository = "https://github.com/roddy-devs/postie"
|
|
37
|
+
Issues = "https://github.com/roddy-devs/postie/issues"
|
|
38
|
+
|
|
39
|
+
[tool.setuptools.packages.find]
|
|
40
|
+
include = ["postie*"]
|
postiee-0.1.0/setup.cfg
ADDED