ctfy.sdk.admin_resources.email
client.admin.email — preview and test-send the mail templates.
1"""``client.admin.email`` — preview and test-send the mail templates.""" 2 3from __future__ import annotations 4 5from ctfy.sdk._helpers import _raise_for_status 6from ctfy.sdk.base import BaseHttpClient 7from ctfy.server.models import ( 8 EmailSuppressionInfo, 9 EmailSuppressionListResponse, 10 EmailTemplateListResponse, 11 TestEmailResult, 12) 13 14 15class AdminEmailResource: 16 """The only path that puts a rendered template into a real inbox. 17 18 A template's failure mode is a rendering one that only a specific 19 client exhibits, so judging one means mailing it somewhere and 20 looking. Sends are synchronous (the caller wants the verdict, not a 21 queue receipt), skip the recipient's notification preferences (an 22 admin asking to see a template is not the platform contacting them), 23 and are audited server-side with the address. 24 """ 25 26 def __init__(self, http: BaseHttpClient) -> None: 27 self._http = http 28 29 def templates(self) -> EmailTemplateListResponse: 30 """Every template with its preference category, whether that 31 category is forced, and the subject its sample data renders — 32 which doubles as a check that template and sample still agree. 33 """ 34 resp = self._http.request("GET", "/admin/email/templates") 35 _raise_for_status(resp) 36 return EmailTemplateListResponse.model_validate(resp.json()) 37 38 def send_test(self, template: str, to: str = "", language: str = "") -> TestEmailResult: 39 """Mail one template's sample render to *to* (default: the 40 caller's own address). 41 42 *language* is the shape to send: ``zh-CN`` or ``en`` (what a 43 recipient with a known language gets) or ``""`` — the Chinese 44 message followed by the English one, which is what a recipient 45 with no known language gets. 46 47 ``sent`` is the provider's verdict, not an enqueue receipt — a 48 false with a populated ``error`` means the message was rejected. 49 """ 50 resp = self._http.request( 51 "POST", 52 "/admin/email/test", 53 json={"template": template, "to": to, "language": language}, 54 ) 55 _raise_for_status(resp) 56 return TestEmailResult.model_validate(resp.json()) 57 58 def suppressions(self, *, limit: int = 100, offset: int = 0) -> EmailSuppressionListResponse: 59 """Addresses the platform will not mail, newest first. 60 61 ``webhook_configured`` is the field to read before concluding 62 anything from an empty list: without a Svix secret nothing is 63 ever recorded automatically, so "no bounces" and "nobody 64 watching" look identical. 65 """ 66 resp = self._http.request( 67 "GET", 68 "/admin/email/suppressions", 69 params={"limit": limit, "offset": offset}, 70 ) 71 _raise_for_status(resp) 72 return EmailSuppressionListResponse.model_validate(resp.json()) 73 74 def suppress(self, email: str, detail: str = "") -> EmailSuppressionInfo: 75 """Stop mailing an address, by operator decision. 76 77 Recorded as ``manual`` so it stays distinguishable from the 78 provider's own verdicts — the first question about a suppression 79 that turns out to be wrong is whether a human or a bounce put it 80 there. 81 """ 82 resp = self._http.request( 83 "POST", 84 "/admin/email/suppressions", 85 json={"email": email, "detail": detail}, 86 ) 87 _raise_for_status(resp) 88 return EmailSuppressionInfo.model_validate(resp.json()) 89 90 def unsuppress(self, email: str) -> bool: 91 """Let an address receive platform mail again. 92 93 The un-trap. A suppression is invisible to the person it affects 94 — their sign-in codes simply stop arriving — so an entry made by 95 a misconfigured receiving server has to be removable. 96 """ 97 resp = self._http.request("DELETE", f"/admin/email/suppressions/{email}") 98 _raise_for_status(resp) 99 removed = resp.json().get("removed") 100 return bool(removed)
16class AdminEmailResource: 17 """The only path that puts a rendered template into a real inbox. 18 19 A template's failure mode is a rendering one that only a specific 20 client exhibits, so judging one means mailing it somewhere and 21 looking. Sends are synchronous (the caller wants the verdict, not a 22 queue receipt), skip the recipient's notification preferences (an 23 admin asking to see a template is not the platform contacting them), 24 and are audited server-side with the address. 25 """ 26 27 def __init__(self, http: BaseHttpClient) -> None: 28 self._http = http 29 30 def templates(self) -> EmailTemplateListResponse: 31 """Every template with its preference category, whether that 32 category is forced, and the subject its sample data renders — 33 which doubles as a check that template and sample still agree. 34 """ 35 resp = self._http.request("GET", "/admin/email/templates") 36 _raise_for_status(resp) 37 return EmailTemplateListResponse.model_validate(resp.json()) 38 39 def send_test(self, template: str, to: str = "", language: str = "") -> TestEmailResult: 40 """Mail one template's sample render to *to* (default: the 41 caller's own address). 42 43 *language* is the shape to send: ``zh-CN`` or ``en`` (what a 44 recipient with a known language gets) or ``""`` — the Chinese 45 message followed by the English one, which is what a recipient 46 with no known language gets. 47 48 ``sent`` is the provider's verdict, not an enqueue receipt — a 49 false with a populated ``error`` means the message was rejected. 50 """ 51 resp = self._http.request( 52 "POST", 53 "/admin/email/test", 54 json={"template": template, "to": to, "language": language}, 55 ) 56 _raise_for_status(resp) 57 return TestEmailResult.model_validate(resp.json()) 58 59 def suppressions(self, *, limit: int = 100, offset: int = 0) -> EmailSuppressionListResponse: 60 """Addresses the platform will not mail, newest first. 61 62 ``webhook_configured`` is the field to read before concluding 63 anything from an empty list: without a Svix secret nothing is 64 ever recorded automatically, so "no bounces" and "nobody 65 watching" look identical. 66 """ 67 resp = self._http.request( 68 "GET", 69 "/admin/email/suppressions", 70 params={"limit": limit, "offset": offset}, 71 ) 72 _raise_for_status(resp) 73 return EmailSuppressionListResponse.model_validate(resp.json()) 74 75 def suppress(self, email: str, detail: str = "") -> EmailSuppressionInfo: 76 """Stop mailing an address, by operator decision. 77 78 Recorded as ``manual`` so it stays distinguishable from the 79 provider's own verdicts — the first question about a suppression 80 that turns out to be wrong is whether a human or a bounce put it 81 there. 82 """ 83 resp = self._http.request( 84 "POST", 85 "/admin/email/suppressions", 86 json={"email": email, "detail": detail}, 87 ) 88 _raise_for_status(resp) 89 return EmailSuppressionInfo.model_validate(resp.json()) 90 91 def unsuppress(self, email: str) -> bool: 92 """Let an address receive platform mail again. 93 94 The un-trap. A suppression is invisible to the person it affects 95 — their sign-in codes simply stop arriving — so an entry made by 96 a misconfigured receiving server has to be removable. 97 """ 98 resp = self._http.request("DELETE", f"/admin/email/suppressions/{email}") 99 _raise_for_status(resp) 100 removed = resp.json().get("removed") 101 return bool(removed)
The only path that puts a rendered template into a real inbox.
A template's failure mode is a rendering one that only a specific client exhibits, so judging one means mailing it somewhere and looking. Sends are synchronous (the caller wants the verdict, not a queue receipt), skip the recipient's notification preferences (an admin asking to see a template is not the platform contacting them), and are audited server-side with the address.
30 def templates(self) -> EmailTemplateListResponse: 31 """Every template with its preference category, whether that 32 category is forced, and the subject its sample data renders — 33 which doubles as a check that template and sample still agree. 34 """ 35 resp = self._http.request("GET", "/admin/email/templates") 36 _raise_for_status(resp) 37 return EmailTemplateListResponse.model_validate(resp.json())
Every template with its preference category, whether that category is forced, and the subject its sample data renders — which doubles as a check that template and sample still agree.
39 def send_test(self, template: str, to: str = "", language: str = "") -> TestEmailResult: 40 """Mail one template's sample render to *to* (default: the 41 caller's own address). 42 43 *language* is the shape to send: ``zh-CN`` or ``en`` (what a 44 recipient with a known language gets) or ``""`` — the Chinese 45 message followed by the English one, which is what a recipient 46 with no known language gets. 47 48 ``sent`` is the provider's verdict, not an enqueue receipt — a 49 false with a populated ``error`` means the message was rejected. 50 """ 51 resp = self._http.request( 52 "POST", 53 "/admin/email/test", 54 json={"template": template, "to": to, "language": language}, 55 ) 56 _raise_for_status(resp) 57 return TestEmailResult.model_validate(resp.json())
Mail one template's sample render to to (default: the caller's own address).
language is the shape to send: zh-CN or en (what a
recipient with a known language gets) or "" — the Chinese
message followed by the English one, which is what a recipient
with no known language gets.
sent is the provider's verdict, not an enqueue receipt — a
false with a populated error means the message was rejected.
59 def suppressions(self, *, limit: int = 100, offset: int = 0) -> EmailSuppressionListResponse: 60 """Addresses the platform will not mail, newest first. 61 62 ``webhook_configured`` is the field to read before concluding 63 anything from an empty list: without a Svix secret nothing is 64 ever recorded automatically, so "no bounces" and "nobody 65 watching" look identical. 66 """ 67 resp = self._http.request( 68 "GET", 69 "/admin/email/suppressions", 70 params={"limit": limit, "offset": offset}, 71 ) 72 _raise_for_status(resp) 73 return EmailSuppressionListResponse.model_validate(resp.json())
Addresses the platform will not mail, newest first.
webhook_configured is the field to read before concluding
anything from an empty list: without a Svix secret nothing is
ever recorded automatically, so "no bounces" and "nobody
watching" look identical.
75 def suppress(self, email: str, detail: str = "") -> EmailSuppressionInfo: 76 """Stop mailing an address, by operator decision. 77 78 Recorded as ``manual`` so it stays distinguishable from the 79 provider's own verdicts — the first question about a suppression 80 that turns out to be wrong is whether a human or a bounce put it 81 there. 82 """ 83 resp = self._http.request( 84 "POST", 85 "/admin/email/suppressions", 86 json={"email": email, "detail": detail}, 87 ) 88 _raise_for_status(resp) 89 return EmailSuppressionInfo.model_validate(resp.json())
Stop mailing an address, by operator decision.
Recorded as manual so it stays distinguishable from the
provider's own verdicts — the first question about a suppression
that turns out to be wrong is whether a human or a bounce put it
there.
91 def unsuppress(self, email: str) -> bool: 92 """Let an address receive platform mail again. 93 94 The un-trap. A suppression is invisible to the person it affects 95 — their sign-in codes simply stop arriving — so an entry made by 96 a misconfigured receiving server has to be removable. 97 """ 98 resp = self._http.request("DELETE", f"/admin/email/suppressions/{email}") 99 _raise_for_status(resp) 100 removed = resp.json().get("removed") 101 return bool(removed)
Let an address receive platform mail again.
The un-trap. A suppression is invisible to the person it affects — their sign-in codes simply stop arriving — so an entry made by a misconfigured receiving server has to be removable.