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)
class AdminEmailResource:
 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.

AdminEmailResource(http: ctfy.sdk.base.BaseHttpClient)
27    def __init__(self, http: BaseHttpClient) -> None:
28        self._http = http
def templates(self) -> ctfy.server.models.EmailTemplateListResponse:
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.

def send_test( self, template: str, to: str = '', language: str = '') -> ctfy.server.models.TestEmailResult:
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.

def suppressions( self, *, limit: int = 100, offset: int = 0) -> ctfy.server.models.EmailSuppressionListResponse:
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.

def suppress( self, email: str, detail: str = '') -> ctfy.server.models.EmailSuppressionInfo:
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.

def unsuppress(self, email: str) -> bool:
 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.