replylayer 0.14.0__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.
- replylayer/__init__.py +143 -0
- replylayer/_client.py +128 -0
- replylayer/_http.py +326 -0
- replylayer/_pagination.py +34 -0
- replylayer/errors.py +152 -0
- replylayer/py.typed +0 -0
- replylayer/resources/__init__.py +0 -0
- replylayer/resources/account.py +36 -0
- replylayer/resources/api_keys.py +59 -0
- replylayer/resources/attachments.py +115 -0
- replylayer/resources/domains.py +79 -0
- replylayer/resources/drafts.py +342 -0
- replylayer/resources/health.py +21 -0
- replylayer/resources/inbound_blocklist.py +93 -0
- replylayer/resources/legal_holds.py +107 -0
- replylayer/resources/mailboxes.py +768 -0
- replylayer/resources/messages.py +425 -0
- replylayer/resources/recipients.py +59 -0
- replylayer/resources/suppressions.py +84 -0
- replylayer/resources/threads.py +117 -0
- replylayer/resources/webhooks.py +175 -0
- replylayer/types.py +1578 -0
- replylayer-0.14.0.dist-info/METADATA +502 -0
- replylayer-0.14.0.dist-info/RECORD +25 -0
- replylayer-0.14.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,768 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, overload
|
|
4
|
+
from urllib.parse import quote
|
|
5
|
+
|
|
6
|
+
from .._http import AsyncHttpClient, SyncHttpClient
|
|
7
|
+
from ..types import (
|
|
8
|
+
AttachmentAccessResponse,
|
|
9
|
+
AttachmentAllowedFileFamilyRequest,
|
|
10
|
+
AttachmentExposureMode,
|
|
11
|
+
HitlMode,
|
|
12
|
+
PiiMode,
|
|
13
|
+
PiiRedactionConfig,
|
|
14
|
+
RecipientPolicyMode,
|
|
15
|
+
ScannerPolicy,
|
|
16
|
+
SenderPolicyMode,
|
|
17
|
+
SubaddressMode,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _build_attachment_access_body(
|
|
22
|
+
*,
|
|
23
|
+
enable: bool | None = None,
|
|
24
|
+
mode: AttachmentExposureMode | None = None,
|
|
25
|
+
allowed_file_families: list[AttachmentAllowedFileFamilyRequest] | None = None,
|
|
26
|
+
accept_disclaimer_version: str | None = None,
|
|
27
|
+
accept_image_risk_version: str | None = None,
|
|
28
|
+
reauth_token: str | None = None,
|
|
29
|
+
) -> dict[str, Any]:
|
|
30
|
+
body: dict[str, Any] = {}
|
|
31
|
+
if enable is not None:
|
|
32
|
+
body["enable"] = enable
|
|
33
|
+
if mode is not None:
|
|
34
|
+
body["mode"] = mode
|
|
35
|
+
if allowed_file_families is not None:
|
|
36
|
+
body["allowed_file_families"] = allowed_file_families
|
|
37
|
+
if accept_disclaimer_version is not None:
|
|
38
|
+
body["accept_disclaimer_version"] = accept_disclaimer_version
|
|
39
|
+
if accept_image_risk_version is not None:
|
|
40
|
+
body["accept_image_risk_version"] = accept_image_risk_version
|
|
41
|
+
if reauth_token is not None:
|
|
42
|
+
body["reauth_token"] = reauth_token
|
|
43
|
+
return body
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SyncMailboxAllowlist:
|
|
47
|
+
"""Migration 036 — mailbox-scoped recipient allowlist (sync).
|
|
48
|
+
|
|
49
|
+
Reached via `client.mailboxes.allowlist`. Mutations are admin-only; an
|
|
50
|
+
`agent`-role key that calls `add`/`add_bulk`/`delete` gets 403
|
|
51
|
+
`INSUFFICIENT_SCOPE`. See ``docs/allowlist.md`` for the rationale.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, http: SyncHttpClient) -> None:
|
|
55
|
+
self._http = http
|
|
56
|
+
|
|
57
|
+
def list(
|
|
58
|
+
self,
|
|
59
|
+
mailbox_id: str,
|
|
60
|
+
*,
|
|
61
|
+
limit: int | None = None,
|
|
62
|
+
cursor: str | None = None,
|
|
63
|
+
all: bool | None = None,
|
|
64
|
+
) -> dict[str, Any]:
|
|
65
|
+
query: dict[str, Any] = {}
|
|
66
|
+
if limit is not None:
|
|
67
|
+
query["limit"] = str(limit)
|
|
68
|
+
if cursor is not None:
|
|
69
|
+
query["cursor"] = cursor
|
|
70
|
+
if all:
|
|
71
|
+
query["all"] = "true"
|
|
72
|
+
return self._http.request(
|
|
73
|
+
"GET", f"/v1/mailboxes/{mailbox_id}/allowlist", query=query
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def add(self, mailbox_id: str, *, email: str) -> dict[str, Any]:
|
|
77
|
+
return self._http.request(
|
|
78
|
+
"POST", f"/v1/mailboxes/{mailbox_id}/allowlist", body={"email": email}
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def add_bulk(self, mailbox_id: str, *, emails: list[str]) -> dict[str, Any]:
|
|
82
|
+
return self._http.request(
|
|
83
|
+
"POST",
|
|
84
|
+
f"/v1/mailboxes/{mailbox_id}/allowlist/bulk",
|
|
85
|
+
body={"emails": emails},
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def delete(
|
|
89
|
+
self,
|
|
90
|
+
mailbox_id: str,
|
|
91
|
+
email: str,
|
|
92
|
+
*,
|
|
93
|
+
force_empty: bool = False,
|
|
94
|
+
) -> dict[str, Any]:
|
|
95
|
+
query: dict[str, Any] = {}
|
|
96
|
+
if force_empty:
|
|
97
|
+
query["force_empty"] = "true"
|
|
98
|
+
return self._http.request(
|
|
99
|
+
"DELETE",
|
|
100
|
+
f"/v1/mailboxes/{mailbox_id}/allowlist/{quote(email, safe='')}",
|
|
101
|
+
query=query,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def list_blocked_attempts(
|
|
105
|
+
self,
|
|
106
|
+
mailbox_id: str,
|
|
107
|
+
*,
|
|
108
|
+
limit: int | None = None,
|
|
109
|
+
cursor: str | None = None,
|
|
110
|
+
all: bool | None = None,
|
|
111
|
+
aggregate: bool | None = None,
|
|
112
|
+
within_days: int | None = None,
|
|
113
|
+
) -> dict[str, Any]:
|
|
114
|
+
"""Migration 038 — blocked attempts log.
|
|
115
|
+
|
|
116
|
+
Default ``aggregate=True`` returns a top-N grouped view without
|
|
117
|
+
pagination (``next_cursor`` is always ``None``). Pass
|
|
118
|
+
``aggregate=False`` for raw per-attempt history with tuple-cursor
|
|
119
|
+
pagination. Pass ``within_days=7`` for a "blocked this week" filter
|
|
120
|
+
(1..365).
|
|
121
|
+
"""
|
|
122
|
+
query: dict[str, Any] = {}
|
|
123
|
+
if limit is not None:
|
|
124
|
+
query["limit"] = str(limit)
|
|
125
|
+
if cursor is not None:
|
|
126
|
+
query["cursor"] = cursor
|
|
127
|
+
if all:
|
|
128
|
+
query["all"] = "true"
|
|
129
|
+
if aggregate is False:
|
|
130
|
+
query["aggregate"] = "false"
|
|
131
|
+
if within_days is not None:
|
|
132
|
+
query["within_days"] = str(within_days)
|
|
133
|
+
return self._http.request(
|
|
134
|
+
"GET",
|
|
135
|
+
f"/v1/mailboxes/{mailbox_id}/allowlist/blocked-attempts",
|
|
136
|
+
query=query,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class AsyncMailboxAllowlist:
|
|
141
|
+
"""Migration 036 — mailbox-scoped recipient allowlist (async)."""
|
|
142
|
+
|
|
143
|
+
def __init__(self, http: AsyncHttpClient) -> None:
|
|
144
|
+
self._http = http
|
|
145
|
+
|
|
146
|
+
async def list(
|
|
147
|
+
self,
|
|
148
|
+
mailbox_id: str,
|
|
149
|
+
*,
|
|
150
|
+
limit: int | None = None,
|
|
151
|
+
cursor: str | None = None,
|
|
152
|
+
all: bool | None = None,
|
|
153
|
+
) -> dict[str, Any]:
|
|
154
|
+
query: dict[str, Any] = {}
|
|
155
|
+
if limit is not None:
|
|
156
|
+
query["limit"] = str(limit)
|
|
157
|
+
if cursor is not None:
|
|
158
|
+
query["cursor"] = cursor
|
|
159
|
+
if all:
|
|
160
|
+
query["all"] = "true"
|
|
161
|
+
return await self._http.request(
|
|
162
|
+
"GET", f"/v1/mailboxes/{mailbox_id}/allowlist", query=query
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
async def add(self, mailbox_id: str, *, email: str) -> dict[str, Any]:
|
|
166
|
+
return await self._http.request(
|
|
167
|
+
"POST", f"/v1/mailboxes/{mailbox_id}/allowlist", body={"email": email}
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
async def add_bulk(
|
|
171
|
+
self, mailbox_id: str, *, emails: list[str]
|
|
172
|
+
) -> dict[str, Any]:
|
|
173
|
+
return await self._http.request(
|
|
174
|
+
"POST",
|
|
175
|
+
f"/v1/mailboxes/{mailbox_id}/allowlist/bulk",
|
|
176
|
+
body={"emails": emails},
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
async def delete(
|
|
180
|
+
self,
|
|
181
|
+
mailbox_id: str,
|
|
182
|
+
email: str,
|
|
183
|
+
*,
|
|
184
|
+
force_empty: bool = False,
|
|
185
|
+
) -> dict[str, Any]:
|
|
186
|
+
query: dict[str, Any] = {}
|
|
187
|
+
if force_empty:
|
|
188
|
+
query["force_empty"] = "true"
|
|
189
|
+
return await self._http.request(
|
|
190
|
+
"DELETE",
|
|
191
|
+
f"/v1/mailboxes/{mailbox_id}/allowlist/{quote(email, safe='')}",
|
|
192
|
+
query=query,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
async def list_blocked_attempts(
|
|
196
|
+
self,
|
|
197
|
+
mailbox_id: str,
|
|
198
|
+
*,
|
|
199
|
+
limit: int | None = None,
|
|
200
|
+
cursor: str | None = None,
|
|
201
|
+
all: bool | None = None,
|
|
202
|
+
aggregate: bool | None = None,
|
|
203
|
+
within_days: int | None = None,
|
|
204
|
+
) -> dict[str, Any]:
|
|
205
|
+
"""Migration 038 — blocked attempts log (async).
|
|
206
|
+
|
|
207
|
+
Default ``aggregate=True`` returns the grouped top-N view; pass
|
|
208
|
+
``aggregate=False`` for raw rows. ``within_days=7`` applies a
|
|
209
|
+
recency filter (1..365).
|
|
210
|
+
"""
|
|
211
|
+
query: dict[str, Any] = {}
|
|
212
|
+
if limit is not None:
|
|
213
|
+
query["limit"] = str(limit)
|
|
214
|
+
if cursor is not None:
|
|
215
|
+
query["cursor"] = cursor
|
|
216
|
+
if all:
|
|
217
|
+
query["all"] = "true"
|
|
218
|
+
if aggregate is False:
|
|
219
|
+
query["aggregate"] = "false"
|
|
220
|
+
if within_days is not None:
|
|
221
|
+
query["within_days"] = str(within_days)
|
|
222
|
+
return await self._http.request(
|
|
223
|
+
"GET",
|
|
224
|
+
f"/v1/mailboxes/{mailbox_id}/allowlist/blocked-attempts",
|
|
225
|
+
query=query,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class SyncMailboxInboundAllowlist:
|
|
230
|
+
"""Migration 047 — mailbox-scoped INBOUND sender allowlist (sync).
|
|
231
|
+
|
|
232
|
+
Reached via ``client.mailboxes.inbound_allowlist``. Mirrors
|
|
233
|
+
``SyncMailboxAllowlist`` structurally but auth differs: BOTH admin
|
|
234
|
+
and agent keys can mutate (the firewall protects the customer FROM
|
|
235
|
+
senders, not against their own agents).
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
def __init__(self, http: SyncHttpClient) -> None:
|
|
239
|
+
self._http = http
|
|
240
|
+
|
|
241
|
+
def list(
|
|
242
|
+
self,
|
|
243
|
+
mailbox_id: str,
|
|
244
|
+
*,
|
|
245
|
+
limit: int | None = None,
|
|
246
|
+
cursor: str | None = None,
|
|
247
|
+
all: bool | None = None,
|
|
248
|
+
) -> dict[str, Any]:
|
|
249
|
+
query: dict[str, Any] = {}
|
|
250
|
+
if limit is not None:
|
|
251
|
+
query["limit"] = str(limit)
|
|
252
|
+
if cursor is not None:
|
|
253
|
+
query["cursor"] = cursor
|
|
254
|
+
if all:
|
|
255
|
+
query["all"] = "true"
|
|
256
|
+
return self._http.request(
|
|
257
|
+
"GET", f"/v1/mailboxes/{mailbox_id}/inbound-allowlist", query=query
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
def add(self, mailbox_id: str, *, email: str) -> dict[str, Any]:
|
|
261
|
+
return self._http.request(
|
|
262
|
+
"POST",
|
|
263
|
+
f"/v1/mailboxes/{mailbox_id}/inbound-allowlist",
|
|
264
|
+
body={"email": email},
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
def add_bulk(self, mailbox_id: str, *, emails: list[str]) -> dict[str, Any]:
|
|
268
|
+
return self._http.request(
|
|
269
|
+
"POST",
|
|
270
|
+
f"/v1/mailboxes/{mailbox_id}/inbound-allowlist/bulk",
|
|
271
|
+
body={"emails": emails},
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
def delete(
|
|
275
|
+
self,
|
|
276
|
+
mailbox_id: str,
|
|
277
|
+
email: str,
|
|
278
|
+
*,
|
|
279
|
+
force_empty: bool = False,
|
|
280
|
+
) -> dict[str, Any]:
|
|
281
|
+
query: dict[str, Any] = {}
|
|
282
|
+
if force_empty:
|
|
283
|
+
query["force_empty"] = "true"
|
|
284
|
+
return self._http.request(
|
|
285
|
+
"DELETE",
|
|
286
|
+
f"/v1/mailboxes/{mailbox_id}/inbound-allowlist/{quote(email, safe='')}",
|
|
287
|
+
query=query,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
def list_blocked_attempts(
|
|
291
|
+
self,
|
|
292
|
+
mailbox_id: str,
|
|
293
|
+
*,
|
|
294
|
+
limit: int | None = None,
|
|
295
|
+
cursor: str | None = None,
|
|
296
|
+
all: bool | None = None,
|
|
297
|
+
aggregate: bool | None = None,
|
|
298
|
+
within_days: int | None = None,
|
|
299
|
+
) -> dict[str, Any]:
|
|
300
|
+
"""Migration 047 — inbound firewall blocked-attempts log.
|
|
301
|
+
|
|
302
|
+
Mirrors the outbound ``list_blocked_attempts`` shape; the audit
|
|
303
|
+
rows under inspection here are
|
|
304
|
+
``action='mailbox.inbound_firewall.blocked'``.
|
|
305
|
+
"""
|
|
306
|
+
query: dict[str, Any] = {}
|
|
307
|
+
if limit is not None:
|
|
308
|
+
query["limit"] = str(limit)
|
|
309
|
+
if cursor is not None:
|
|
310
|
+
query["cursor"] = cursor
|
|
311
|
+
if all:
|
|
312
|
+
query["all"] = "true"
|
|
313
|
+
if aggregate is False:
|
|
314
|
+
query["aggregate"] = "false"
|
|
315
|
+
if within_days is not None:
|
|
316
|
+
query["within_days"] = str(within_days)
|
|
317
|
+
return self._http.request(
|
|
318
|
+
"GET",
|
|
319
|
+
f"/v1/mailboxes/{mailbox_id}/inbound-allowlist/blocked-attempts",
|
|
320
|
+
query=query,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class AsyncMailboxInboundAllowlist:
|
|
325
|
+
"""Migration 047 — mailbox-scoped INBOUND sender allowlist (async)."""
|
|
326
|
+
|
|
327
|
+
def __init__(self, http: AsyncHttpClient) -> None:
|
|
328
|
+
self._http = http
|
|
329
|
+
|
|
330
|
+
async def list(
|
|
331
|
+
self,
|
|
332
|
+
mailbox_id: str,
|
|
333
|
+
*,
|
|
334
|
+
limit: int | None = None,
|
|
335
|
+
cursor: str | None = None,
|
|
336
|
+
all: bool | None = None,
|
|
337
|
+
) -> dict[str, Any]:
|
|
338
|
+
query: dict[str, Any] = {}
|
|
339
|
+
if limit is not None:
|
|
340
|
+
query["limit"] = str(limit)
|
|
341
|
+
if cursor is not None:
|
|
342
|
+
query["cursor"] = cursor
|
|
343
|
+
if all:
|
|
344
|
+
query["all"] = "true"
|
|
345
|
+
return await self._http.request(
|
|
346
|
+
"GET", f"/v1/mailboxes/{mailbox_id}/inbound-allowlist", query=query
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
async def add(self, mailbox_id: str, *, email: str) -> dict[str, Any]:
|
|
350
|
+
return await self._http.request(
|
|
351
|
+
"POST",
|
|
352
|
+
f"/v1/mailboxes/{mailbox_id}/inbound-allowlist",
|
|
353
|
+
body={"email": email},
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
async def add_bulk(
|
|
357
|
+
self, mailbox_id: str, *, emails: list[str]
|
|
358
|
+
) -> dict[str, Any]:
|
|
359
|
+
return await self._http.request(
|
|
360
|
+
"POST",
|
|
361
|
+
f"/v1/mailboxes/{mailbox_id}/inbound-allowlist/bulk",
|
|
362
|
+
body={"emails": emails},
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
async def delete(
|
|
366
|
+
self,
|
|
367
|
+
mailbox_id: str,
|
|
368
|
+
email: str,
|
|
369
|
+
*,
|
|
370
|
+
force_empty: bool = False,
|
|
371
|
+
) -> dict[str, Any]:
|
|
372
|
+
query: dict[str, Any] = {}
|
|
373
|
+
if force_empty:
|
|
374
|
+
query["force_empty"] = "true"
|
|
375
|
+
return await self._http.request(
|
|
376
|
+
"DELETE",
|
|
377
|
+
f"/v1/mailboxes/{mailbox_id}/inbound-allowlist/{quote(email, safe='')}",
|
|
378
|
+
query=query,
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
async def list_blocked_attempts(
|
|
382
|
+
self,
|
|
383
|
+
mailbox_id: str,
|
|
384
|
+
*,
|
|
385
|
+
limit: int | None = None,
|
|
386
|
+
cursor: str | None = None,
|
|
387
|
+
all: bool | None = None,
|
|
388
|
+
aggregate: bool | None = None,
|
|
389
|
+
within_days: int | None = None,
|
|
390
|
+
) -> dict[str, Any]:
|
|
391
|
+
query: dict[str, Any] = {}
|
|
392
|
+
if limit is not None:
|
|
393
|
+
query["limit"] = str(limit)
|
|
394
|
+
if cursor is not None:
|
|
395
|
+
query["cursor"] = cursor
|
|
396
|
+
if all:
|
|
397
|
+
query["all"] = "true"
|
|
398
|
+
if aggregate is False:
|
|
399
|
+
query["aggregate"] = "false"
|
|
400
|
+
if within_days is not None:
|
|
401
|
+
query["within_days"] = str(within_days)
|
|
402
|
+
return await self._http.request(
|
|
403
|
+
"GET",
|
|
404
|
+
f"/v1/mailboxes/{mailbox_id}/inbound-allowlist/blocked-attempts",
|
|
405
|
+
query=query,
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
class SyncMailboxes:
|
|
410
|
+
def __init__(self, http: SyncHttpClient) -> None:
|
|
411
|
+
self._http = http
|
|
412
|
+
self.allowlist = SyncMailboxAllowlist(http)
|
|
413
|
+
self.inbound_allowlist = SyncMailboxInboundAllowlist(http)
|
|
414
|
+
|
|
415
|
+
def create(
|
|
416
|
+
self,
|
|
417
|
+
*,
|
|
418
|
+
name: str,
|
|
419
|
+
self_hosted_imap_folder: str | None = None,
|
|
420
|
+
) -> dict[str, Any]:
|
|
421
|
+
body: dict[str, Any] = {"name": name}
|
|
422
|
+
if self_hosted_imap_folder is not None:
|
|
423
|
+
body["self_hosted_imap_folder"] = self_hosted_imap_folder
|
|
424
|
+
return self._http.request("POST", "/v1/mailboxes", body=body)
|
|
425
|
+
|
|
426
|
+
def list(self) -> dict[str, Any]:
|
|
427
|
+
return self._http.request("GET", "/v1/mailboxes")
|
|
428
|
+
|
|
429
|
+
def get_mailbox(self, id: str) -> dict[str, Any]:
|
|
430
|
+
"""Single-mailbox detail.
|
|
431
|
+
|
|
432
|
+
Returns the same shape as a list entry plus
|
|
433
|
+
``legacy_wildcard_active`` (true when the row is on the legacy
|
|
434
|
+
attachment-access compat path; the dashboard banner reads this).
|
|
435
|
+
|
|
436
|
+
Auth: session, admin keys, and mailbox-bound agent keys may read.
|
|
437
|
+
Unbound agent keys see 404 NOT_FOUND.
|
|
438
|
+
"""
|
|
439
|
+
return self._http.request("GET", f"/v1/mailboxes/{id}")
|
|
440
|
+
|
|
441
|
+
def delete(self, id: str) -> dict[str, Any]:
|
|
442
|
+
return self._http.request("DELETE", f"/v1/mailboxes/{id}")
|
|
443
|
+
|
|
444
|
+
def update(
|
|
445
|
+
self,
|
|
446
|
+
id: str,
|
|
447
|
+
*,
|
|
448
|
+
# Round-1 c3: scanner_policy is now optional on the PATCH wire.
|
|
449
|
+
# Pass None (default) to leave the existing policy untouched.
|
|
450
|
+
scanner_policy: ScannerPolicy | None = None,
|
|
451
|
+
pii_mode: PiiMode | None = None,
|
|
452
|
+
default_subaddress_mode: SubaddressMode | None = None,
|
|
453
|
+
recipient_policy_mode: RecipientPolicyMode | None = None,
|
|
454
|
+
force_empty: bool = False,
|
|
455
|
+
hitl_mode: HitlMode | None = None,
|
|
456
|
+
# Migration 085 — thread-scoped reply bypass toggle. None means "do not
|
|
457
|
+
# include the field in the PATCH".
|
|
458
|
+
allow_thread_replies: bool | None = None,
|
|
459
|
+
# PR 8.1 — per-detector redaction visibility. Pass {} to reset to
|
|
460
|
+
# platform default; pass a partial map (e.g.
|
|
461
|
+
# {"EMAIL_ADDRESS": {"redact": False}}) to disable redaction on
|
|
462
|
+
# selected detectors. Tier-gated Pro+ for any redact:false entry.
|
|
463
|
+
# NOTE: None means "do not include the field in the PATCH"; pass {}
|
|
464
|
+
# to explicitly reset to platform default.
|
|
465
|
+
pii_redaction_config: PiiRedactionConfig | None = None,
|
|
466
|
+
) -> dict[str, Any]:
|
|
467
|
+
body: dict[str, Any] = {}
|
|
468
|
+
if scanner_policy is not None:
|
|
469
|
+
body["scanner_policy"] = scanner_policy
|
|
470
|
+
if pii_mode is not None:
|
|
471
|
+
body["pii_mode"] = pii_mode
|
|
472
|
+
if default_subaddress_mode is not None:
|
|
473
|
+
body["default_subaddress_mode"] = default_subaddress_mode
|
|
474
|
+
if recipient_policy_mode is not None:
|
|
475
|
+
body["recipient_policy_mode"] = recipient_policy_mode
|
|
476
|
+
if force_empty:
|
|
477
|
+
body["force_empty"] = True
|
|
478
|
+
if hitl_mode is not None:
|
|
479
|
+
body["hitl_mode"] = hitl_mode
|
|
480
|
+
if allow_thread_replies is not None:
|
|
481
|
+
body["allow_thread_replies"] = allow_thread_replies
|
|
482
|
+
if pii_redaction_config is not None:
|
|
483
|
+
body["pii_redaction_config"] = pii_redaction_config
|
|
484
|
+
return self._http.request("PATCH", f"/v1/mailboxes/{id}", body=body)
|
|
485
|
+
|
|
486
|
+
def set_recipient_policy(
|
|
487
|
+
self,
|
|
488
|
+
id: str,
|
|
489
|
+
mode: RecipientPolicyMode,
|
|
490
|
+
*,
|
|
491
|
+
force_empty: bool = False,
|
|
492
|
+
scanner_policy: ScannerPolicy | None = None,
|
|
493
|
+
) -> dict[str, Any]:
|
|
494
|
+
"""Convenience wrapper for the recipient-policy flip.
|
|
495
|
+
|
|
496
|
+
Round-1 c3: scanner_policy is now optional on the PATCH wire.
|
|
497
|
+
Pass it here only if you want to update it alongside the flip;
|
|
498
|
+
otherwise leave None and the server preserves the existing
|
|
499
|
+
value via COALESCE.
|
|
500
|
+
"""
|
|
501
|
+
return self.update(
|
|
502
|
+
id,
|
|
503
|
+
scanner_policy=scanner_policy,
|
|
504
|
+
recipient_policy_mode=mode,
|
|
505
|
+
force_empty=force_empty,
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
def set_thread_replies(self, id: str, allow: bool) -> dict[str, Any]:
|
|
509
|
+
"""Migration 085 — toggle the thread-scoped reply bypass.
|
|
510
|
+
|
|
511
|
+
Inert in ``blocklist`` mode; in ``allowlist`` mode, ``True`` lets an
|
|
512
|
+
agent reply to / follow up with any visible inbound participant of a
|
|
513
|
+
thread without that address gaining standing send-authority. ``False``
|
|
514
|
+
restores strict allowlist-only sends.
|
|
515
|
+
"""
|
|
516
|
+
return self.update(id, allow_thread_replies=allow)
|
|
517
|
+
|
|
518
|
+
def set_sender_policy(
|
|
519
|
+
self,
|
|
520
|
+
id: str,
|
|
521
|
+
mode: SenderPolicyMode,
|
|
522
|
+
*,
|
|
523
|
+
force_empty: bool = False,
|
|
524
|
+
) -> dict[str, Any]:
|
|
525
|
+
"""Migration 047 — flip the inbound firewall mode (per-mailbox).
|
|
526
|
+
|
|
527
|
+
Pass ``force_empty=True`` to flip to ``allowlist`` while the
|
|
528
|
+
inbound allowlist is empty; the server otherwise returns 409
|
|
529
|
+
``SENDER_POLICY_FLIP_EMPTY_ALLOWLIST``. Auth: admin + agent.
|
|
530
|
+
"""
|
|
531
|
+
body: dict[str, Any] = {"mode": mode}
|
|
532
|
+
if force_empty:
|
|
533
|
+
body["force_empty"] = True
|
|
534
|
+
return self._http.request(
|
|
535
|
+
"PATCH", f"/v1/mailboxes/{id}/sender-policy", body=body
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
@overload
|
|
539
|
+
def set_attachment_access(
|
|
540
|
+
self,
|
|
541
|
+
id: str,
|
|
542
|
+
*,
|
|
543
|
+
enable: bool,
|
|
544
|
+
accept_disclaimer_version: str | None = None,
|
|
545
|
+
) -> AttachmentAccessResponse: ...
|
|
546
|
+
|
|
547
|
+
@overload
|
|
548
|
+
def set_attachment_access(
|
|
549
|
+
self,
|
|
550
|
+
id: str,
|
|
551
|
+
*,
|
|
552
|
+
mode: AttachmentExposureMode,
|
|
553
|
+
allowed_file_families: list[AttachmentAllowedFileFamilyRequest] | None = None,
|
|
554
|
+
accept_disclaimer_version: str | None = None,
|
|
555
|
+
accept_image_risk_version: str | None = None,
|
|
556
|
+
reauth_token: str | None = None,
|
|
557
|
+
) -> AttachmentAccessResponse: ...
|
|
558
|
+
|
|
559
|
+
def set_attachment_access(
|
|
560
|
+
self,
|
|
561
|
+
id: str,
|
|
562
|
+
*,
|
|
563
|
+
enable: bool | None = None,
|
|
564
|
+
mode: AttachmentExposureMode | None = None,
|
|
565
|
+
allowed_file_families: list[AttachmentAllowedFileFamilyRequest] | None = None,
|
|
566
|
+
accept_disclaimer_version: str | None = None,
|
|
567
|
+
accept_image_risk_version: str | None = None,
|
|
568
|
+
reauth_token: str | None = None,
|
|
569
|
+
) -> AttachmentAccessResponse:
|
|
570
|
+
"""Set the per-mailbox agent attachment-access policy.
|
|
571
|
+
|
|
572
|
+
Approved downloads (``mode='raw_download_selected_types'``) **enablement** —
|
|
573
|
+
including any widening of ``allowed_file_families`` against an
|
|
574
|
+
explicit raw-download row —
|
|
575
|
+
is rejected for Bearer-key callers with
|
|
576
|
+
``403 REAUTH_REQUIRES_SESSION``. The route requires session-cookie
|
|
577
|
+
auth + a fresh TOTP code (or password if MFA is not enabled)
|
|
578
|
+
supplied as ``reauth_token``. The SDK's HTTP client does not
|
|
579
|
+
carry session cookies, so approved-download enablement must go through the
|
|
580
|
+
dashboard. The SDK still works for:
|
|
581
|
+
|
|
582
|
+
- reading state (see ``get_mailbox``)
|
|
583
|
+
- flipping between ``metadata_only`` and ``derived_content``
|
|
584
|
+
- same-or-narrower writes within an already-explicit raw-download row
|
|
585
|
+
- disabling via the legacy ``enable=False`` shape
|
|
586
|
+
|
|
587
|
+
If ``allowed_file_families`` includes ``"image"``, the server also
|
|
588
|
+
requires ``accept_image_risk_version`` to match
|
|
589
|
+
``current_image_risk_version`` unless the mailbox already has current
|
|
590
|
+
image-risk acceptance.
|
|
591
|
+
|
|
592
|
+
See docs/inbox-attachment-integration-briefing.md.
|
|
593
|
+
"""
|
|
594
|
+
body = _build_attachment_access_body(
|
|
595
|
+
enable=enable,
|
|
596
|
+
mode=mode,
|
|
597
|
+
allowed_file_families=allowed_file_families,
|
|
598
|
+
accept_disclaimer_version=accept_disclaimer_version,
|
|
599
|
+
accept_image_risk_version=accept_image_risk_version,
|
|
600
|
+
reauth_token=reauth_token,
|
|
601
|
+
)
|
|
602
|
+
return self._http.request(
|
|
603
|
+
"POST",
|
|
604
|
+
f"/v1/mailboxes/{id}/attachment-access",
|
|
605
|
+
body=body,
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
class AsyncMailboxes:
|
|
610
|
+
def __init__(self, http: AsyncHttpClient) -> None:
|
|
611
|
+
self._http = http
|
|
612
|
+
self.allowlist = AsyncMailboxAllowlist(http)
|
|
613
|
+
self.inbound_allowlist = AsyncMailboxInboundAllowlist(http)
|
|
614
|
+
|
|
615
|
+
async def create(
|
|
616
|
+
self,
|
|
617
|
+
*,
|
|
618
|
+
name: str,
|
|
619
|
+
self_hosted_imap_folder: str | None = None,
|
|
620
|
+
) -> dict[str, Any]:
|
|
621
|
+
body: dict[str, Any] = {"name": name}
|
|
622
|
+
if self_hosted_imap_folder is not None:
|
|
623
|
+
body["self_hosted_imap_folder"] = self_hosted_imap_folder
|
|
624
|
+
return await self._http.request("POST", "/v1/mailboxes", body=body)
|
|
625
|
+
|
|
626
|
+
async def list(self) -> dict[str, Any]:
|
|
627
|
+
return await self._http.request("GET", "/v1/mailboxes")
|
|
628
|
+
|
|
629
|
+
async def get_mailbox(self, id: str) -> dict[str, Any]:
|
|
630
|
+
"""Single-mailbox detail (async). See SyncMailboxes.get_mailbox."""
|
|
631
|
+
return await self._http.request("GET", f"/v1/mailboxes/{id}")
|
|
632
|
+
|
|
633
|
+
async def delete(self, id: str) -> dict[str, Any]:
|
|
634
|
+
return await self._http.request("DELETE", f"/v1/mailboxes/{id}")
|
|
635
|
+
|
|
636
|
+
async def update(
|
|
637
|
+
self,
|
|
638
|
+
id: str,
|
|
639
|
+
*,
|
|
640
|
+
# Round-1 c3: scanner_policy is now optional on the PATCH wire.
|
|
641
|
+
# Pass None (default) to leave the existing policy untouched.
|
|
642
|
+
scanner_policy: ScannerPolicy | None = None,
|
|
643
|
+
pii_mode: PiiMode | None = None,
|
|
644
|
+
default_subaddress_mode: SubaddressMode | None = None,
|
|
645
|
+
recipient_policy_mode: RecipientPolicyMode | None = None,
|
|
646
|
+
force_empty: bool = False,
|
|
647
|
+
hitl_mode: HitlMode | None = None,
|
|
648
|
+
# Migration 085 — thread-scoped reply bypass toggle. None = do not
|
|
649
|
+
# include in the PATCH.
|
|
650
|
+
allow_thread_replies: bool | None = None,
|
|
651
|
+
# PR 8.1 — per-detector redaction visibility. Pass {} to reset to
|
|
652
|
+
# platform default; pass a partial map to disable redaction on
|
|
653
|
+
# selected detectors. None = do not include in the PATCH.
|
|
654
|
+
pii_redaction_config: PiiRedactionConfig | None = None,
|
|
655
|
+
) -> dict[str, Any]:
|
|
656
|
+
body: dict[str, Any] = {}
|
|
657
|
+
if scanner_policy is not None:
|
|
658
|
+
body["scanner_policy"] = scanner_policy
|
|
659
|
+
if pii_mode is not None:
|
|
660
|
+
body["pii_mode"] = pii_mode
|
|
661
|
+
if default_subaddress_mode is not None:
|
|
662
|
+
body["default_subaddress_mode"] = default_subaddress_mode
|
|
663
|
+
if recipient_policy_mode is not None:
|
|
664
|
+
body["recipient_policy_mode"] = recipient_policy_mode
|
|
665
|
+
if force_empty:
|
|
666
|
+
body["force_empty"] = True
|
|
667
|
+
if hitl_mode is not None:
|
|
668
|
+
body["hitl_mode"] = hitl_mode
|
|
669
|
+
if allow_thread_replies is not None:
|
|
670
|
+
body["allow_thread_replies"] = allow_thread_replies
|
|
671
|
+
if pii_redaction_config is not None:
|
|
672
|
+
body["pii_redaction_config"] = pii_redaction_config
|
|
673
|
+
return await self._http.request("PATCH", f"/v1/mailboxes/{id}", body=body)
|
|
674
|
+
|
|
675
|
+
async def set_recipient_policy(
|
|
676
|
+
self,
|
|
677
|
+
id: str,
|
|
678
|
+
mode: RecipientPolicyMode,
|
|
679
|
+
*,
|
|
680
|
+
force_empty: bool = False,
|
|
681
|
+
scanner_policy: ScannerPolicy | None = None,
|
|
682
|
+
) -> dict[str, Any]:
|
|
683
|
+
"""Convenience wrapper for the recipient-policy flip (async).
|
|
684
|
+
|
|
685
|
+
Round-1 c3: scanner_policy is now optional on the PATCH wire.
|
|
686
|
+
Pass it here only if you want to update it alongside the flip;
|
|
687
|
+
otherwise leave None and the server preserves the existing
|
|
688
|
+
value via COALESCE. (Earlier draft of this wrapper sent {}
|
|
689
|
+
as a passthrough, which under the relaxed schema would have
|
|
690
|
+
been a real scanner-policy update overwriting the existing
|
|
691
|
+
value — round-2 audit caught the async-only regression.)
|
|
692
|
+
"""
|
|
693
|
+
return await self.update(
|
|
694
|
+
id,
|
|
695
|
+
scanner_policy=scanner_policy,
|
|
696
|
+
recipient_policy_mode=mode,
|
|
697
|
+
force_empty=force_empty,
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
async def set_thread_replies(self, id: str, allow: bool) -> dict[str, Any]:
|
|
701
|
+
"""Migration 085 — toggle the thread-scoped reply bypass (async).
|
|
702
|
+
|
|
703
|
+
Inert in ``blocklist`` mode; in ``allowlist`` mode, ``True`` lets an
|
|
704
|
+
agent reply to / follow up with any visible inbound participant of a
|
|
705
|
+
thread without that address gaining standing send-authority.
|
|
706
|
+
"""
|
|
707
|
+
return await self.update(id, allow_thread_replies=allow)
|
|
708
|
+
|
|
709
|
+
async def set_sender_policy(
|
|
710
|
+
self,
|
|
711
|
+
id: str,
|
|
712
|
+
mode: SenderPolicyMode,
|
|
713
|
+
*,
|
|
714
|
+
force_empty: bool = False,
|
|
715
|
+
) -> dict[str, Any]:
|
|
716
|
+
"""Migration 047 — flip the inbound firewall mode (async)."""
|
|
717
|
+
body: dict[str, Any] = {"mode": mode}
|
|
718
|
+
if force_empty:
|
|
719
|
+
body["force_empty"] = True
|
|
720
|
+
return await self._http.request(
|
|
721
|
+
"PATCH", f"/v1/mailboxes/{id}/sender-policy", body=body
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
@overload
|
|
725
|
+
async def set_attachment_access(
|
|
726
|
+
self,
|
|
727
|
+
id: str,
|
|
728
|
+
*,
|
|
729
|
+
enable: bool,
|
|
730
|
+
accept_disclaimer_version: str | None = None,
|
|
731
|
+
) -> AttachmentAccessResponse: ...
|
|
732
|
+
|
|
733
|
+
@overload
|
|
734
|
+
async def set_attachment_access(
|
|
735
|
+
self,
|
|
736
|
+
id: str,
|
|
737
|
+
*,
|
|
738
|
+
mode: AttachmentExposureMode,
|
|
739
|
+
allowed_file_families: list[AttachmentAllowedFileFamilyRequest] | None = None,
|
|
740
|
+
accept_disclaimer_version: str | None = None,
|
|
741
|
+
accept_image_risk_version: str | None = None,
|
|
742
|
+
reauth_token: str | None = None,
|
|
743
|
+
) -> AttachmentAccessResponse: ...
|
|
744
|
+
|
|
745
|
+
async def set_attachment_access(
|
|
746
|
+
self,
|
|
747
|
+
id: str,
|
|
748
|
+
*,
|
|
749
|
+
enable: bool | None = None,
|
|
750
|
+
mode: AttachmentExposureMode | None = None,
|
|
751
|
+
allowed_file_families: list[AttachmentAllowedFileFamilyRequest] | None = None,
|
|
752
|
+
accept_disclaimer_version: str | None = None,
|
|
753
|
+
accept_image_risk_version: str | None = None,
|
|
754
|
+
reauth_token: str | None = None,
|
|
755
|
+
) -> AttachmentAccessResponse:
|
|
756
|
+
body = _build_attachment_access_body(
|
|
757
|
+
enable=enable,
|
|
758
|
+
mode=mode,
|
|
759
|
+
allowed_file_families=allowed_file_families,
|
|
760
|
+
accept_disclaimer_version=accept_disclaimer_version,
|
|
761
|
+
accept_image_risk_version=accept_image_risk_version,
|
|
762
|
+
reauth_token=reauth_token,
|
|
763
|
+
)
|
|
764
|
+
return await self._http.request(
|
|
765
|
+
"POST",
|
|
766
|
+
f"/v1/mailboxes/{id}/attachment-access",
|
|
767
|
+
body=body,
|
|
768
|
+
)
|