sentroy-client-sdk 1.1.5__tar.gz

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.
@@ -0,0 +1,4 @@
1
+ node_modules/
2
+ dist/
3
+ *.tgz
4
+ version.sh
@@ -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,209 @@
1
+ <p align="center">
2
+ <img src="https://sentroy.com/business/sentroy-logo-light.png" alt="Sentroy" width="240" />
3
+ </p>
4
+
5
+ <h3 align="center">Sentroy Client SDK for Python</h3>
6
+
7
+ <p align="center">
8
+ Server-side SDK to interact with the Sentroy platform API.<br />
9
+ List domains, manage mailboxes, fetch templates, read inbox, and send emails.
10
+ </p>
11
+
12
+ <p align="center">
13
+ <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>
14
+ <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>
15
+ </p>
16
+
17
+ ---
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ pip install sentroy-client-sdk
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ ```python
28
+ from sentroy import Sentroy
29
+
30
+ sentroy = Sentroy(
31
+ base_url="https://sentroy.com",
32
+ company_slug="my-company",
33
+ access_token="stk_...",
34
+ )
35
+ ```
36
+
37
+ > Access tokens can be created from **Admin > Access Tokens** in the Sentroy dashboard.
38
+
39
+ ## Usage
40
+
41
+ ### Domains
42
+
43
+ ```python
44
+ # List all domains
45
+ domains = sentroy.domains.list()
46
+
47
+ # Get a single domain
48
+ domain = sentroy.domains.get("domain-id")
49
+ ```
50
+
51
+ ### Mailboxes
52
+
53
+ ```python
54
+ # List all mailbox accounts
55
+ mailboxes = sentroy.mailboxes.list()
56
+ ```
57
+
58
+ ### Templates
59
+
60
+ ```python
61
+ # List all templates
62
+ templates = sentroy.templates.list()
63
+
64
+ # Get a template by ID
65
+ template = sentroy.templates.get("template-id")
66
+ ```
67
+
68
+ Templates support multiple languages. A field like `name` or `subject` can be a plain string or a dict keyed by language code:
69
+
70
+ ```python
71
+ # Example template response
72
+ {
73
+ "id": "b3f1a2c4-...",
74
+ "name": {"en": "Welcome Email", "tr": "Hosgeldin E-postasi"},
75
+ "subject": {"en": "Welcome, {{name}}!", "tr": "Hosgeldin, {{name}}!"},
76
+ "mjmlBody": {"en": "<mjml>...</mjml>", "tr": "<mjml>...</mjml>"},
77
+ "variables": ["name", "company"],
78
+ "domainId": "a1b2c3d4-...",
79
+ "domainName": "example.com"
80
+ }
81
+ ```
82
+
83
+ Use the `variables` list to know which placeholders (`{{name}}`, `{{company}}`) the template expects.
84
+
85
+ ### Inbox
86
+
87
+ ```python
88
+ from sentroy import InboxListParams
89
+
90
+ # List messages
91
+ messages = sentroy.inbox.list(InboxListParams(
92
+ mailbox="info@example.com",
93
+ folder="INBOX",
94
+ page=1,
95
+ limit=20,
96
+ ))
97
+
98
+ # Get a single message
99
+ message = sentroy.inbox.get(1234, mailbox="info@example.com")
100
+
101
+ # List IMAP folders
102
+ folders = sentroy.inbox.list_folders("info@example.com")
103
+
104
+ # Get a thread by subject
105
+ thread = sentroy.inbox.get_thread("Re: Project update", "info@example.com")
106
+
107
+ # Mark as read / unread
108
+ sentroy.inbox.mark_as_read(1234, mailbox="info@example.com")
109
+ sentroy.inbox.mark_as_unread(1234, mailbox="info@example.com")
110
+
111
+ # Move message
112
+ sentroy.inbox.move(1234, "Trash", from_folder="INBOX", mailbox="info@example.com")
113
+
114
+ # Delete message
115
+ sentroy.inbox.delete(1234, mailbox="info@example.com")
116
+ ```
117
+
118
+ ### Send Email
119
+
120
+ ```python
121
+ from sentroy import SendParams, Attachment
122
+
123
+ # Send with a template
124
+ result = sentroy.send.email(SendParams(
125
+ to="user@example.com",
126
+ from_addr="info@example.com",
127
+ subject="Welcome!",
128
+ domain_id="domain-id",
129
+ template_id="template-id",
130
+ variables={
131
+ "name": "John",
132
+ "company": "Acme",
133
+ },
134
+ ))
135
+
136
+ # Send with a specific language
137
+ result = sentroy.send.email(SendParams(
138
+ to="user@example.com",
139
+ from_addr="info@example.com",
140
+ subject="Hosgeldin!",
141
+ domain_id="domain-id",
142
+ template_id="template-id",
143
+ lang="tr",
144
+ variables={"name": "Ahmet"},
145
+ ))
146
+
147
+ # Send with raw HTML
148
+ result = sentroy.send.email(SendParams(
149
+ to=["user1@example.com", "user2@example.com"],
150
+ from_addr="info@example.com",
151
+ subject="Hello",
152
+ domain_id="domain-id",
153
+ html="<h1>Hello World</h1>",
154
+ ))
155
+
156
+ # Send with attachments
157
+ result = sentroy.send.email(SendParams(
158
+ to="user@example.com",
159
+ from_addr="info@example.com",
160
+ subject="Invoice",
161
+ domain_id="domain-id",
162
+ html="<p>Please find your invoice attached.</p>",
163
+ attachments=[
164
+ Attachment(
165
+ filename="invoice.pdf",
166
+ content=base64_string,
167
+ content_type="application/pdf",
168
+ ),
169
+ ],
170
+ ))
171
+ ```
172
+
173
+ ## Error Handling
174
+
175
+ ```python
176
+ from sentroy import Sentroy, SentroyError
177
+
178
+ try:
179
+ sentroy.send.email(params)
180
+ except SentroyError as err:
181
+ print(err.status_code) # 401, 403, 500, etc.
182
+ print(err) # Human-readable error
183
+ ```
184
+
185
+ ## Configuration
186
+
187
+ | Option | Type | Required | Description |
188
+ |--------|------|----------|-------------|
189
+ | `base_url` | `str` | Yes | Sentroy instance URL (e.g. `https://sentroy.com`) |
190
+ | `company_slug` | `str` | Yes | Your company slug |
191
+ | `access_token` | `str` | Yes | Access token (`stk_...`) |
192
+ | `timeout` | `int` | No | Request timeout in seconds (default: `30`) |
193
+
194
+ ## Requirements
195
+
196
+ - Python 3.10+
197
+ - Zero external dependencies (stdlib only)
198
+
199
+ ## Raw Documentation
200
+
201
+ For AI agents and LLMs — plain-text version of this document:
202
+
203
+ ```
204
+ https://raw.githubusercontent.com/Sentroy-Co/client-sdk/refs/heads/main/python/README.md
205
+ ```
206
+
207
+ ## License
208
+
209
+ [MIT](LICENSE)
@@ -0,0 +1,31 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "sentroy-client-sdk"
7
+ version = "1.1.5"
8
+ description = "Server-side Python SDK for Sentroy platform API"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ keywords = ["sentroy", "email", "sdk", "api", "mail"]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Typing :: Typed",
23
+ ]
24
+
25
+ [tool.hatch.build.targets.wheel]
26
+ packages = ["sentroy"]
27
+
28
+ [project.urls]
29
+ Homepage = "https://github.com/Sentroy-Co/client-sdk"
30
+ Repository = "https://github.com/Sentroy-Co/client-sdk"
31
+ Issues = "https://github.com/Sentroy-Co/client-sdk/issues"
@@ -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
+ ]
@@ -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)
@@ -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)
@@ -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)
@@ -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 [])]
@@ -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)
@@ -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)
@@ -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
+ )