sentroy-client-sdk 1.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sentroy/__init__.py +83 -0
- sentroy/_http.py +95 -0
- sentroy/domains.py +23 -0
- sentroy/inbox.py +127 -0
- sentroy/mailboxes.py +16 -0
- sentroy/send.py +16 -0
- sentroy/templates.py +23 -0
- sentroy/types.py +311 -0
- sentroy_client_sdk-1.1.5.dist-info/METADATA +230 -0
- sentroy_client_sdk-1.1.5.dist-info/RECORD +11 -0
- sentroy_client_sdk-1.1.5.dist-info/WHEEL +4 -0
sentroy/__init__.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import urllib.parse
|
|
4
|
+
|
|
5
|
+
from sentroy._http import SentroyError, _HttpClient
|
|
6
|
+
from sentroy.domains import DomainsResource
|
|
7
|
+
from sentroy.inbox import InboxResource
|
|
8
|
+
from sentroy.mailboxes import MailboxesResource
|
|
9
|
+
from sentroy.send import SendResource
|
|
10
|
+
from sentroy.templates import TemplatesResource
|
|
11
|
+
from sentroy.types import (
|
|
12
|
+
Attachment,
|
|
13
|
+
AttachmentInfo,
|
|
14
|
+
Domain,
|
|
15
|
+
InboxListParams,
|
|
16
|
+
LocalizedString,
|
|
17
|
+
Mailbox,
|
|
18
|
+
MailboxUser,
|
|
19
|
+
MessageAddress,
|
|
20
|
+
MessageDetail,
|
|
21
|
+
MessageSummary,
|
|
22
|
+
SendParams,
|
|
23
|
+
SendResult,
|
|
24
|
+
Template,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Sentroy:
|
|
29
|
+
"""Sentroy platform client.
|
|
30
|
+
|
|
31
|
+
Example::
|
|
32
|
+
|
|
33
|
+
sentroy = Sentroy(
|
|
34
|
+
base_url="https://sentroy.com",
|
|
35
|
+
company_slug="my-company",
|
|
36
|
+
access_token="stk_abc123...",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
domains = sentroy.domains.list()
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
domains: DomainsResource
|
|
43
|
+
mailboxes: MailboxesResource
|
|
44
|
+
templates: TemplatesResource
|
|
45
|
+
inbox: InboxResource
|
|
46
|
+
send: SendResource
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
*,
|
|
51
|
+
base_url: str,
|
|
52
|
+
company_slug: str,
|
|
53
|
+
access_token: str,
|
|
54
|
+
timeout: int = 30,
|
|
55
|
+
) -> None:
|
|
56
|
+
base = base_url.rstrip("/")
|
|
57
|
+
api_base = f"{base}/api/companies/{urllib.parse.quote(company_slug, safe='')}"
|
|
58
|
+
http = _HttpClient(api_base, access_token, timeout)
|
|
59
|
+
|
|
60
|
+
self.domains = DomainsResource(http)
|
|
61
|
+
self.mailboxes = MailboxesResource(http)
|
|
62
|
+
self.templates = TemplatesResource(http)
|
|
63
|
+
self.inbox = InboxResource(http)
|
|
64
|
+
self.send = SendResource(http)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
__all__ = [
|
|
68
|
+
"Sentroy",
|
|
69
|
+
"SentroyError",
|
|
70
|
+
"Attachment",
|
|
71
|
+
"AttachmentInfo",
|
|
72
|
+
"Domain",
|
|
73
|
+
"InboxListParams",
|
|
74
|
+
"LocalizedString",
|
|
75
|
+
"Mailbox",
|
|
76
|
+
"MailboxUser",
|
|
77
|
+
"MessageAddress",
|
|
78
|
+
"MessageDetail",
|
|
79
|
+
"MessageSummary",
|
|
80
|
+
"SendParams",
|
|
81
|
+
"SendResult",
|
|
82
|
+
"Template",
|
|
83
|
+
]
|
sentroy/_http.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import urllib.request
|
|
5
|
+
import urllib.parse
|
|
6
|
+
import urllib.error
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SentroyError(Exception):
|
|
11
|
+
"""Raised when the Sentroy API returns an error response."""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
status_code: int,
|
|
16
|
+
body: Any,
|
|
17
|
+
message: Optional[str] = None,
|
|
18
|
+
) -> None:
|
|
19
|
+
self.status_code = status_code
|
|
20
|
+
self.body = body
|
|
21
|
+
super().__init__(message or f"Sentroy API error ({status_code})")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class _HttpClient:
|
|
25
|
+
"""Low-level HTTP client that wraps urllib.request for Sentroy API calls."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, base_url: str, token: str, timeout: int = 30) -> None:
|
|
28
|
+
self._base_url = base_url.rstrip("/")
|
|
29
|
+
self._token = token
|
|
30
|
+
self._timeout = timeout
|
|
31
|
+
|
|
32
|
+
def _build_url(
|
|
33
|
+
self,
|
|
34
|
+
path: str,
|
|
35
|
+
query: Optional[dict[str, Any]] = None,
|
|
36
|
+
) -> str:
|
|
37
|
+
url = f"{self._base_url}{path}"
|
|
38
|
+
if query:
|
|
39
|
+
filtered = {
|
|
40
|
+
k: str(v) for k, v in query.items() if v is not None
|
|
41
|
+
}
|
|
42
|
+
if filtered:
|
|
43
|
+
url = f"{url}?{urllib.parse.urlencode(filtered)}"
|
|
44
|
+
return url
|
|
45
|
+
|
|
46
|
+
def _request(
|
|
47
|
+
self,
|
|
48
|
+
method: str,
|
|
49
|
+
path: str,
|
|
50
|
+
query: Optional[dict[str, Any]] = None,
|
|
51
|
+
body: Optional[Any] = None,
|
|
52
|
+
) -> Any:
|
|
53
|
+
url = self._build_url(path, query)
|
|
54
|
+
|
|
55
|
+
headers: dict[str, str] = {
|
|
56
|
+
"Authorization": f"Bearer {self._token}",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
data: Optional[bytes] = None
|
|
60
|
+
if body is not None:
|
|
61
|
+
headers["Content-Type"] = "application/json"
|
|
62
|
+
data = json.dumps(body).encode("utf-8")
|
|
63
|
+
|
|
64
|
+
req = urllib.request.Request(
|
|
65
|
+
url,
|
|
66
|
+
data=data,
|
|
67
|
+
headers=headers,
|
|
68
|
+
method=method,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
with urllib.request.urlopen(req, timeout=self._timeout) as resp:
|
|
73
|
+
raw = resp.read().decode("utf-8")
|
|
74
|
+
envelope = json.loads(raw) if raw else {}
|
|
75
|
+
return envelope.get("data")
|
|
76
|
+
except urllib.error.HTTPError as exc:
|
|
77
|
+
raw_body = exc.read().decode("utf-8") if exc.fp else ""
|
|
78
|
+
try:
|
|
79
|
+
envelope = json.loads(raw_body)
|
|
80
|
+
except (json.JSONDecodeError, ValueError):
|
|
81
|
+
envelope = {"error": raw_body}
|
|
82
|
+
message = envelope.get("error") or f"Request failed with status {exc.code}"
|
|
83
|
+
raise SentroyError(exc.code, envelope, message) from exc
|
|
84
|
+
|
|
85
|
+
def get(self, path: str, query: Optional[dict[str, Any]] = None) -> Any:
|
|
86
|
+
return self._request("GET", path, query=query)
|
|
87
|
+
|
|
88
|
+
def post(self, path: str, body: Optional[Any] = None) -> Any:
|
|
89
|
+
return self._request("POST", path, body=body)
|
|
90
|
+
|
|
91
|
+
def put(self, path: str, body: Optional[Any] = None) -> Any:
|
|
92
|
+
return self._request("PUT", path, body=body)
|
|
93
|
+
|
|
94
|
+
def delete(self, path: str, query: Optional[dict[str, Any]] = None) -> Any:
|
|
95
|
+
return self._request("DELETE", path, query=query)
|
sentroy/domains.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import urllib.parse
|
|
4
|
+
|
|
5
|
+
from sentroy._http import _HttpClient
|
|
6
|
+
from sentroy.types import Domain
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DomainsResource:
|
|
10
|
+
"""Interact with the Domains API."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, http: _HttpClient) -> None:
|
|
13
|
+
self._http = http
|
|
14
|
+
|
|
15
|
+
def list(self) -> list[Domain]:
|
|
16
|
+
"""List all verified domains for the company."""
|
|
17
|
+
data = self._http.get("/domains")
|
|
18
|
+
return [Domain.from_dict(d) for d in (data or [])]
|
|
19
|
+
|
|
20
|
+
def get(self, id: str) -> Domain:
|
|
21
|
+
"""Get a single domain by ID."""
|
|
22
|
+
data = self._http.get(f"/domains/{urllib.parse.quote(id, safe='')}")
|
|
23
|
+
return Domain.from_dict(data)
|
sentroy/inbox.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
from sentroy._http import _HttpClient
|
|
6
|
+
from sentroy.types import (
|
|
7
|
+
InboxListParams,
|
|
8
|
+
Mailbox,
|
|
9
|
+
MessageDetail,
|
|
10
|
+
MessageSummary,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class InboxResource:
|
|
15
|
+
"""Interact with the Inbox API."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, http: _HttpClient) -> None:
|
|
18
|
+
self._http = http
|
|
19
|
+
|
|
20
|
+
def list(self, params: Optional[InboxListParams] = None) -> list[MessageSummary]:
|
|
21
|
+
"""List messages in a mailbox folder."""
|
|
22
|
+
query: dict[str, Any] = {}
|
|
23
|
+
if params is not None:
|
|
24
|
+
if params.mailbox is not None:
|
|
25
|
+
query["mailbox"] = params.mailbox
|
|
26
|
+
if params.folder is not None:
|
|
27
|
+
query["folder"] = params.folder
|
|
28
|
+
if params.page is not None:
|
|
29
|
+
query["page"] = params.page
|
|
30
|
+
if params.limit is not None:
|
|
31
|
+
query["limit"] = params.limit
|
|
32
|
+
if params.unread_only:
|
|
33
|
+
query["unreadOnly"] = "true"
|
|
34
|
+
data = self._http.get("/inbox", query or None)
|
|
35
|
+
return [MessageSummary.from_dict(d) for d in (data or [])]
|
|
36
|
+
|
|
37
|
+
def get(
|
|
38
|
+
self,
|
|
39
|
+
uid: int,
|
|
40
|
+
*,
|
|
41
|
+
mailbox: Optional[str] = None,
|
|
42
|
+
folder: Optional[str] = None,
|
|
43
|
+
) -> MessageDetail:
|
|
44
|
+
"""Get a single message detail."""
|
|
45
|
+
query: dict[str, Any] = {}
|
|
46
|
+
if mailbox is not None:
|
|
47
|
+
query["mailbox"] = mailbox
|
|
48
|
+
if folder is not None:
|
|
49
|
+
query["folder"] = folder
|
|
50
|
+
data = self._http.get(f"/inbox/{uid}", query or None)
|
|
51
|
+
return MessageDetail.from_dict(data)
|
|
52
|
+
|
|
53
|
+
def list_folders(self, mailbox: Optional[str] = None) -> list[Mailbox]:
|
|
54
|
+
"""List IMAP folders (mailboxes) for a given email account."""
|
|
55
|
+
query: dict[str, Any] = {}
|
|
56
|
+
if mailbox is not None:
|
|
57
|
+
query["mailbox"] = mailbox
|
|
58
|
+
data = self._http.get("/inbox/mailboxes", query or None)
|
|
59
|
+
return [Mailbox.from_dict(d) for d in (data or [])]
|
|
60
|
+
|
|
61
|
+
def get_thread(
|
|
62
|
+
self,
|
|
63
|
+
subject: str,
|
|
64
|
+
mailbox: Optional[str] = None,
|
|
65
|
+
) -> list[MessageDetail]:
|
|
66
|
+
"""Get thread messages by subject."""
|
|
67
|
+
query: dict[str, Any] = {"subject": subject}
|
|
68
|
+
if mailbox is not None:
|
|
69
|
+
query["mailbox"] = mailbox
|
|
70
|
+
data = self._http.get("/inbox/thread", query)
|
|
71
|
+
return [MessageDetail.from_dict(d) for d in (data or [])]
|
|
72
|
+
|
|
73
|
+
def mark_as_read(
|
|
74
|
+
self,
|
|
75
|
+
uid: int,
|
|
76
|
+
*,
|
|
77
|
+
mailbox: Optional[str] = None,
|
|
78
|
+
folder: Optional[str] = None,
|
|
79
|
+
) -> None:
|
|
80
|
+
"""Mark a message as read."""
|
|
81
|
+
self._http.post(f"/inbox/{uid}/read", {
|
|
82
|
+
"mailbox": mailbox,
|
|
83
|
+
"folder": folder,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
def mark_as_unread(
|
|
87
|
+
self,
|
|
88
|
+
uid: int,
|
|
89
|
+
*,
|
|
90
|
+
mailbox: Optional[str] = None,
|
|
91
|
+
folder: Optional[str] = None,
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Mark a message as unread."""
|
|
94
|
+
self._http.delete(f"/inbox/{uid}/read", {
|
|
95
|
+
"mailbox": mailbox,
|
|
96
|
+
"folder": folder,
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
def move(
|
|
100
|
+
self,
|
|
101
|
+
uid: int,
|
|
102
|
+
to: str,
|
|
103
|
+
*,
|
|
104
|
+
from_folder: Optional[str] = None,
|
|
105
|
+
mailbox: Optional[str] = None,
|
|
106
|
+
) -> None:
|
|
107
|
+
"""Move a message to another folder."""
|
|
108
|
+
self._http.post(f"/inbox/{uid}/move", {
|
|
109
|
+
"to": to,
|
|
110
|
+
"from": from_folder,
|
|
111
|
+
"mailbox": mailbox,
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
def delete(
|
|
115
|
+
self,
|
|
116
|
+
uid: int,
|
|
117
|
+
*,
|
|
118
|
+
mailbox: Optional[str] = None,
|
|
119
|
+
folder: Optional[str] = None,
|
|
120
|
+
) -> None:
|
|
121
|
+
"""Delete a message."""
|
|
122
|
+
query: dict[str, Any] = {}
|
|
123
|
+
if mailbox is not None:
|
|
124
|
+
query["mailbox"] = mailbox
|
|
125
|
+
if folder is not None:
|
|
126
|
+
query["folder"] = folder
|
|
127
|
+
self._http.delete(f"/inbox/{uid}", query or None)
|
sentroy/mailboxes.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from sentroy._http import _HttpClient
|
|
4
|
+
from sentroy.types import MailboxUser
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MailboxesResource:
|
|
8
|
+
"""Interact with the Mailboxes API."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, http: _HttpClient) -> None:
|
|
11
|
+
self._http = http
|
|
12
|
+
|
|
13
|
+
def list(self) -> list[MailboxUser]:
|
|
14
|
+
"""List all mailbox accounts for the company."""
|
|
15
|
+
data = self._http.get("/mailboxes")
|
|
16
|
+
return [MailboxUser.from_dict(d) for d in (data or [])]
|
sentroy/send.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from sentroy._http import _HttpClient
|
|
4
|
+
from sentroy.types import SendParams, SendResult
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SendResource:
|
|
8
|
+
"""Interact with the Send API."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, http: _HttpClient) -> None:
|
|
11
|
+
self._http = http
|
|
12
|
+
|
|
13
|
+
def email(self, params: SendParams) -> SendResult:
|
|
14
|
+
"""Send an email."""
|
|
15
|
+
data = self._http.post("/send", params.to_dict())
|
|
16
|
+
return SendResult.from_dict(data)
|
sentroy/templates.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import urllib.parse
|
|
4
|
+
|
|
5
|
+
from sentroy._http import _HttpClient
|
|
6
|
+
from sentroy.types import Template
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TemplatesResource:
|
|
10
|
+
"""Interact with the Templates API."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, http: _HttpClient) -> None:
|
|
13
|
+
self._http = http
|
|
14
|
+
|
|
15
|
+
def list(self) -> list[Template]:
|
|
16
|
+
"""List all templates."""
|
|
17
|
+
data = self._http.get("/templates")
|
|
18
|
+
return [Template.from_dict(d) for d in (data or [])]
|
|
19
|
+
|
|
20
|
+
def get(self, id: str) -> Template:
|
|
21
|
+
"""Get a single template by ID."""
|
|
22
|
+
data = self._http.get(f"/templates/{urllib.parse.quote(id, safe='')}")
|
|
23
|
+
return Template.from_dict(data)
|
sentroy/types.py
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Optional, Union
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# -- Domains -----------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Domain:
|
|
11
|
+
id: str
|
|
12
|
+
domain: str
|
|
13
|
+
status: str
|
|
14
|
+
spf_verified: bool
|
|
15
|
+
dkim_verified: bool
|
|
16
|
+
dmarc_verified: bool
|
|
17
|
+
created_at: str
|
|
18
|
+
updated_at: str
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def from_dict(cls, d: dict[str, Any]) -> Domain:
|
|
22
|
+
return cls(
|
|
23
|
+
id=d["id"],
|
|
24
|
+
domain=d["domain"],
|
|
25
|
+
status=d["status"],
|
|
26
|
+
spf_verified=d.get("spfVerified", d.get("spf_verified", False)),
|
|
27
|
+
dkim_verified=d.get("dkimVerified", d.get("dkim_verified", False)),
|
|
28
|
+
dmarc_verified=d.get("dmarcVerified", d.get("dmarc_verified", False)),
|
|
29
|
+
created_at=d.get("createdAt", d.get("created_at", "")),
|
|
30
|
+
updated_at=d.get("updatedAt", d.get("updated_at", "")),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# -- Mailboxes ---------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class MailboxUser:
|
|
38
|
+
email: str
|
|
39
|
+
domain: str
|
|
40
|
+
username: str
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def from_dict(cls, d: dict[str, Any]) -> MailboxUser:
|
|
44
|
+
return cls(
|
|
45
|
+
email=d["email"],
|
|
46
|
+
domain=d["domain"],
|
|
47
|
+
username=d["username"],
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# -- Templates ----------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
LocalizedString = Union[str, dict[str, str]]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class Template:
|
|
58
|
+
id: str
|
|
59
|
+
name: LocalizedString
|
|
60
|
+
subject: LocalizedString
|
|
61
|
+
mjml_body: LocalizedString
|
|
62
|
+
html_body: Optional[LocalizedString] = None
|
|
63
|
+
variables: Optional[list[str]] = None
|
|
64
|
+
domain_id: Optional[str] = None
|
|
65
|
+
domain_name: Optional[str] = None
|
|
66
|
+
created_at: str = ""
|
|
67
|
+
updated_at: str = ""
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def from_dict(cls, d: dict[str, Any]) -> Template:
|
|
71
|
+
return cls(
|
|
72
|
+
id=d["id"],
|
|
73
|
+
name=d["name"],
|
|
74
|
+
subject=d["subject"],
|
|
75
|
+
mjml_body=d.get("mjmlBody", d.get("mjml_body", "")),
|
|
76
|
+
html_body=d.get("htmlBody", d.get("html_body")),
|
|
77
|
+
variables=d.get("variables"),
|
|
78
|
+
domain_id=d.get("domainId", d.get("domain_id")),
|
|
79
|
+
domain_name=d.get("domainName", d.get("domain_name")),
|
|
80
|
+
created_at=d.get("createdAt", d.get("created_at", "")),
|
|
81
|
+
updated_at=d.get("updatedAt", d.get("updated_at", "")),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# -- Inbox --------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class MessageAddress:
|
|
89
|
+
name: str
|
|
90
|
+
address: str
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def from_dict(cls, d: dict[str, Any]) -> MessageAddress:
|
|
94
|
+
return cls(name=d.get("name", ""), address=d.get("address", ""))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass
|
|
98
|
+
class MessageSummary:
|
|
99
|
+
uid: int
|
|
100
|
+
subject: str
|
|
101
|
+
from_addr: MessageAddress
|
|
102
|
+
to: list[MessageAddress]
|
|
103
|
+
date: str
|
|
104
|
+
seen: bool
|
|
105
|
+
flagged: bool
|
|
106
|
+
size: int
|
|
107
|
+
has_attachments: bool
|
|
108
|
+
preview: str
|
|
109
|
+
message_id: Optional[str]
|
|
110
|
+
in_reply_to: Optional[str]
|
|
111
|
+
category: str
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
def from_dict(cls, d: dict[str, Any]) -> MessageSummary:
|
|
115
|
+
from_raw = d.get("from", d.get("from_addr", {}))
|
|
116
|
+
from_addr = MessageAddress.from_dict(from_raw) if isinstance(from_raw, dict) else MessageAddress(name="", address="")
|
|
117
|
+
return cls(
|
|
118
|
+
uid=d["uid"],
|
|
119
|
+
subject=d.get("subject", ""),
|
|
120
|
+
from_addr=from_addr,
|
|
121
|
+
to=[MessageAddress.from_dict(a) for a in d.get("to", [])],
|
|
122
|
+
date=d.get("date", ""),
|
|
123
|
+
seen=d.get("seen", False),
|
|
124
|
+
flagged=d.get("flagged", False),
|
|
125
|
+
size=d.get("size", 0),
|
|
126
|
+
has_attachments=d.get("hasAttachments", d.get("has_attachments", False)),
|
|
127
|
+
preview=d.get("preview", ""),
|
|
128
|
+
message_id=d.get("messageId", d.get("message_id")),
|
|
129
|
+
in_reply_to=d.get("inReplyTo", d.get("in_reply_to")),
|
|
130
|
+
category=d.get("category", ""),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass
|
|
135
|
+
class AttachmentInfo:
|
|
136
|
+
part_id: str
|
|
137
|
+
filename: str
|
|
138
|
+
size: int
|
|
139
|
+
content_type: str
|
|
140
|
+
content_id: Optional[str] = None
|
|
141
|
+
|
|
142
|
+
@classmethod
|
|
143
|
+
def from_dict(cls, d: dict[str, Any]) -> AttachmentInfo:
|
|
144
|
+
return cls(
|
|
145
|
+
part_id=d.get("partId", d.get("part_id", "")),
|
|
146
|
+
filename=d.get("filename", ""),
|
|
147
|
+
size=d.get("size", 0),
|
|
148
|
+
content_type=d.get("contentType", d.get("content_type", "")),
|
|
149
|
+
content_id=d.get("contentId", d.get("content_id")),
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclass
|
|
154
|
+
class MessageDetail:
|
|
155
|
+
uid: int
|
|
156
|
+
subject: str
|
|
157
|
+
from_addr: MessageAddress
|
|
158
|
+
to: list[MessageAddress]
|
|
159
|
+
cc: list[MessageAddress]
|
|
160
|
+
reply_to: Optional[MessageAddress]
|
|
161
|
+
date: str
|
|
162
|
+
seen: bool
|
|
163
|
+
flagged: bool
|
|
164
|
+
text_body: Optional[str]
|
|
165
|
+
html_body: Optional[str]
|
|
166
|
+
attachments: list[AttachmentInfo]
|
|
167
|
+
headers: dict[str, str]
|
|
168
|
+
message_id: Optional[str]
|
|
169
|
+
in_reply_to: Optional[str]
|
|
170
|
+
references: list[str]
|
|
171
|
+
folder: Optional[str] = None
|
|
172
|
+
|
|
173
|
+
@classmethod
|
|
174
|
+
def from_dict(cls, d: dict[str, Any]) -> MessageDetail:
|
|
175
|
+
from_raw = d.get("from", d.get("from_addr", {}))
|
|
176
|
+
from_addr = MessageAddress.from_dict(from_raw) if isinstance(from_raw, dict) else MessageAddress(name="", address="")
|
|
177
|
+
reply_to_raw = d.get("replyTo", d.get("reply_to"))
|
|
178
|
+
reply_to = MessageAddress.from_dict(reply_to_raw) if isinstance(reply_to_raw, dict) else None
|
|
179
|
+
return cls(
|
|
180
|
+
uid=d["uid"],
|
|
181
|
+
subject=d.get("subject", ""),
|
|
182
|
+
from_addr=from_addr,
|
|
183
|
+
to=[MessageAddress.from_dict(a) for a in d.get("to", [])],
|
|
184
|
+
cc=[MessageAddress.from_dict(a) for a in d.get("cc", [])],
|
|
185
|
+
reply_to=reply_to,
|
|
186
|
+
date=d.get("date", ""),
|
|
187
|
+
seen=d.get("seen", False),
|
|
188
|
+
flagged=d.get("flagged", False),
|
|
189
|
+
text_body=d.get("textBody", d.get("text_body")),
|
|
190
|
+
html_body=d.get("htmlBody", d.get("html_body")),
|
|
191
|
+
attachments=[AttachmentInfo.from_dict(a) for a in d.get("attachments", [])],
|
|
192
|
+
headers=d.get("headers", {}),
|
|
193
|
+
message_id=d.get("messageId", d.get("message_id")),
|
|
194
|
+
in_reply_to=d.get("inReplyTo", d.get("in_reply_to")),
|
|
195
|
+
references=d.get("references", []),
|
|
196
|
+
folder=d.get("folder"),
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@dataclass
|
|
201
|
+
class Mailbox:
|
|
202
|
+
name: str
|
|
203
|
+
path: str
|
|
204
|
+
special_use: Optional[str]
|
|
205
|
+
total_messages: int
|
|
206
|
+
unread_messages: int
|
|
207
|
+
|
|
208
|
+
@classmethod
|
|
209
|
+
def from_dict(cls, d: dict[str, Any]) -> Mailbox:
|
|
210
|
+
return cls(
|
|
211
|
+
name=d.get("name", ""),
|
|
212
|
+
path=d.get("path", ""),
|
|
213
|
+
special_use=d.get("specialUse", d.get("special_use")),
|
|
214
|
+
total_messages=d.get("totalMessages", d.get("total_messages", 0)),
|
|
215
|
+
unread_messages=d.get("unreadMessages", d.get("unread_messages", 0)),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@dataclass
|
|
220
|
+
class InboxListParams:
|
|
221
|
+
mailbox: Optional[str] = None
|
|
222
|
+
folder: Optional[str] = None
|
|
223
|
+
page: Optional[int] = None
|
|
224
|
+
limit: Optional[int] = None
|
|
225
|
+
unread_only: Optional[bool] = None
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# -- Send ---------------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
@dataclass
|
|
231
|
+
class Attachment:
|
|
232
|
+
filename: str
|
|
233
|
+
content: str
|
|
234
|
+
content_type: Optional[str] = None
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@dataclass
|
|
238
|
+
class SendParams:
|
|
239
|
+
to: Union[str, list[str]]
|
|
240
|
+
from_addr: str
|
|
241
|
+
subject: str
|
|
242
|
+
domain_id: str
|
|
243
|
+
cc: Optional[Union[str, list[str]]] = None
|
|
244
|
+
template_id: Optional[str] = None
|
|
245
|
+
lang: Optional[str] = None # Template language code (e.g. "en", "tr")
|
|
246
|
+
html: Optional[str] = None
|
|
247
|
+
text: Optional[str] = None
|
|
248
|
+
variables: Optional[dict[str, str]] = None
|
|
249
|
+
reply_to: Optional[str] = None
|
|
250
|
+
attachments: Optional[list[Attachment]] = None
|
|
251
|
+
scheduled_at: Optional[str] = None
|
|
252
|
+
headers: Optional[dict[str, str]] = None
|
|
253
|
+
in_reply_to: Optional[str] = None
|
|
254
|
+
references: Optional[list[str]] = None
|
|
255
|
+
|
|
256
|
+
def to_dict(self) -> dict[str, Any]:
|
|
257
|
+
d: dict[str, Any] = {
|
|
258
|
+
"to": self.to,
|
|
259
|
+
"from": self.from_addr,
|
|
260
|
+
"subject": self.subject,
|
|
261
|
+
"domainId": self.domain_id,
|
|
262
|
+
}
|
|
263
|
+
if self.cc is not None:
|
|
264
|
+
d["cc"] = self.cc
|
|
265
|
+
if self.template_id is not None:
|
|
266
|
+
d["templateId"] = self.template_id
|
|
267
|
+
if self.lang is not None:
|
|
268
|
+
d["lang"] = self.lang
|
|
269
|
+
if self.html is not None:
|
|
270
|
+
d["html"] = self.html
|
|
271
|
+
if self.text is not None:
|
|
272
|
+
d["text"] = self.text
|
|
273
|
+
if self.variables is not None:
|
|
274
|
+
d["variables"] = self.variables
|
|
275
|
+
if self.reply_to is not None:
|
|
276
|
+
d["replyTo"] = self.reply_to
|
|
277
|
+
if self.attachments is not None:
|
|
278
|
+
d["attachments"] = [
|
|
279
|
+
{
|
|
280
|
+
"filename": a.filename,
|
|
281
|
+
"content": a.content,
|
|
282
|
+
**({"contentType": a.content_type} if a.content_type is not None else {}),
|
|
283
|
+
}
|
|
284
|
+
for a in self.attachments
|
|
285
|
+
]
|
|
286
|
+
if self.scheduled_at is not None:
|
|
287
|
+
d["scheduledAt"] = self.scheduled_at
|
|
288
|
+
if self.headers is not None:
|
|
289
|
+
d["headers"] = self.headers
|
|
290
|
+
if self.in_reply_to is not None:
|
|
291
|
+
d["inReplyTo"] = self.in_reply_to
|
|
292
|
+
if self.references is not None:
|
|
293
|
+
d["references"] = self.references
|
|
294
|
+
return d
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
@dataclass
|
|
298
|
+
class SendResult:
|
|
299
|
+
job_id: str
|
|
300
|
+
mail_log_id: str
|
|
301
|
+
status: str
|
|
302
|
+
scheduled_at: Optional[str] = None
|
|
303
|
+
|
|
304
|
+
@classmethod
|
|
305
|
+
def from_dict(cls, d: dict[str, Any]) -> SendResult:
|
|
306
|
+
return cls(
|
|
307
|
+
job_id=d.get("jobId", d.get("job_id", "")),
|
|
308
|
+
mail_log_id=d.get("mailLogId", d.get("mail_log_id", "")),
|
|
309
|
+
status=d.get("status", ""),
|
|
310
|
+
scheduled_at=d.get("scheduledAt", d.get("scheduled_at")),
|
|
311
|
+
)
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sentroy-client-sdk
|
|
3
|
+
Version: 1.1.5
|
|
4
|
+
Summary: Server-side Python SDK for Sentroy platform API
|
|
5
|
+
Project-URL: Homepage, https://github.com/Sentroy-Co/client-sdk
|
|
6
|
+
Project-URL: Repository, https://github.com/Sentroy-Co/client-sdk
|
|
7
|
+
Project-URL: Issues, https://github.com/Sentroy-Co/client-sdk/issues
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Keywords: api,email,mail,sdk,sentroy
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
<p align="center">
|
|
23
|
+
<img src="https://sentroy.com/business/sentroy-logo-light.png" alt="Sentroy" width="240" />
|
|
24
|
+
</p>
|
|
25
|
+
|
|
26
|
+
<h3 align="center">Sentroy Client SDK for Python</h3>
|
|
27
|
+
|
|
28
|
+
<p align="center">
|
|
29
|
+
Server-side SDK to interact with the Sentroy platform API.<br />
|
|
30
|
+
List domains, manage mailboxes, fetch templates, read inbox, and send emails.
|
|
31
|
+
</p>
|
|
32
|
+
|
|
33
|
+
<p align="center">
|
|
34
|
+
<a href="https://pypi.org/project/sentroy-client-sdk/"><img src="https://img.shields.io/pypi/v/sentroy-client-sdk.svg" alt="PyPI version" /></a>
|
|
35
|
+
<a href="https://github.com/Sentroy-Co/client-sdk/blob/main/LICENSE"><img src="https://img.shields.io/pypi/l/sentroy-client-sdk.svg" alt="license" /></a>
|
|
36
|
+
</p>
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install sentroy-client-sdk
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Quick Start
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from sentroy import Sentroy
|
|
50
|
+
|
|
51
|
+
sentroy = Sentroy(
|
|
52
|
+
base_url="https://sentroy.com",
|
|
53
|
+
company_slug="my-company",
|
|
54
|
+
access_token="stk_...",
|
|
55
|
+
)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
> Access tokens can be created from **Admin > Access Tokens** in the Sentroy dashboard.
|
|
59
|
+
|
|
60
|
+
## Usage
|
|
61
|
+
|
|
62
|
+
### Domains
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
# List all domains
|
|
66
|
+
domains = sentroy.domains.list()
|
|
67
|
+
|
|
68
|
+
# Get a single domain
|
|
69
|
+
domain = sentroy.domains.get("domain-id")
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Mailboxes
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
# List all mailbox accounts
|
|
76
|
+
mailboxes = sentroy.mailboxes.list()
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Templates
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
# List all templates
|
|
83
|
+
templates = sentroy.templates.list()
|
|
84
|
+
|
|
85
|
+
# Get a template by ID
|
|
86
|
+
template = sentroy.templates.get("template-id")
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Templates support multiple languages. A field like `name` or `subject` can be a plain string or a dict keyed by language code:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
# Example template response
|
|
93
|
+
{
|
|
94
|
+
"id": "b3f1a2c4-...",
|
|
95
|
+
"name": {"en": "Welcome Email", "tr": "Hosgeldin E-postasi"},
|
|
96
|
+
"subject": {"en": "Welcome, {{name}}!", "tr": "Hosgeldin, {{name}}!"},
|
|
97
|
+
"mjmlBody": {"en": "<mjml>...</mjml>", "tr": "<mjml>...</mjml>"},
|
|
98
|
+
"variables": ["name", "company"],
|
|
99
|
+
"domainId": "a1b2c3d4-...",
|
|
100
|
+
"domainName": "example.com"
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Use the `variables` list to know which placeholders (`{{name}}`, `{{company}}`) the template expects.
|
|
105
|
+
|
|
106
|
+
### Inbox
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from sentroy import InboxListParams
|
|
110
|
+
|
|
111
|
+
# List messages
|
|
112
|
+
messages = sentroy.inbox.list(InboxListParams(
|
|
113
|
+
mailbox="info@example.com",
|
|
114
|
+
folder="INBOX",
|
|
115
|
+
page=1,
|
|
116
|
+
limit=20,
|
|
117
|
+
))
|
|
118
|
+
|
|
119
|
+
# Get a single message
|
|
120
|
+
message = sentroy.inbox.get(1234, mailbox="info@example.com")
|
|
121
|
+
|
|
122
|
+
# List IMAP folders
|
|
123
|
+
folders = sentroy.inbox.list_folders("info@example.com")
|
|
124
|
+
|
|
125
|
+
# Get a thread by subject
|
|
126
|
+
thread = sentroy.inbox.get_thread("Re: Project update", "info@example.com")
|
|
127
|
+
|
|
128
|
+
# Mark as read / unread
|
|
129
|
+
sentroy.inbox.mark_as_read(1234, mailbox="info@example.com")
|
|
130
|
+
sentroy.inbox.mark_as_unread(1234, mailbox="info@example.com")
|
|
131
|
+
|
|
132
|
+
# Move message
|
|
133
|
+
sentroy.inbox.move(1234, "Trash", from_folder="INBOX", mailbox="info@example.com")
|
|
134
|
+
|
|
135
|
+
# Delete message
|
|
136
|
+
sentroy.inbox.delete(1234, mailbox="info@example.com")
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Send Email
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from sentroy import SendParams, Attachment
|
|
143
|
+
|
|
144
|
+
# Send with a template
|
|
145
|
+
result = sentroy.send.email(SendParams(
|
|
146
|
+
to="user@example.com",
|
|
147
|
+
from_addr="info@example.com",
|
|
148
|
+
subject="Welcome!",
|
|
149
|
+
domain_id="domain-id",
|
|
150
|
+
template_id="template-id",
|
|
151
|
+
variables={
|
|
152
|
+
"name": "John",
|
|
153
|
+
"company": "Acme",
|
|
154
|
+
},
|
|
155
|
+
))
|
|
156
|
+
|
|
157
|
+
# Send with a specific language
|
|
158
|
+
result = sentroy.send.email(SendParams(
|
|
159
|
+
to="user@example.com",
|
|
160
|
+
from_addr="info@example.com",
|
|
161
|
+
subject="Hosgeldin!",
|
|
162
|
+
domain_id="domain-id",
|
|
163
|
+
template_id="template-id",
|
|
164
|
+
lang="tr",
|
|
165
|
+
variables={"name": "Ahmet"},
|
|
166
|
+
))
|
|
167
|
+
|
|
168
|
+
# Send with raw HTML
|
|
169
|
+
result = sentroy.send.email(SendParams(
|
|
170
|
+
to=["user1@example.com", "user2@example.com"],
|
|
171
|
+
from_addr="info@example.com",
|
|
172
|
+
subject="Hello",
|
|
173
|
+
domain_id="domain-id",
|
|
174
|
+
html="<h1>Hello World</h1>",
|
|
175
|
+
))
|
|
176
|
+
|
|
177
|
+
# Send with attachments
|
|
178
|
+
result = sentroy.send.email(SendParams(
|
|
179
|
+
to="user@example.com",
|
|
180
|
+
from_addr="info@example.com",
|
|
181
|
+
subject="Invoice",
|
|
182
|
+
domain_id="domain-id",
|
|
183
|
+
html="<p>Please find your invoice attached.</p>",
|
|
184
|
+
attachments=[
|
|
185
|
+
Attachment(
|
|
186
|
+
filename="invoice.pdf",
|
|
187
|
+
content=base64_string,
|
|
188
|
+
content_type="application/pdf",
|
|
189
|
+
),
|
|
190
|
+
],
|
|
191
|
+
))
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Error Handling
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
from sentroy import Sentroy, SentroyError
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
sentroy.send.email(params)
|
|
201
|
+
except SentroyError as err:
|
|
202
|
+
print(err.status_code) # 401, 403, 500, etc.
|
|
203
|
+
print(err) # Human-readable error
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Configuration
|
|
207
|
+
|
|
208
|
+
| Option | Type | Required | Description |
|
|
209
|
+
|--------|------|----------|-------------|
|
|
210
|
+
| `base_url` | `str` | Yes | Sentroy instance URL (e.g. `https://sentroy.com`) |
|
|
211
|
+
| `company_slug` | `str` | Yes | Your company slug |
|
|
212
|
+
| `access_token` | `str` | Yes | Access token (`stk_...`) |
|
|
213
|
+
| `timeout` | `int` | No | Request timeout in seconds (default: `30`) |
|
|
214
|
+
|
|
215
|
+
## Requirements
|
|
216
|
+
|
|
217
|
+
- Python 3.10+
|
|
218
|
+
- Zero external dependencies (stdlib only)
|
|
219
|
+
|
|
220
|
+
## Raw Documentation
|
|
221
|
+
|
|
222
|
+
For AI agents and LLMs — plain-text version of this document:
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
https://raw.githubusercontent.com/Sentroy-Co/client-sdk/refs/heads/main/python/README.md
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## License
|
|
229
|
+
|
|
230
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
sentroy/__init__.py,sha256=ZGLGrc3C63h3vOSmIExtgXMrqXBClTNsmdTLdBMy1qo,1875
|
|
2
|
+
sentroy/_http.py,sha256=y9gHm7-hlFAFvHoqTtf6duaVN-bMkpDNIfA3L85o7SA,3019
|
|
3
|
+
sentroy/domains.py,sha256=EYWbRm-oGYeDzNgxSprlckj2YDosDBXAxxBK5Rd8Evk,662
|
|
4
|
+
sentroy/inbox.py,sha256=yyYO43Z6o2SXBhy1Gc_NlAbjkWUte6PMN6BrPyH_4As,3808
|
|
5
|
+
sentroy/mailboxes.py,sha256=kBYvctvvi6eRz3OViVUgbBMmkEYT8E5_zUJGR81s3d0,467
|
|
6
|
+
sentroy/send.py,sha256=4oBJ053mF-_Le2Mr7Bkm7Zkqr0gdeuM2C9E3VfSykMA,446
|
|
7
|
+
sentroy/templates.py,sha256=bhZpoWEByh3TeJrmd2Iy0FKqGLQjaOr6zxCpfspQfWM,659
|
|
8
|
+
sentroy/types.py,sha256=GfHpZyu4lvs621ss5VQdYrPoO2i46yvtSQCOd7UsG7g,9715
|
|
9
|
+
sentroy_client_sdk-1.1.5.dist-info/METADATA,sha256=FP4L9Xr7tWC3LbXG_4ttOevxIiSCiS1N7oRAWuyOIqo,5853
|
|
10
|
+
sentroy_client_sdk-1.1.5.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
11
|
+
sentroy_client_sdk-1.1.5.dist-info/RECORD,,
|