feat: forward custom headers and send_to_unsubscribed to the SDK - #2
Merged
Conversation
The Customer.io API accepts custom email headers and a per-send `send_to_unsubscribed` override, and the underlying `customerio` SDK exposes both on `SendEmailRequest` — but this wrapper's `_build_email_request` forwarded neither, so callers had no way to reach them. The practical consequence was that RFC 8058 one-click unsubscribe was impossible to send. Gmail and Yahoo only render their one-click unsubscribe button when a message carries *both* `List-Unsubscribe` and `List-Unsubscribe-Post: List-Unsubscribe=One-Click`; with no header support, notification-class mail could not meet their bulk-sender rules. `send_to_unsubscribed` matters for the same feature. When the opt-out of record lives in the caller's own database rather than in Customer.io, Customer.io's global `unsubscribed` flag tracks marketing consent only, and must not suppress notification mail the user still wants. Setting it per send keeps that decision in reviewable code instead of a dashboard checkbox invisible from the repo. Typed `bool | None` and forwarded on `is not None` rather than truthiness: `False` is a real instruction from the caller, and distinct from "leave it unset so the transactional message's own configuration applies". `SendEmailRequest._to_dict()` omits `None` fields, so that distinction survives to the wire — pinned by a test asserting on the serialized payload, not just the request attributes. Both parameters are threaded through `send_email()`, `send_text()` and `send_html()`; supporting headers on template sends but not inline ones would have been an arbitrary seam. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
headersandsend_to_unsubscribedpassthrough tosend_email(),send_text()andsend_html(). Both are already supported by the Customer.io API and by the underlyingcustomerioSDK'sSendEmailRequest—_build_email_requestjust never forwarded them, so callers had no way to reach them.Why
RFC 8058 one-click unsubscribe was impossible to send. Gmail and Yahoo only render their one-click unsubscribe button when a message carries both
List-UnsubscribeandList-Unsubscribe-Post: List-Unsubscribe=One-Click. With no header support, notification-class mail could not meet their bulk-sender rules.send_to_unsubscribedserves the same feature. When the opt-out of record lives in the caller's own database, Customer.io's globalunsubscribedflag tracks marketing consent only, and must not suppress notification mail the user still wants. Setting it per send keeps that decision in reviewable code rather than a dashboard checkbox that's invisible from the repo.Found while reviewing
livedinc/lived#511, which needs exactly this to satisfy its ticket's "independently unsubscribable" acceptance criterion.Design notes
bool | None, forwarded onis not Nonerather than truthiness.Falseis a real instruction from the caller, distinct from "leave it unset so the transactional message's own configuration applies".SendEmailRequest._to_dict()omitsNonefields, so that three-way distinction survives to the wire.send_emailcarries the RFC 8058 detail — both headers required, and the endpoint must answer an unauthenticatedPOSTwith a2xxand no redirect. That's the part which is easy to get wrong.Testing
TestHeaders,TestSendToUnsubscribed) covering template and inline sends, multi-recipient fan-out, and the unset case._to_dict()— the actual POST body — because accepting a kwarg is worthless if it isn't serialized. This pins the SDK contract.pre-commit run --all-files, 99 passed, coverage 90% (gate 80%),poetry buildOK.mypy src/reports 2 errors, both pre-existing onmainand untouched here (client.py:15missingcustomeriostubs,client.py:44no-any-return). mypy is configured inpyproject.tomlbut isn't in pre-commit or CI, so it isn't gating.Version
Bumped to
0.3.0with a CHANGELOG entry — additive, no breaking changes.🤖 Generated with Claude Code