Repository navigation
Conversation
A code send takes cc and bcc as one address or a list, and attachments as files with bytes or Base64 content. The client encodes bytes once, before any retry. A file it can't send raises EmailError with the code invalid_attachment and a field such as attachments__0__content, and the error never repeats the file's content. A template_id send refuses all three fields before any request. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Note 🤖 Claude Opus 5.5 writing on behalf of Michael Decisions for the attachments slice:
|
posthog-python Compliance ReportDate: 2026-11-04T08:44:04.078632+00:00 ✅ All Tests Passed!121/121 tests passed Capture_V1 Tests✅ 95/95 tests passed View Details
Capture_Ai Tests✅ 5/5 tests passed View Details
Feature_Flags Tests✅ 17/17 tests passed View Details
Feature_Flags_Local_Evaluation Tests✅ 4/4 tests passed View Details
|
next(..., None) used None as its no-match value, so a None key hid the unknown keys after it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Not approved — this change needs a human reviewer.
Re-adding the stamphog label gives the same result unless the changed files change.
StampHog can't approve this pull request automatically. Two gates refused it. The deny-list gate flagged a public API change: posthog/emails.py adds the EmailAttachment type and new cc, bcc and attachments fields on CodeEmailArgs, and references/public_api_snapshot.txt records them. The tier gate classified the change as T2-never (336 lines across 5 files, cross-cutting, a feature), a tier that is never auto-approved.
The size gate passed, so splitting the pull request won't change the outcome. Ask a human maintainer to review it, since a public API addition needs that sign-off.
Gate mechanics and policy version
| Gate | Result | |
|---|---|---|
| prerequisites | ✓ | all clear |
| deny-list | ✗ | matches: public_api |
| size | ✓ | 124L, 2F substantive, 336L/5F incl. docs/generated/snapshots — within ceiling |
| tier | ✗ | classified as T2-never: T2-never (336L, 5F, cross-cutting, feat) |
| stamphog 2.7.0 | .stamphog/policy.yml @ unknown · reviewed head a080862 |
| attachments, (str, bytes, bytearray, memoryview, Mapping) | ||
| ) or not isinstance(attachments, Sequence): | ||
| raise _invalid_attachment("attachments must be a list of files.", "attachments") | ||
| return [_attachment(index, file) for index, file in enumerate(attachments)] |
There was a problem hiding this comment.
Medium: Bound attachments before encoding
The documented 10-file/512 KB limits are not checked before this comprehension encodes every attachment and _encode materializes the JSON body; an isolated preparation check accepted an 8 MiB file and allocated roughly 32 MiB beyond the original input. As defense in depth for applications forwarding user uploads, check file count and aggregate size before encoding, including bounds for Base64 strings, since server-side rejection cannot protect the host process from these allocations.
There was a problem hiding this comment.
Not changing this. The server owns the 10-file and 512 KB limits, so it can raise them without a new SDK release. A copy of the limits here would drift. The caller already holds the file in memory, and Base64 adds a third to its size. That cost is the caller's choice, and the server refuses the send before anything goes out.
PR overviewThe PR extends emails.send to support CC and BCC recipients and email attachments, including encoding attachments for the request body. One issue remains open: attachment count and size limits are not enforced before encoding and JSON serialization, allowing oversized inputs to cause substantial memory allocations in the calling application. This is a defense-in-depth gap for applications forwarding user uploads, since server-side rejection occurs too late to prevent those local allocations. No issues have been addressed yet. Open issues (1)
Fixed/addressed: 0 · PR risk: 4/10 |
…/emails-code-attachments # Conflicts: # posthog/emails.py # posthog/test/test_emails.py
A text file passed as a str was sent unchecked as if it were Base64. The client now raises invalid_attachment before any request and hints to pass the file's bytes instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The base PR owns emails-code-form.md, so this PR no longer edits it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
💡 Motivation and Context
Problem: an app moving its email from Resend to PostHog can't send a receipt with a PDF, or copy finance on an invoice.
posthog.emails.sendtakes renderedhtml/textonly (#1171).Change: the code form takes
cc,bccandattachments, as the send contract defines them.cc/bcc: one address or a list. A string becomes a one-item list on the wire.cc,bccorattachmentslist is left out, as feat: send email content from code with posthog.emails.send #1171 does forreply_to, so an older server still sends the email.attachments:{filename, content, content_type?, content_id?}. Bytes are Base64-encoded once, before the retry loop. A string is sent as is.pathkey (Resend style), an unknown key, a missing or emptyfilename, or content that isn't bytes or a Base64 string raisesinvalid_attachmentwith afieldsuch asattachments__0__content. The error never repeats the content.template_id, all three fields are refused before any request.Stacked on #1171. It mirrors posthog-node in PostHog/posthog-js#5514.
Impact: Grow Workflows. It removes a blocker for Resend migrations, seen as code sends with
migrated_fromin project 2.Merge danger: low, two-way door. The fields are new and optional in an unreleased API. Release this only after the server half deploys; until then a send with these fields gets
400.💚 How did you test it?
ac102e0: CI green, review clean, and both owed proofs captured.emails.sendposts (bytes become Base64,content_type/content_idmapped), the refusals on the template form, and theinvalid_attachmentcases. The sync and async clients are both covered.attachmentsand nothing is sent. A plain send still delivers.📝 Checklist
If releasing new changes
sampo addto generate a changeset file🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Written by Claude Opus 5.5 (
claude-opus-5-5) in Claude Code, for Michael. The fields come back from the pre-cut code of #1171 (e654aef), without headers, tags or local templates.Involvement: 2 questions answered. Reviews: GPT-6.1 Sol and Opus 5.5, rounds 1 and 2 and five final reads; StampHog. Proof captured: live run and cross-version.
🤖 Generated with Claude Code