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 = "") -> TestEmailResult: 39 """Mail one template's sample render to *to* (default: the 40 caller's own address). 41 42 ``sent`` is the provider's verdict, not an enqueue receipt — a 43 false with a populated ``error`` means the message was rejected. 44 """ 45 resp = self._http.request( 46 "POST", 47 "/admin/email/test", 48 json={"template": template, "to": to}, 49 ) 50 _raise_for_status(resp) 51 return TestEmailResult.model_validate(resp.json()) 52 53 def suppressions(self, *, limit: int = 100, offset: int = 0) -> EmailSuppressionListResponse: 54 """Addresses the platform will not mail, newest first. 55 56 ``webhook_configured`` is the field to read before concluding 57 anything from an empty list: without a Svix secret nothing is 58 ever recorded automatically, so "no bounces" and "nobody 59 watching" look identical. 60 """ 61 resp = self._http.request( 62 "GET", 63 "/admin/email/suppressions", 64 params={"limit": limit, "offset": offset}, 65 ) 66 _raise_for_status(resp) 67 return EmailSuppressionListResponse.model_validate(resp.json()) 68 69 def suppress(self, email: str, detail: str = "") -> EmailSuppressionInfo: 70 """Stop mailing an address, by operator decision. 71 72 Recorded as ``manual`` so it stays distinguishable from the 73 provider's own verdicts — the first question about a suppression 74 that turns out to be wrong is whether a human or a bounce put it 75 there. 76 """ 77 resp = self._http.request( 78 "POST", 79 "/admin/email/suppressions", 80 json={"email": email, "detail": detail}, 81 ) 82 _raise_for_status(resp) 83 return EmailSuppressionInfo.model_validate(resp.json()) 84 85 def unsuppress(self, email: str) -> bool: 86 """Let an address receive platform mail again. 87 88 The un-trap. A suppression is invisible to the person it affects 89 — their sign-in codes simply stop arriving — so an entry made by 90 a misconfigured receiving server has to be removable. 91 """ 92 resp = self._http.request("DELETE", f"/admin/email/suppressions/{email}") 93 _raise_for_status(resp) 94 removed = resp.json().get("removed") 95 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 = "") -> TestEmailResult: 40 """Mail one template's sample render to *to* (default: the 41 caller's own address). 42 43 ``sent`` is the provider's verdict, not an enqueue receipt — a 44 false with a populated ``error`` means the message was rejected. 45 """ 46 resp = self._http.request( 47 "POST", 48 "/admin/email/test", 49 json={"template": template, "to": to}, 50 ) 51 _raise_for_status(resp) 52 return TestEmailResult.model_validate(resp.json()) 53 54 def suppressions(self, *, limit: int = 100, offset: int = 0) -> EmailSuppressionListResponse: 55 """Addresses the platform will not mail, newest first. 56 57 ``webhook_configured`` is the field to read before concluding 58 anything from an empty list: without a Svix secret nothing is 59 ever recorded automatically, so "no bounces" and "nobody 60 watching" look identical. 61 """ 62 resp = self._http.request( 63 "GET", 64 "/admin/email/suppressions", 65 params={"limit": limit, "offset": offset}, 66 ) 67 _raise_for_status(resp) 68 return EmailSuppressionListResponse.model_validate(resp.json()) 69 70 def suppress(self, email: str, detail: str = "") -> EmailSuppressionInfo: 71 """Stop mailing an address, by operator decision. 72 73 Recorded as ``manual`` so it stays distinguishable from the 74 provider's own verdicts — the first question about a suppression 75 that turns out to be wrong is whether a human or a bounce put it 76 there. 77 """ 78 resp = self._http.request( 79 "POST", 80 "/admin/email/suppressions", 81 json={"email": email, "detail": detail}, 82 ) 83 _raise_for_status(resp) 84 return EmailSuppressionInfo.model_validate(resp.json()) 85 86 def unsuppress(self, email: str) -> bool: 87 """Let an address receive platform mail again. 88 89 The un-trap. A suppression is invisible to the person it affects 90 — their sign-in codes simply stop arriving — so an entry made by 91 a misconfigured receiving server has to be removable. 92 """ 93 resp = self._http.request("DELETE", f"/admin/email/suppressions/{email}") 94 _raise_for_status(resp) 95 removed = resp.json().get("removed") 96 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 = "") -> TestEmailResult: 40 """Mail one template's sample render to *to* (default: the 41 caller's own address). 42 43 ``sent`` is the provider's verdict, not an enqueue receipt — a 44 false with a populated ``error`` means the message was rejected. 45 """ 46 resp = self._http.request( 47 "POST", 48 "/admin/email/test", 49 json={"template": template, "to": to}, 50 ) 51 _raise_for_status(resp) 52 return TestEmailResult.model_validate(resp.json())
Mail one template's sample render to to (default: the caller's own address).
sent is the provider's verdict, not an enqueue receipt — a
false with a populated error means the message was rejected.
54 def suppressions(self, *, limit: int = 100, offset: int = 0) -> EmailSuppressionListResponse: 55 """Addresses the platform will not mail, newest first. 56 57 ``webhook_configured`` is the field to read before concluding 58 anything from an empty list: without a Svix secret nothing is 59 ever recorded automatically, so "no bounces" and "nobody 60 watching" look identical. 61 """ 62 resp = self._http.request( 63 "GET", 64 "/admin/email/suppressions", 65 params={"limit": limit, "offset": offset}, 66 ) 67 _raise_for_status(resp) 68 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.
70 def suppress(self, email: str, detail: str = "") -> EmailSuppressionInfo: 71 """Stop mailing an address, by operator decision. 72 73 Recorded as ``manual`` so it stays distinguishable from the 74 provider's own verdicts — the first question about a suppression 75 that turns out to be wrong is whether a human or a bounce put it 76 there. 77 """ 78 resp = self._http.request( 79 "POST", 80 "/admin/email/suppressions", 81 json={"email": email, "detail": detail}, 82 ) 83 _raise_for_status(resp) 84 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.
86 def unsuppress(self, email: str) -> bool: 87 """Let an address receive platform mail again. 88 89 The un-trap. A suppression is invisible to the person it affects 90 — their sign-in codes simply stop arriving — so an entry made by 91 a misconfigured receiving server has to be removable. 92 """ 93 resp = self._http.request("DELETE", f"/admin/email/suppressions/{email}") 94 _raise_for_status(resp) 95 removed = resp.json().get("removed") 96 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.