Skip to content

Commit 2007fe0

Browse files
committed
Improve README and refactor message class into enums, manager, and schemas
1 parent 9e54184 commit 2007fe0

9 files changed

Lines changed: 487 additions & 540 deletions

File tree

README.md

Lines changed: 58 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,59 @@ The official Python SDK for [Postmark](https://postmarkapp.com) - The email deli
1111

1212
(pip install coming soon)
1313

14-
**Requirements:** Python 3.9 or higher
14+
### Prerequisites
15+
- Python 3.9+
16+
- Poetry (for dependency management)
17+
18+
# Get Started
19+
20+
## Clone the repository
21+
```bash
22+
$ git clone [https://github.com/ActiveCampaign/postmark-python.git](https://github.com/ActiveCampaign/postmark-python.git)
23+
$ cd postmark-python
24+
```
25+
## Install dependencies
26+
`$ poetry install`
27+
28+
## Populate .env file
29+
[Get your Postmark account and server tokens here](https://account.postmarkapp.com/api_tokens)
30+
31+
Create an `.env` file, and populate it with:
32+
```bash
33+
POSTMARK_SERVER_TOKEN={PostmarkServerToken}
34+
POSTMARK_ACCOUNT_TOKEN={PostmarkAccountToken}
35+
POSTMARK_SENDER_EMAIL=test@example.com
36+
POSTMARK_TEST_MODE=false
37+
POSTMARK_TRACK_OPENS=true
38+
POSTMARK_LOG_LEVEL=1
39+
```
40+
41+
## Running examples:
42+
- Get messages example: `$ poetry run python examples/get_messages.py`
43+
- Send messages example: `$ poetry run python examples/send_messages.py`
44+
45+
46+
## Developing
47+
48+
### Running Tests
49+
50+
```bash
51+
# Run all tests
52+
poetry run pytest
53+
54+
# Run with coverage report
55+
poetry run pytest --cov=postmark --cov-report=term-missing
56+
57+
# Run specific test file
58+
poetry run pytest postmark/tests/test_messages.py
59+
60+
# Run with verbose output
61+
poetry run pytest -v
62+
63+
# See HTML coverage report
64+
poetry run pytest --cov=postmark --cov-report=html
65+
open htmlcov/index.html
66+
```
1567

1668
## Quick Start
1769

@@ -31,7 +83,7 @@ async def get_messages():
3183

3284
print(f"Found {total} messages")
3385
for msg in messages[:5]:
34-
print(f" - {msg.subject} (from: {msg.from_})")
86+
print(f" - {msg.subject} (from: {msg.sender})")
3587

3688
asyncio.run(get_messages())
3789
```
@@ -40,11 +92,7 @@ asyncio.run(get_messages())
4092

4193
The SDK requires a Postmark Server Token for API authentication. You can find your token in the [Postmark dashboard](https://account.postmarkapp.com/servers).
4294

43-
```python
44-
server_token = "your-postmark-server-token"
45-
```
46-
47-
For security, we recommend storing your token in environment variables:
95+
We recommend using environment variables:
4896

4997
```python
5098
import os
@@ -79,7 +127,7 @@ async def send_email():
79127

80128
# Method 2: Using the Email model (Recommended for type safety)
81129
email = Email(
82-
from_="sender@example.com",
130+
sender="sender@example.com",
83131
to="receiver@example.com",
84132
subject="Hello via Model",
85133
text_body="This is a test using the model."
@@ -114,7 +162,7 @@ async def search_messages():
114162
asyncio.run(search_messages())
115163
```
116164

117-
### Get Message Details
165+
### Get a single Message Details by Message ID
118166

119167
```python
120168
async def get_message_details():
@@ -125,7 +173,7 @@ async def get_message_details():
125173
message = await server.messages.Outbound.get(message_id=message_id)
126174

127175
print(f"Subject: {message.subject}")
128-
print(f"From: {message.from_}")
176+
print(f"From: {message.sender}")
129177
print(f"HTML Body: {message.html_body}")
130178

131179
asyncio.run(get_message_details())
@@ -191,7 +239,6 @@ async def safe_message_search():
191239
asyncio.run(safe_message_search())
192240

193241
```
194-
195242
### Exception Types
196243

197244
- **`InvalidAPIKeyException`**: Invalid or missing API key (401)
@@ -276,50 +323,6 @@ Async generator that lazily retrieves messages matching filters, handling pagina
276323

277324
## Development
278325

279-
### Prerequisites
280-
- Python 3.9+
281-
- Poetry (for dependency management)
282-
283-
### Setup
284-
285-
```bash
286-
# Clone the repository
287-
git clone [https://github.com/ActiveCampaign/postmark-python.git](https://github.com/ActiveCampaign/postmark-python.git)
288-
cd postmark-python
289-
290-
# Install dependencies
291-
poetry install
292-
293-
# Run tests
294-
poetry run pytest
295-
296-
# Run with coverage
297-
poetry run pytest --cov=postmark --cov-report=term-missing
298-
299-
# Run examples
300-
poetry run python examples/get_messages.py
301-
```
302-
303-
### Running Tests
304-
305-
```bash
306-
# Run all tests
307-
poetry run pytest
308-
309-
# Run with coverage report
310-
poetry run pytest --cov=postmark --cov-report=term-missing
311-
312-
# Run specific test file
313-
poetry run pytest postmark/tests/test_messages.py
314-
315-
# Run with verbose output
316-
poetry run pytest -v
317-
318-
# See HTML coverage report
319-
poetry run pytest --cov=postmark --cov-report=html
320-
open htmlcov/index.html
321-
```
322-
323326
### Code Formatting
324327

325328
This project uses [Black](https://github.com/psf/black) for code formatting:

example.env

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
POSTMARK_SERVER_TOKEN=test-token-0000-0000-000000
2-
POSTMARK_ACCOUNT_TOKEN=test-token-0000-0000-000000
1+
POSTMARK_SERVER_TOKEN=111-YOUR-SERVER-TOKEN-0000-000000
2+
POSTMARK_ACCOUNT_TOKEN=222-YOUR-ACCOUNT-TOKEN-0000-000000
33
POSTMARK_SENDER_EMAIL=test@example.com
44
POSTMARK_TEST_MODE=false
55
POSTMARK_TRACK_OPENS=true

examples/get_messages.py

Lines changed: 91 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ async def list_example():
2424
if messages:
2525
for msg in messages[:3]:
2626
print(f" - {msg.subject}")
27+
print(f" ID: {msg.message_id}")
2728
print(f" FROM: {msg.sender}")
2829
print(f" TO: {', '.join([e.email for e in msg.to])}")
2930
print(f" DATE: {msg.received_at}")
@@ -53,6 +54,7 @@ async def stream_example():
5354
# Print details for the first 3...
5455
if count <= 3:
5556
print(f" #{count} - {msg.subject}")
57+
print(f" ID: {msg.message_id}")
5658
print(f" FROM: {msg.sender}")
5759
print(f" TO: {', '.join([e.email for e in msg.to])}")
5860

@@ -62,9 +64,94 @@ async def stream_example():
6264
print(f"Error in stream_example: {e}")
6365

6466

67+
async def get_details_example(message_id: str):
68+
"""
69+
Example using .get() to retrieve full details of a specific message.
70+
Populates and prints all available data from the returned OutboundMessageDetails object.
71+
"""
72+
server_token: str = os.getenv("POSTMARK_SERVER_TOKEN")
73+
server = postmark.ServerClient(server_token=server_token)
74+
75+
print(f"Fetching details for message ID: {message_id}...")
76+
77+
try:
78+
# Get message details
79+
message = await server.messages.Outbound.get(message_id)
80+
81+
print("\n--- Message Details ---")
82+
print(f"Message ID: {message.message_id}")
83+
print(f"Status: {message.status}")
84+
print(f"Message Stream: {message.message_stream}")
85+
print(f"Received At: {message.received_at}")
86+
print(f"From: {message.sender}")
87+
88+
# Helper to format list of EmailAddress objects
89+
format_emails = lambda emails: ", ".join(
90+
[f"{e.name} <{e.email}>" if e.name else e.email for e in emails]
91+
)
92+
93+
print(f"To: {format_emails(message.to)}")
94+
if message.cc:
95+
print(f"Cc: {format_emails(message.cc)}")
96+
if message.bcc:
97+
print(f"Bcc: {format_emails(message.bcc)}")
98+
99+
print(f"Subject: {message.subject}")
100+
if message.tag:
101+
print(f"Tag: {message.tag}")
102+
103+
print("\n--- Content Bodies ---")
104+
print(
105+
f"Text Body: {len(message.text_body) if message.text_body else 'None'}"
106+
)
107+
print(
108+
f"HTML Body: {len(message.html_body) if message.html_body else 'None'}"
109+
)
110+
if message.body:
111+
print(f"Raw Body: {len(message.body)} chars")
112+
113+
print("\n--- Configuration ---")
114+
print(f"Track Opens: {message.track_opens}")
115+
print(f"Track Links: {message.track_links}")
116+
print(f"Sandboxed: {message.sandboxed}")
117+
118+
if message.metadata:
119+
print(f"\n--- Metadata ---")
120+
for k, v in message.metadata.items():
121+
print(f" {k}: {v}")
122+
123+
if message.attachments:
124+
print(f"\n--- Attachments ({len(message.attachments)}) ---")
125+
for attachment in message.attachments:
126+
# Check if it's an object (OutboundMessageDetails usually returns Attachment objects)
127+
if hasattr(attachment, "name"):
128+
print(
129+
f" - {attachment.name} ({attachment.content_type}, {attachment.content_length} bytes)"
130+
)
131+
else:
132+
print(f" - {attachment}")
133+
134+
if message.message_events:
135+
print(f"\n--- Message Events ({len(message.message_events)}) ---")
136+
for event in message.message_events:
137+
# Simplistic dump of details if present
138+
details_str = (
139+
f" - {event.details.model_dump(exclude_none=True)}"
140+
if event.details
141+
else ""
142+
)
143+
print(f" - [{event.received_at}] {event.type}{details_str}")
144+
145+
except Exception as e:
146+
print(f"Error getting message details: {e}")
147+
148+
65149
if __name__ == "__main__":
66-
print("--- RUNNING LIST EXAMPLE ---")
67-
asyncio.run(list_example())
150+
print("--- GET SINGLE MESSAGE BY ID EXAMPLE ---")
151+
asyncio.run(get_details_example(message_id="a32a9dc7-a81c-4c4d-a8a3-32124297d0dd"))
152+
153+
# print("--- RUNNING LIST EXAMPLE ---")
154+
# asyncio.run(list_example())
68155

69-
print("\n--- RUNNING STREAM EXAMPLE ---")
70-
asyncio.run(stream_example())
156+
# print("\n--- RUNNING STREAM EXAMPLE ---")
157+
# asyncio.run(stream_example())

0 commit comments

Comments
 (0)