Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/scripts/check_strict_types.sh
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ span.end()

_ = (flag_value, all_flags, enabled, payload, evaluations, active)

emails = Emails("phs_example")
template_send: SendEmailResult = emails.send(to="user@example.com", template_id="0190f6d2-7c1e-7b3a-9f5e-1c2d3e4f5a6b", template_data={"name": "Ada"})
emails = Emails("phs_example", migrated_from="resend")
template_send: SendEmailResult = emails.send(to="user@example.com", template_id="0190f6d2-7c1e-7b3a-9f5e-1c2d3e4f5a6b", template_data={"name": "Ada"}, capture_engagement=False, max_retries=0)
rendered_send: SendEmailResult = emails.send(to="user@example.com", template_key="password-reset", subject="Reset", html="<p>Reset</p>", from_="Acme <security@example.com>", reply_to=["help@example.com"])
local_template_send: SendEmailResult = emails.send(to="user@example.com", template_key="welcome", subject="Hi {{ variables.name }}", text="Hi", template_data={"name": "Ada"})
invite_send: SendEmailResult = emails.send(to="user@example.com", template_key="invite", subject="Invite", html="<p>Invite</p>", cc="team@example.com", bcc=["audit@example.com"], headers={"X-Entity-Ref-ID": "order-123"}, tags=[{"name": "category", "value": "invite"}], attachments=[{"filename": "invite.ics", "content": b"BEGIN:VCALENDAR", "content_type": "text/calendar"}, {"filename": "logo.png", "content": "iVBORw0KGgo=", "content_id": "logo"}])
Expand Down
2 changes: 1 addition & 1 deletion .sampo/changesets/emails-code-form.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
pypi/posthog: minor
---

Send transactional email from content in your code with `posthog.emails.send(template_key=..., subject=..., html=...)`. Without `template_data`, the content is sent as is. With `template_data`, the content is a template: PostHog fills in its variables, and the client uploads it by key whenever PostHog doesn't hold its content yet, then sends again with the same idempotency key. A send that mixes `template_id` and `template_key` fails type checks and raises `TypeError`.
Send transactional email from content in your code with `posthog.emails.send(template_key=..., subject=..., html=...)`. Without `template_data`, the content is sent as is. With `template_data`, the content is a template: PostHog fills in its variables, and the client uploads it by key whenever PostHog doesn't hold its content yet, then sends again with the same idempotency key. A send that mixes `template_id` and `template_key` fails type checks and raises `EmailValidationError`.

A code send also takes `cc`, `bcc`, `headers`, `tags` and `attachments`. An attachment's `content` can be `bytes`, `bytearray` or `memoryview`, which the client encodes as Base64, or a Base64 string.
5 changes: 5 additions & 0 deletions .sampo/changesets/emails-contract-alignment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
pypi/posthog: minor
---

`emails.send` follows the shared email SDK contract. An argument that fails a check raises `EmailValidationError`, an `EmailError` and a `ValueError` with the check's `code` and the argument in `field`. A disabled client raises `client_disabled`, and a client without a usable app host raises `missing_app_host`. A send takes `max_retries`. Every send says whether to capture opens and clicks: `capture_engagement` defaults to `True` on both forms, and `False` turns capture off for that send. Set `migrated_from`, such as `"resend"`, on the client to send it with every email request. A `Retry-After` over 30 seconds waits 30 seconds instead of failing the send. `SendEmailResult` has no `replayed` field.
10 changes: 8 additions & 2 deletions posthog/async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ def __init__(
app_host: Optional[str] = None,
email_request_timeout_seconds: float = 15,
email_request_max_retries: int = 2,
migrated_from: Optional[str] = None,
) -> None:
if flush_at <= 0:
raise ValueError("flush_at must be greater than zero")
Expand Down Expand Up @@ -177,10 +178,13 @@ def __init__(
else resolved_secret_key
) or None
self.personal_api_key = self.secret_key
_validate_options(email_request_timeout_seconds, email_request_max_retries)
_validate_options(
email_request_timeout_seconds, email_request_max_retries, migrated_from
)
self.app_host = app_host
self.email_request_timeout_seconds = email_request_timeout_seconds
self.email_request_max_retries = email_request_max_retries
self.migrated_from = migrated_from
self.feature_flags_request_timeout_seconds = (
feature_flags_request_timeout_seconds
)
Expand Down Expand Up @@ -266,7 +270,8 @@ def emails(self) -> AsyncEmails:
scope, and ``posthog[async]``. Sends go to ``app_host``, or to the PostHog
Cloud app host of ``host``. ``email_request_timeout_seconds`` and
``email_request_max_retries`` configure each send. A disabled or shut-down
client sends nothing. See ``posthog.emails`` for setup.
client sends nothing and raises ``client_disabled`` or ``client_closed``.
See ``posthog.emails`` for setup.

Examples:
```python
Expand All @@ -287,6 +292,7 @@ def emails(self) -> AsyncEmails:
app_host=self.app_host,
timeout=self.email_request_timeout_seconds,
max_retries=self.email_request_max_retries,
migrated_from=self.migrated_from,
)
return self._emails

Expand Down
14 changes: 11 additions & 3 deletions posthog/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,7 @@ def __init__(
app_host: Optional[str] = None,
email_request_timeout_seconds: float = 15,
email_request_max_retries: int = 2,
migrated_from: Optional[str] = None,
):
"""
Initialize a new PostHog client instance.
Expand Down Expand Up @@ -785,6 +786,8 @@ def __init__(
email_request_max_retries: Number of retries for an email send after a
network error, a rate limit, or an unavailable service. Defaults to 2.
Set to 0 when a job runner already retries the send.
migrated_from: The email provider you moved from, such as
``"resend"``. ``emails`` sends it with every request.
super_properties: Properties merged into every captured event.
enable_exception_autocapture: Automatically capture uncaught
exceptions.
Expand Down Expand Up @@ -1049,10 +1052,13 @@ def __init__(
else resolved_secret_key
) or None
self.personal_api_key = self.secret_key
_validate_options(email_request_timeout_seconds, email_request_max_retries)
_validate_options(
email_request_timeout_seconds, email_request_max_retries, migrated_from
)
self.app_host = app_host
self.email_request_timeout_seconds = email_request_timeout_seconds
self.email_request_max_retries = email_request_max_retries
self.migrated_from = migrated_from
if debug:
# Ensures that debug level messages are logged when debug mode is on.
# Otherwise, defaults to WARNING level. See https://docs.python.org/3/howto/logging.html#what-happens-if-no-configuration-is-provided
Expand Down Expand Up @@ -1157,8 +1163,9 @@ def emails(self) -> Emails:
Needs ``secret_key``, a project secret API key with the ``hog_flow:write``
scope. Sends go to ``app_host``, or to the PostHog Cloud app host of
``host``. ``email_request_timeout_seconds`` and ``email_request_max_retries``
configure each send. A disabled or shut-down client sends nothing. See
``posthog.emails`` for setup.
configure each send. A disabled or shut-down client sends nothing and
raises ``client_disabled`` or ``client_closed``. See ``posthog.emails``
for setup.

Examples:
```python
Expand All @@ -1180,6 +1187,7 @@ def emails(self) -> Emails:
app_host=self.app_host,
timeout=self.email_request_timeout_seconds,
max_retries=self.email_request_max_retries,
migrated_from=self.migrated_from,
)
return self._emails

Expand Down
Loading