Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
625f373
feat: send email content from code with posthog.emails.send
Silthus Nov 2, 2026
e654aef
Merge branch 'main' into feat/emails-code-form
Silthus Nov 2, 2026
20fad8c
feat: cut emails.send to the template and rendered code forms
Silthus Nov 2, 2026
9a4dd48
test(emails): cover the async code-form send and migrated_from
Silthus Nov 2, 2026
685367c
feat: send cc, bcc and attachments with emails.send
Silthus Nov 3, 2026
f906fb3
fix: refuse every unknown attachment key, a None key included
Silthus Nov 3, 2026
bf687be
test: cover a None attachment key before an unknown one
Silthus Nov 3, 2026
a080862
docs: describe an older server's refusal without its error code
Silthus Nov 3, 2026
b93ea5c
fix(emails): leave reply_to out when the list is empty
Silthus Nov 3, 2026
4a7c6df
Merge remote-tracking branch 'origin/feat/emails-code-form' into feat…
Silthus Nov 3, 2026
f368be7
fix(emails): refuse an attachment string that is not base64
Silthus Nov 4, 2026
8e58646
chore(emails): name the refused-send test after what it guards
Silthus Nov 4, 2026
1b7fd90
chore(emails): drop a comment the name and test already state
Silthus Nov 4, 2026
f5d6f0a
chore(emails): give cc, bcc and attachments their own changeset
Silthus Nov 4, 2026
ac102e0
fix(emails): refuse an attachment with an empty filename
Silthus Nov 4, 2026
860b42f
Merge remote-tracking branch 'origin/main' into feat/emails-code-atta…
Silthus Nov 6, 2026
dcf3fe1
Merge remote-tracking branch 'origin/main' into feat/emails-code-atta…
Silthus Nov 9, 2026
85bb350
feat(emails): upload attachments by hash, then send their references
Silthus Nov 9, 2026
629efcd
fix(emails): send the storage upload exactly as signed
Silthus Nov 9, 2026
2a77a17
fix(emails): keep the environment's TLS settings and the signed URL p…
Silthus Nov 9, 2026
1fe4233
fix(emails): upload shared bytes once and retry a storage's passing f…
Silthus Nov 9, 2026
0794b7b
fix(emails): retry a storage's 429 and name what the comments said
Silthus Nov 9, 2026
391a797
fix(emails): name attachments when PostHog has no upload route
Silthus Nov 9, 2026
c3005cf
fix(emails): size attachments before copying them
Silthus Nov 9, 2026
dd7759e
fix(emails): size Base64 content by its data, not its padding
Silthus Nov 9, 2026
4b7c311
test(emails): pin each body's count in the size estimate
Silthus Nov 9, 2026
dfc3f8f
fix(emails): report PostHog's own answer to an upload-route 404
Silthus Nov 9, 2026
1828040
fix(emails): say which Base64 the SDK takes and where a refused uploa…
Silthus Nov 10, 2026
5ecd581
fix(emails): count the subject in the message estimate like the server
Silthus Nov 10, 2026
7b735ce
test(emails): check uploads against their signed checksum like real s…
Silthus Nov 10, 2026
7ffa68a
fix(emails): size padded Base64 without a copy and never follow a sto…
Silthus Nov 10, 2026
a8dd43b
test(emails): prove the async upload refuses a storage redirect over …
Silthus Nov 10, 2026
4cd73ff
Merge main into feat/emails-code-attachments
Silthus Nov 10, 2026
912f846
test(emails): test attachments against fakes that behave like PostHog…
Silthus Nov 10, 2026
5e8e899
fix(emails): count the text PostHog derives and upload only over https
Silthus Nov 10, 2026
29b1d4a
fix(emails): name the caller's attachment in upload refusals and read…
Silthus Nov 10, 2026
ae0b1ff
test(emails): serve the test's TLS storage at TLS 1.2 or later
Silthus Nov 10, 2026
0098de8
test(emails): assert outcomes, not request traces, in the real-socket…
Silthus Nov 10, 2026
3db6c98
test(emails): run the field refusal and size tests against the PostHo…
Silthus Nov 10, 2026
9c8aa03
test(emails): prove an upload never follows storage to another server
Silthus Nov 10, 2026
f3f5fa3
test(emails): name the lost-reply retry test for what it proves
Silthus Nov 10, 2026
b32cc2d
fix(emails): refuse extra Base64 padding before copying the string
Silthus Nov 10, 2026
7de6993
feat(emails): name a failed attachment upload attachment_upload_failed
Silthus Nov 10, 2026
acaa523
Merge main into feat/emails-code-attachments
Silthus Nov 12, 2026
174a60b
test(emails): follow a storage 307 to another server over real sockets
Silthus Nov 12, 2026
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
7 changes: 5 additions & 2 deletions .github/scripts/check_strict_types.sh
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,11 @@ _ = (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"})
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"], capture_engagement=False, max_retries=0)
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"], attachments=[{"filename": "invite.ics", "content": b"BEGIN:VCALENDAR", "content_type": "text/calendar"}, {"filename": "logo.png", "content": "iVBORw0KGgo=", "content_id": "logo"}])
tracked_send: SendEmailResult = emails.send(to="user@example.com", template_key="welcome", subject="Welcome", html="<p>Hi</p>", tracking={"clicks": True, "engagement": False})
untracked_send: SendEmailResult = emails.send(to="user@example.com", template_key="welcome", subject="Welcome", html="<p>Hi</p>", tracking=False)
not_sensitive_send: SendEmailResult = emails.send(to="user@example.com", template_key="newsletter", subject="News", html="<p>News</p>", sensitive=False)
_ = (template_send, rendered_send, tracked_send, untracked_send)
_ = (template_send, rendered_send, invite_send, tracked_send, untracked_send, not_sensitive_send)


async def async_email_types() -> None:
Expand Down Expand Up @@ -113,7 +114,9 @@ Emails("phs_example").send(to="user@example.com", template_id="0190f6d2-7c1e-7b3
Emails("phs_example").send(to="user@example.com", template_id="0190f6d2-7c1e-7b3a-9f5e-1c2d3e4f5a6b", sensitive=False) # expected-type-error
Emails("phs_example").send(to="user@example.com", template_key="welcome", subject="Hi", html="<p>Hi</p>", tracking={"open": False}) # expected-type-error
Emails("phs_example").send(to="user@example.com", template_key="welcome", subject="Hi", html="<p>Hi</p>", template_data={"name": "Ada"}) # expected-type-error
Emails("phs_example").send(to="user@example.com", template_key="welcome", subject="Hi", html="<p>Hi</p>", cc="team@example.com") # expected-type-error
Emails("phs_example").send(to="user@example.com", template_key="welcome", subject="Hi", html="<p>Hi</p>", attachments=[{"filename": "a.txt", "content": [104, 105]}]) # expected-type-error
Emails("phs_example").send(to="user@example.com", template_key="welcome", subject="Hi", html="<p>Hi</p>", attachments=[{"filename": "a.txt", "content": b"hi", "path": "/tmp/a.txt"}]) # expected-type-error
Emails("phs_example").send(to="user@example.com", template_id="0190f6d2-7c1e-7b3a-9f5e-1c2d3e4f5a6b", attachments=[{"filename": "a.txt", "content": b"hi"}]) # expected-type-error


async def invalid_async_email() -> None:
Expand Down
5 changes: 5 additions & 0 deletions .sampo/changesets/emails-code-attachments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
pypi/posthog: minor
---

A code send with `posthog.emails.send` takes `cc`, `bcc` and `attachments`: at most 10 files and 40 MB per message after Base64 encoding, about 29 MB of files, with the HTML counted twice when there is no `text`, since PostHog derives the text from it. An attachment is a `filename` and its `content`, as bytes or a Base64 string, with an optional `content_type` and `content_id`. The client checks the limits before any request, uploads each file PostHog doesn't hold yet straight to PostHog's file storage over https, never following a redirect, then sends the email with each file's SHA-256. A refused or redirected upload raises `EmailError` with the code `attachment_upload_failed`, and an unreachable storage `network_error` with `field` naming the attachment. Either way no email is sent.
Loading
Loading