Coverage for postrfp/mail/postmark.py: 81%
32 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-22 21:34 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-22 21:34 +0000
1"""
2Postmark email delivery implementation.
4This module implements the MailerProtocol through module-level functions.
5The module itself can be used wherever MailerProtocol is expected.
6"""
8import logging
9from typing import Union
11import requests
13from postrfp import conf
14from postrfp.shared.init.sysconfig import configure_postrfp
15from postrfp.mail.schemas import TemplateEmailModel, SimpleEmailModel
18log = logging.getLogger(__name__)
20REQUEST_TIMEOUT = 5 # seconds
23def api_headers(key: str) -> dict[str, str]:
24 return {
25 "X-Postmark-Server-Token": key,
26 "Accept": "application/json",
27 "Content-Type": "application/json",
28 }
31def _handle_response(to_addr: str, response: requests.Response) -> str:
32 if not response.ok:
33 log.warning("Mail delivery to %s failed: %s", to_addr, response.text)
34 response.raise_for_status()
35 return response.json()["MessageID"]
38def send_simple_email(model: SimpleEmailModel, headers: dict[str, str]) -> str:
39 """
40 Send a simple email using Postmark API.
41 NB: This function modifies the 'To' field to use an override address, this is for safety in
42 development.
43 """
44 # Convert Pydantic model to dict
45 model_dict = model.model_dump()
46 model_dict["To"] = conf.CONF.email_to_override
47 resp = requests.post(
48 "https://api.postmarkapp.com/email",
49 json=model_dict,
50 headers=headers,
51 timeout=REQUEST_TIMEOUT,
52 )
53 return _handle_response(model_dict["To"], resp)
56def send_template_email(model: TemplateEmailModel, headers: dict[str, str]) -> str:
57 """
58 Send a template-based email using Postmark API.
60 NB: This function modifies the 'To' field to use an override address, this is for safety in
61 development.
62 """
63 model_dict = model.model_dump()
64 model_dict["To"] = conf.CONF.email_to_override
65 resp = requests.post(
66 "https://api.postmarkapp.com/email/withTemplate",
67 json=model_dict,
68 headers=headers,
69 timeout=REQUEST_TIMEOUT,
70 )
71 return _handle_response(model_dict["To"], resp)
74def send_email(model: Union[TemplateEmailModel, SimpleEmailModel]) -> str:
75 """
76 Unified email sending interface for Postmark API.
78 Determines whether to use template-based or simple email sending
79 based on the type of the Pydantic model.
81 Args:
82 model: Email data Pydantic model - either TemplateEmailModel for event
83 notifications or SimpleEmailModel for basic messages.
85 Returns:
86 str: Postmark MessageID
88 Raises:
89 HTTPError: If Postmark API request fails
90 KeyError: If required email fields are missing
91 """
92 headers = api_headers(conf.CONF.postmark_api_key)
94 if isinstance(model, TemplateEmailModel):
95 # Use template-based email for event notifications
96 return send_template_email(model, headers)
97 elif isinstance(model, SimpleEmailModel):
98 # Use simple email for basic messages
99 return send_simple_email(model, headers)
100 else:
101 raise TypeError(f"Unsupported email model type: {type(model)}")
104if __name__ == "__main__": # pragma: no cover
105 from postrfp.mail.schemas import SimpleEmailModel
107 configure_postrfp()
108 headers = api_headers("POSTMARK_API_TEST")
109 headers = api_headers(conf.CONF.postmark_api_key)
111 model = SimpleEmailModel(
112 To="patrick.dobbs@supplierselect.com",
113 From="notifications@supplierselect.com",
114 Subject="Test blackhole",
115 TextBody="Just a test",
116 Tag="Testing",
117 )
118 resp = send_simple_email(model, headers)
119 print(resp)