Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
a7cd02c
feat(messaging): add code-managed fields to message templates
Silthus Oct 31, 2026
5ce7197
feat(messaging): upload email templates from code by key
Silthus Oct 31, 2026
7a74ced
fix(messaging): check the key's workflow before an upload takes its c…
Silthus Oct 31, 2026
e8fff1d
feat(messaging): lock code-managed templates and adopt templates into…
Silthus Oct 31, 2026
98e10b5
chore(messaging): test the key workflow check when an upload adopts a…
Silthus Oct 31, 2026
3d3a661
feat(workflows): link a file and ref on any known git host
Silthus Oct 25, 2026
ca3de9c
feat(workflows): show code-managed email templates read-only
Silthus Oct 25, 2026
d161fa7
feat(workflows): show the sender on a read-only email template
Silthus Oct 25, 2026
30b4ac8
feat(messaging): send an uploaded email template by its key
Silthus Oct 31, 2026
faade61
fix(workflows): say when a read-only template's sender could not load
Silthus Oct 25, 2026
be7252c
fix(messaging): refuse unknown fields on a send by template key
Silthus Oct 31, 2026
a1d56b3
fix(workflows): take a released template's content as published
Silthus Oct 25, 2026
83f56a3
fix(workflows): treat only a full commit hash as a commit in a templa…
Silthus Oct 25, 2026
b6b4404
chore(workflows): serialize an email preview's doctype with XMLSerial…
Silthus Oct 25, 2026
c94a372
fix(messaging): show only the banner when an upload refuses a templat…
Silthus Oct 26, 2026
1390333
fix(workflows): drop an empty design from a template copy
Silthus Oct 31, 2026
1c8455d
feat(messaging): point a sender refusal at the from a send names
Silthus Oct 31, 2026
7ea5657
Merge branch 'email-v2/03-mcp-send-email' into email-v2/04a-template-…
Silthus Nov 1, 2026
ad25625
fix(messaging): say what a null managed_by means on a template
Silthus Nov 1, 2026
cb7a9a1
Merge branch 'email-v2/04a-template-code-fields' into email-v2/04-tem…
Silthus Nov 1, 2026
979d1e0
fix(messaging): repair a drifted workflow on upload and keep paused c…
Silthus Nov 1, 2026
8c9fa9f
chore(messaging): drop an unused key lookup and test an unclaimed key
Silthus Nov 1, 2026
c3a9531
Merge branch 'email-v2/04-template-uploads' into email-v2/04c-templat…
Silthus Nov 1, 2026
62c59f3
fix(messaging): check the key workflow's access on adoption and name …
Silthus Nov 1, 2026
6a4522a
Merge branch 'email-v2/04c-template-lock-adopt' into email-v2/05-temp…
Silthus Nov 1, 2026
dd818ef
fix(messaging): compare a drifted workflow with the text a code templ…
Silthus Nov 1, 2026
723e264
fix(messaging): take capture_engagement on a send by template id and …
Silthus Nov 1, 2026
45f48eb
Merge branch 'email-v2/05-template-send' into email-v2/06-template-ed…
Silthus Nov 1, 2026
cb45a25
fix(workflows): tell a failed template release once and keep refused …
Silthus Nov 1, 2026
af9921b
Merge branch 'email-v2/06-template-editor' into email-v2/08-admission…
Silthus Nov 1, 2026
79554a8
fix(messaging): answer a refused named sender as invalid_request like…
Silthus Nov 1, 2026
e852cd9
fix(messaging): name the paused key workflow an upload asks to resume
Silthus Nov 1, 2026
2e23381
chore(messaging): say why send refusals keep the invalid_request type
Silthus Nov 1, 2026
5cc95f4
Merge branch 'email-v2/04-template-uploads' into email-v2/04c-templat…
Silthus Nov 1, 2026
1da8b44
Merge branch 'email-v2/04c-template-lock-adopt' into email-v2/05-temp…
Silthus Nov 1, 2026
c4e4a65
Merge branch 'email-v2/05-template-send' into email-v2/06-template-ed…
Silthus Nov 1, 2026
27d62ee
Merge branch 'email-v2/06-template-editor' into email-v2/08-admission…
Silthus Nov 1, 2026
df50152
fix(messaging): repair a workflow whose recipient changed on re-upload
Silthus Nov 1, 2026
aa16f12
Merge branch 'email-v2/03-mcp-send-email' into email-v2/04a-template-…
Silthus Nov 1, 2026
6df2894
Merge branch 'email-v2/04a-template-code-fields' into email-v2/04-tem…
Silthus Nov 1, 2026
c9db420
Merge branch 'email-v2/04-template-uploads' into email-v2/04c-templat…
Silthus Nov 1, 2026
33e700b
Merge branch 'email-v2/04c-template-lock-adopt' into email-v2/05-temp…
Silthus Nov 1, 2026
d9bdbd8
Merge branch 'email-v2/05-template-send' into email-v2/06-template-ed…
Silthus Nov 1, 2026
c8a4107
Merge branch 'email-v2/06-template-editor' into email-v2/08-admission…
Silthus Nov 1, 2026
9e0c2ea
fix(messaging): accept a workflow with no stored sender when checking…
Silthus Nov 1, 2026
c336ecc
Merge branch 'email-v2/04c-template-lock-adopt' into email-v2/05-temp…
Silthus Nov 1, 2026
29fe02c
Merge branch 'email-v2/05-template-send' into email-v2/06-template-ed…
Silthus Nov 1, 2026
4584bd8
Merge branch 'email-v2/04-template-uploads' into email-v2/04c-templat…
Silthus Nov 1, 2026
20e241f
Merge branch 'email-v2/06-template-editor' into email-v2/08-admission…
Silthus Nov 1, 2026
6d6c5a2
fix(messaging): keep a send by template id on master's route while em…
Silthus Nov 2, 2026
4ca4be0
Merge branch 'email-v2/05-template-send' into email-v2/06-template-ed…
Silthus Nov 2, 2026
95743b9
Merge branch 'email-v2/06-template-editor' into email-v2/08-admission…
Silthus Nov 2, 2026
30b7e40
fix(messaging): refuse capture_engagement on a send by template id ou…
Silthus Nov 2, 2026
d8e43e9
Merge branch 'email-v2/05-template-send' into email-v2/06-template-ed…
Silthus Nov 2, 2026
fd3d820
Merge branch 'email-v2/06-template-editor' into email-v2/08-admission…
Silthus Nov 2, 2026
0f27c16
fix(messaging): refuse capture_engagement only when the send sets it
Silthus Nov 2, 2026
8c5a0af
Merge branch 'email-v2/05-template-send' into email-v2/06-template-ed…
Silthus Nov 2, 2026
8dbf1f7
Merge branch 'email-v2/06-template-editor' into email-v2/08-admission…
Silthus Nov 2, 2026
d090f43
fix(messaging): refuse an if_code_managed upload to a template edited…
Silthus Nov 2, 2026
9974101
Merge branch 'email-v2/04c-template-lock-adopt' into email-v2/05-temp…
Silthus Nov 2, 2026
395cd6c
Merge branch 'email-v2/05-template-send' into email-v2/06-template-ed…
Silthus Nov 2, 2026
d479e16
Merge branch 'email-v2/06-template-editor' into email-v2/08-admission…
Silthus Nov 2, 2026
1899846
chore: update OpenAPI generated types
tests-posthog[bot] Nov 2, 2026
2071baa
chore: update OpenAPI generated types
tests-posthog[bot] Nov 2, 2026
b02e2a9
Merge branch 'email-v2/03-mcp-send-email' into email-v2/04a-template-…
Silthus Nov 2, 2026
a7e6732
Merge branch 'email-v2/04a-template-code-fields' into email-v2/04-tem…
Silthus Nov 2, 2026
b312421
fix(messaging): import HogFlow through the workflows facade in the up…
Silthus Nov 2, 2026
5cf748b
chore(messaging): sync the MCP email template tools with scaffold-yaml
Silthus Nov 2, 2026
8ab6006
Merge branch 'email-v2/04-template-uploads' into email-v2/04c-templat…
Silthus Nov 2, 2026
002a08f
fix(messaging): import HogFlow through the workflows facade in the ad…
Silthus Nov 2, 2026
b206f1b
Merge branch 'email-v2/04c-template-lock-adopt' into email-v2/05-temp…
Silthus Nov 2, 2026
204d110
Merge branch 'email-v2/05-template-send' into email-v2/06-template-ed…
Silthus Nov 2, 2026
5e09413
Merge branch 'email-v2/06-template-editor' into email-v2/08-admission…
Silthus Nov 2, 2026
f32ee01
feat(messaging): point a sender refusal at the from a send names
Silthus Oct 31, 2026
bd945e0
fix(messaging): answer a refused named sender as invalid_request like…
Silthus Nov 1, 2026
aea0171
chore(messaging): say why send refusals keep the invalid_request type
Silthus Nov 1, 2026
72e5f56
Merge the 08 re-cut onto email-v2/03-mcp-send-email into email-v2/08-…
Silthus Nov 2, 2026
8b998cd
Merge branch 'email-v2/03-mcp-send-email' into email-v2/08-admission-…
Silthus Nov 4, 2026
565a57b
Merge branch 'email-v2/02-django-code-send' into email-v2/08-admissio…
Silthus Nov 4, 2026
800fae8
Merge branch 'email-v2/02-django-code-send' into email-v2/08-admissio…
Silthus Nov 4, 2026
5b3f20d
Merge branch 'email-v2/03-mcp-send-email' into email-v2/08-admission-…
Silthus Nov 4, 2026
6006214
chore(messaging): start CI on the base merge
Silthus Nov 4, 2026
bc6bb0c
Merge branch 'email-v2/02-django-code-send' into email-v2/08-admissio…
Silthus Nov 4, 2026
f707ecd
Merge remote-tracking branch 'origin/email-v2/02-django-code-send' in…
Silthus Nov 4, 2026
b405fef
Merge remote-tracking branch 'origin/email-v2/02-django-code-send' in…
Silthus Nov 4, 2026
0b3662a
Merge remote-tracking branch 'origin/email-v2/03-mcp-send-email' into…
Silthus Nov 4, 2026
78eb463
Merge the MCP code send layer into the Django admission checks layer
Silthus Nov 4, 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
5 changes: 5 additions & 0 deletions products/messaging/backend/facade/contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class TransactionalEmailError(Exception):
"""A send the caller has to fix before retrying. `code` is stable for API clients."""

code = "invalid"
attr: str | None = None


class TemplateNotSendable(TransactionalEmailError):
Expand Down Expand Up @@ -102,6 +103,10 @@ class SenderNotVerified(TransactionalEmailError):
code = "sender_not_verified"


class NamedSenderNotVerified(SenderNotVerified):
attr = "from"


class SenderRequired(TransactionalEmailError):
code = "sender_required"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -597,7 +597,7 @@ class SendTransactionalEmailResponseSerializer(serializers.Serializer):
)


class TransactionalEmailRejected(exceptions.APIException):
class TransactionalEmailRejected(exceptions.ValidationError):
STATUS_BY_CODE = {
"quota_limited": 429,
"idempotency_key_in_flight": 409,
Expand All @@ -607,12 +607,12 @@ class TransactionalEmailRejected(exceptions.APIException):
"recipient_suppressed": 422,
"recipient_unreachable": 422,
}
# The caller has to change the request. Without this the error handler reports a server error.
# The caller has to change the request. Without this the error handler types it `validation_error`.
default_type = "invalid_request"

def __init__(self, error: TransactionalEmailError) -> None:
self.status_code = self.STATUS_BY_CODE.get(error.code, status.HTTP_400_BAD_REQUEST)
super().__init__(detail=str(error), code=error.code)
super().__init__(detail={error.attr: [str(error)]} if error.attr else str(error), code=error.code)


class TransactionalEmailUnavailable(exceptions.APIException):
Expand Down
3 changes: 2 additions & 1 deletion products/messaging/backend/services/transactional_email.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
InvalidRecipient,
InvalidRequest,
MarketingTemplate,
NamedSenderNotVerified,
QueuedEmail,
QuotaLimited,
RecipientSuppressed,
Expand Down Expand Up @@ -557,7 +558,7 @@ def _code_send_sender(team_id: int, address: str | None) -> dict[str, Any]:
) from e
except TransactionalEmailSenderUnavailable as e:
if address:
raise SenderNotVerified(f"No verified sender in this project can send from `{email}`.") from e
raise NamedSenderNotVerified(f"No verified sender in this project can send from `{email}`.") from e
raise SenderNotVerified("This project has no verified email sender. Verify a sender domain first.") from e
return {"integrationId": sender_id, "email": email, "name": name}

Expand Down
12 changes: 10 additions & 2 deletions products/messaging/backend/tests/api/test_transactional_email.py
Original file line number Diff line number Diff line change
Expand Up @@ -736,7 +736,11 @@ def test_a_code_send_without_from_and_without_a_default_sends_from(
if code is None:
assert json.loads(mock_post.call_args.kwargs["data"])["send"]["from"] == {"integrationId": created[0].id}
else:
assert response.json()["code"] == code
assert (response.json()["type"], response.json()["code"], response.json()["attr"]) == (
"invalid_request",
code,
None,
)
mock_post.assert_not_called()

def test_refuses_a_code_send_from_an_unverified_domain(self, mock_post, _flag, capture):
Expand All @@ -745,7 +749,11 @@ def test_refuses_a_code_send_from_an_unverified_domain(self, mock_post, _flag, c
)

assert response.status_code == status.HTTP_400_BAD_REQUEST, response.json()
assert response.json()["code"] == "sender_not_verified"
assert (response.json()["type"], response.json()["code"], response.json()["attr"]) == (
"invalid_request",
"sender_not_verified",
"from",
)
assert not TransactionalEmailKey.objects.for_team(self.team.id, canonical=True).exists()
assert capture.call_args.kwargs["properties"]["error_code"] == "sender_not_verified"
mock_post.assert_not_called()
Expand Down
Loading