77from postmark .exceptions import InvalidEmailPayloadException
88from postmark .utils .types import HTTPClient
99
10- from .schemas import Email , Outbound , OutboundMessageDetails , SendResponse
10+ from .schemas import (
11+ BulkEmail ,
12+ BulkSendResponse ,
13+ BulkSendStatus ,
14+ Email ,
15+ Outbound ,
16+ OutboundMessageDetails ,
17+ SendResponse ,
18+ )
1119
1220logger = logging .getLogger (__name__ )
1321
1422
1523def _parse_email (message : Union [Email , Dict [str , Any ]]) -> Email :
1624 """
1725 Coerce a dict to an Email model using snake_case field names,
18- raising InvalidEmailPayloadException on validation failure.
19- Passes through an Email instance unchanged.
26+ or raise InvalidEmailPayloadException
2027 """
2128 if isinstance (message , Email ):
2229 return message
@@ -26,10 +33,27 @@ def _parse_email(message: Union[Email, Dict[str, Any]]) -> Email:
2633 raise InvalidEmailPayloadException (e .errors ()) from e
2734
2835
36+ def _parse_bulk_email (message : Union [BulkEmail , Dict [str , Any ]]) -> BulkEmail :
37+ """
38+ Coerce a dict to a BulkEmail model, raising InvalidEmailPayloadException
39+ on validation failure. Passes through a BulkEmail instance unchanged.
40+ """
41+ if isinstance (message , BulkEmail ):
42+ return message
43+ try :
44+ return BulkEmail .model_validate (message )
45+ except ValidationError as e :
46+ raise InvalidEmailPayloadException (e .errors ()) from e
47+
48+
2949class OutboundManager :
3050 def __init__ (self , client : HTTPClient ):
3151 self .client = client
3252
53+ # -------------------------------------------------------------------------
54+ # Single send
55+ # -------------------------------------------------------------------------
56+
3357 async def send (self , message : Union [Email , Dict [str , Any ]]) -> SendResponse :
3458 """Send a single email."""
3559 email_payload = _parse_email (message )
@@ -41,14 +65,21 @@ async def send(self, message: Union[Email, Dict[str, Any]]) -> SendResponse:
4165 )
4266 return SendResponse (** response .json ())
4367
68+ # -------------------------------------------------------------------------
69+ # Batch send — different messages, one request (max 500)
70+ # -------------------------------------------------------------------------
71+
4472 async def send_batch (
4573 self , messages : List [Union [Email , Dict [str , Any ]]]
4674 ) -> List [SendResponse ]:
47- """Send different emails in a single batch (max 500)."""
75+ """
76+ Send up to 500 different emails in a single request.
77+ Use this when each recipient needs a **completely different** message.
78+ For sending the **same message** to many recipients, use send_bulk().
79+ """
4880 if len (messages ) > 500 :
4981 raise ValueError ("Batch size cannot exceed 500 messages" )
5082
51- # Validate all messages up front so we fail before making any HTTP call
5283 payload = []
5384 for i , msg in enumerate (messages ):
5485 try :
@@ -68,6 +99,41 @@ async def send_batch(
6899 response = await self .client .post ("/email/batch" , json = payload )
69100 return [SendResponse (** item ) for item in response .json ()]
70101
102+ # -------------------------------------------------------------------------
103+ # Bulk send — same message, many recipients, one request
104+ # -------------------------------------------------------------------------
105+
106+ async def send_bulk (
107+ self , message : Union [BulkEmail , Dict [str , Any ]]
108+ ) -> BulkSendResponse :
109+ """
110+ Send the **same message** to multiple recipients in a single request.
111+ """
112+ bulk_payload = _parse_bulk_email (message )
113+
114+ if not bulk_payload .messages :
115+ raise ValueError (
116+ "Bulk email must include at least one recipient in messages"
117+ )
118+
119+ logger .debug (f"Sending bulk email to { len (bulk_payload .messages )} recipients" )
120+ response = await self .client .post (
121+ "/email/bulk" ,
122+ json = bulk_payload .model_dump (by_alias = True , exclude_none = True ),
123+ )
124+ return BulkSendResponse (** response .json ())
125+
126+ async def get_bulk_status (self , bulk_id : str ) -> BulkSendStatus :
127+ """
128+ Poll the status of a bulk send request.
129+ """
130+ response = await self .client .get (f"/email/bulk/{ bulk_id } " )
131+ return BulkSendStatus (** response .json ())
132+
133+ # -------------------------------------------------------------------------
134+ # List / stream / get
135+ # -------------------------------------------------------------------------
136+
71137 async def list (
72138 self ,
73139 count : int = 100 ,
0 commit comments