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
replylayer/types.py
ADDED
|
@@ -0,0 +1,1578 @@
|
|
|
1
|
+
"""SDK types — self-contained for PyPI publishing.
|
|
2
|
+
|
|
3
|
+
Derived from packages/sdk/src/types.ts — keep in sync.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Any, Dict, Literal, TypedDict, Union
|
|
10
|
+
# Required/NotRequired land in `typing` proper at 3.11; package targets 3.10
|
|
11
|
+
# (per pyproject.toml requires-python). typing_extensions backports them.
|
|
12
|
+
from typing_extensions import Required, NotRequired
|
|
13
|
+
|
|
14
|
+
# === Enums (Literal unions) ===
|
|
15
|
+
|
|
16
|
+
MessageDirection = Literal["inbound", "outbound"]
|
|
17
|
+
MessageState = Literal[
|
|
18
|
+
"draft", "received", "scanning", "available", "quarantined",
|
|
19
|
+
# Migration 059 / PR 6 — HITL review queue (Pro+); non-terminal
|
|
20
|
+
# (approve→'available', deny→'blocked').
|
|
21
|
+
"pending_review",
|
|
22
|
+
"blocked", "delivered", "bounced", "deleted",
|
|
23
|
+
# Migration 047 — inbound firewall verdict; non-terminal (releasable to scanning).
|
|
24
|
+
"firewall_blocked",
|
|
25
|
+
]
|
|
26
|
+
MailboxStatus = Literal["active", "paused", "deleted"]
|
|
27
|
+
RecipientStatus = Literal["pending", "confirmed", "expired"]
|
|
28
|
+
SuppressionReason = Literal["hard_bounce", "complaint", "manual", "unsubscribe"]
|
|
29
|
+
PolicyDecision = Literal[
|
|
30
|
+
"allow", "allow_with_warning", "quarantine",
|
|
31
|
+
"require_human_approval", "block",
|
|
32
|
+
]
|
|
33
|
+
AttachmentPolicyAction = Literal["deliver", "metadata_only", "quarantine", "block"]
|
|
34
|
+
AvVerdict = Literal["clean", "infected", "error", "skipped"]
|
|
35
|
+
AttachmentExposureMode = Literal["metadata_only", "derived_content", "raw_download_selected_types"]
|
|
36
|
+
AttachmentAllowedFileFamily = Literal["pdf", "text", "csv", "image", "*"]
|
|
37
|
+
AttachmentAllowedFileFamilyRequest = Literal["pdf", "text", "csv", "image"]
|
|
38
|
+
AttachmentDerivativeVariant = Literal["preview_text"]
|
|
39
|
+
AttachmentDerivativeStatus = Literal["pending", "ready", "blocked", "failed"]
|
|
40
|
+
AttachmentPreviewKind = Literal["text"]
|
|
41
|
+
AttachmentRawRetentionStatus = Literal[
|
|
42
|
+
"not_retained",
|
|
43
|
+
"temporary_processing",
|
|
44
|
+
"retained_for_raw_download",
|
|
45
|
+
]
|
|
46
|
+
WebhookEventType = Literal[
|
|
47
|
+
"message.received", "message.delivered", "message.bounced",
|
|
48
|
+
"message.quarantined", "message.scanner_blocked",
|
|
49
|
+
"attachment.preview.queued", "attachment.preview.ready",
|
|
50
|
+
"attachment.preview.blocked", "attachment.preview.failed",
|
|
51
|
+
# Migration 051 — vocabulary alignment: was suppression.* / allowlist.*;
|
|
52
|
+
# renamed to recipient_blocklist.* / recipient_allowlist.* to mirror
|
|
53
|
+
# the inbound side. recipient_blocklist.complaint_override is new.
|
|
54
|
+
"recipient_blocklist.added", "recipient_blocklist.removed",
|
|
55
|
+
"recipient_blocklist.complaint_override",
|
|
56
|
+
"recipient_allowlist.added", "recipient_allowlist.removed",
|
|
57
|
+
"mailbox.recipient_policy_changed",
|
|
58
|
+
"recipient_allowlist.blocked_attempt",
|
|
59
|
+
# Migration 040 — scheduled-send lifecycle events.
|
|
60
|
+
"message.scheduled", "message.rescheduled",
|
|
61
|
+
"message.schedule_cancelled", "message.dispatch_failed",
|
|
62
|
+
# Migration 047 — inbound firewall events.
|
|
63
|
+
"inbound_sender.blocked",
|
|
64
|
+
"sender_allowlist.added", "sender_allowlist.removed",
|
|
65
|
+
"sender_blocklist.added", "sender_blocklist.removed",
|
|
66
|
+
"mailbox.sender_policy_changed",
|
|
67
|
+
# PR 3 — customer legal hold lifecycle (Pro+ apply; release available
|
|
68
|
+
# on every plan, so subscribing to .released stays useful post-downgrade).
|
|
69
|
+
"legal_hold.applied", "legal_hold.released",
|
|
70
|
+
# PR 6 — HITL review queue. queued fires post-commit when a message
|
|
71
|
+
# routes to state='pending_review'; approved/denied fire when the
|
|
72
|
+
# matching endpoint commits the state transition. Subscribable via
|
|
73
|
+
# POST/PATCH /v1/webhooks.enabled_events.
|
|
74
|
+
"message.review.queued", "message.review.approved", "message.review.denied",
|
|
75
|
+
"webhook.test",
|
|
76
|
+
]
|
|
77
|
+
ApiKeyRole = Literal["admin", "agent"]
|
|
78
|
+
PiiMode = Literal["passthrough", "redacted"]
|
|
79
|
+
OutboundPiiType = Literal["ssn", "credit_card", "phone_number"]
|
|
80
|
+
OutboundPiiAction = Literal["allow", "allow_with_warning", "review", "quarantine", "block"]
|
|
81
|
+
OutboundPiiPolicy = dict[OutboundPiiType, OutboundPiiAction]
|
|
82
|
+
OutboundReviewApprovalNotePolicy = Literal["optional", "required_for_sensitive_pii"]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class OutboundReviewPolicy(TypedDict, total=False):
|
|
86
|
+
approval_note: OutboundReviewApprovalNotePolicy
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# PR 8.2 — per-detector redaction operator kinds.
|
|
90
|
+
# - replace_with_type (default): replaces the span with <TYPE>.
|
|
91
|
+
# - partial_mask: masks alphanumerics from the LEFT, keeps last
|
|
92
|
+
# keep_last (1-6), preserves separators verbatim. Pro+ gated;
|
|
93
|
+
# whitelisted detectors only (PERSON/EMAIL_ADDRESS rejected).
|
|
94
|
+
# - hash_replace: replaces span with <TYPE:hash> using HMAC-SHA256
|
|
95
|
+
# with the per-account salt. Pro+ gated.
|
|
96
|
+
PiiOperatorKind = Literal["replace_with_type", "partial_mask", "hash_replace"]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# Operator config for replace_with_type — the explicit default. Carries
|
|
100
|
+
# only the discriminator. Validator rejects keep_last/mask_char on this
|
|
101
|
+
# kind (round-3 audit fix #4 — prevents dead config persisting).
|
|
102
|
+
class PiiReplaceWithTypeOperator(TypedDict):
|
|
103
|
+
kind: Literal["replace_with_type"]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# Operator config for partial_mask. keep_last is REQUIRED integer in
|
|
107
|
+
# [1, 6]; mask_char is OPTIONAL single character (defaults to '*' when
|
|
108
|
+
# omitted). Validator rejects partial_mask on PERSON / EMAIL_ADDRESS
|
|
109
|
+
# (briefing §6.1 whitelist) — partial-masking a name produces nonsense
|
|
110
|
+
# and partial-masking an email is hard to do well in v1.
|
|
111
|
+
class PiiPartialMaskOperator(TypedDict, total=False):
|
|
112
|
+
kind: Required[Literal["partial_mask"]]
|
|
113
|
+
keep_last: Required[int]
|
|
114
|
+
mask_char: str # NotRequired by virtue of total=False
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# Operator config for hash_replace — replaces the span with
|
|
118
|
+
# <TYPE:HMAC-SHA256(account_salt, span)[0..8]>. No per-detector config
|
|
119
|
+
# in v1; the per-account salt lives on accounts.pii_hash_salt and is
|
|
120
|
+
# lazy-initialized by the mailbox PATCH route on first hash_replace use.
|
|
121
|
+
# Validator rejects keep_last/mask_char on this kind (round-3 audit fix #4).
|
|
122
|
+
class PiiHashReplaceOperator(TypedDict):
|
|
123
|
+
kind: Literal["hash_replace"]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# PR 8.2 — per-detector redaction operator. Concrete Union over the three
|
|
127
|
+
# kinds gives static type-checkers a real contract to verify (mirrors the
|
|
128
|
+
# TS SDK's discriminated union). Round-1 PR 8.2 audit fix #1: prior shape
|
|
129
|
+
# was `Dict[str, Any]` which collapsed to no static guarantee — invalid
|
|
130
|
+
# configs like {"kind": "hash_replace", "keep_last": 4} would slip past
|
|
131
|
+
# typing despite the server's 422 rejection.
|
|
132
|
+
PiiOperator = Union[
|
|
133
|
+
PiiReplaceWithTypeOperator,
|
|
134
|
+
PiiPartialMaskOperator,
|
|
135
|
+
PiiHashReplaceOperator,
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# PR 8.1 — per-detector redaction visibility entry. NULL/absent = platform
|
|
140
|
+
# default (redact at delivery with `<TYPE>` placeholder). PR 8.2 adds the
|
|
141
|
+
# optional `operator` field.
|
|
142
|
+
class PiiRedactionConfigEntry(TypedDict, total=False):
|
|
143
|
+
redact: Required[bool]
|
|
144
|
+
operator: PiiOperator
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# PR 8.1 — partial map keyed by detector type (e.g. `EMAIL_ADDRESS`,
|
|
148
|
+
# `CREDIT_CARD`). Persisted to `mailboxes.pii_redaction_config`. Omitted
|
|
149
|
+
# keys default to platform behaviour (redacted).
|
|
150
|
+
PiiRedactionConfig = Dict[str, PiiRedactionConfigEntry]
|
|
151
|
+
|
|
152
|
+
# Per-send / per-mailbox sub-addressing mode (migration 035).
|
|
153
|
+
# - reply_to (default): only Reply-To is rewritten to
|
|
154
|
+
# mailbox+instance_id@domain. From stays clean.
|
|
155
|
+
# - from: also rewrites From to the subaddressed form.
|
|
156
|
+
# - none: no address rewrite. HMAC secure-reply headers are still
|
|
157
|
+
# injected when an instance_id is passed on the send.
|
|
158
|
+
SubaddressMode = Literal["reply_to", "from", "none"]
|
|
159
|
+
|
|
160
|
+
# Per-mailbox recipient policy mode (migration 036).
|
|
161
|
+
# - blocklist (default): suppressed_addresses is the only pre-send gate.
|
|
162
|
+
# - allowlist: recipient MUST be on recipient_allowlists for this mailbox
|
|
163
|
+
# or the send is rejected with 403 RECIPIENT_NOT_ON_ALLOWLIST. Blocklist
|
|
164
|
+
# still runs first (wins on overlap).
|
|
165
|
+
RecipientPolicyMode = Literal["blocklist", "allowlist"]
|
|
166
|
+
|
|
167
|
+
# Per-mailbox INBOUND sender firewall mode (migration 047). Symmetric
|
|
168
|
+
# counterpart to RecipientPolicyMode for the inbound side.
|
|
169
|
+
# - blocklist (default): inbound_sender_blocklists is the only ingest gate.
|
|
170
|
+
# - allowlist: incoming sender MUST be on inbound_sender_allowlists for
|
|
171
|
+
# this mailbox or the message lands in state='firewall_blocked'.
|
|
172
|
+
# Blocklist still runs first (wins on overlap).
|
|
173
|
+
# HMAC-verified agent replies bypass on both Mailgun and IMAP ingress.
|
|
174
|
+
SenderPolicyMode = Literal["blocklist", "allowlist"]
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class FirewallBlock(TypedDict):
|
|
178
|
+
"""Snapshot of the inbound firewall verdict that landed a message in
|
|
179
|
+
state='firewall_blocked' (migration 047). Surfaced on message read APIs
|
|
180
|
+
only when the row was firewall-blocked. Sender-PII fields
|
|
181
|
+
(envelope_sender, from_address, matched_pattern) are redacted by the
|
|
182
|
+
server when mailboxes.pii_mode='redacted'. Categorical fields pass through.
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
envelope_sender: str | None
|
|
186
|
+
from_address: str | None
|
|
187
|
+
matched_field: Literal["envelope", "from"] | None
|
|
188
|
+
matched_pattern: str | None
|
|
189
|
+
reason_code: Literal["SENDER_BLOCKED", "SENDER_NOT_ON_ALLOWLIST"]
|
|
190
|
+
source_table: Literal["inbound_sender_blocklists", "inbound_sender_allowlists"] | None
|
|
191
|
+
mode: SenderPolicyMode
|
|
192
|
+
|
|
193
|
+
# Sprint 039 — allowlist + suppression entries may be either an exact email
|
|
194
|
+
# (`alice@corp.com`) or a domain pattern (`@corp.com`). Server derives
|
|
195
|
+
# `pattern_type` from the stored string. Optional on the wire (older server
|
|
196
|
+
# versions omit it) so clients targeting pre-0.5.0 servers still typecheck.
|
|
197
|
+
RecipientPatternType = Literal["email", "domain"]
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class _HasPatternType(TypedDict, total=False):
|
|
201
|
+
"""Mixin: `pattern_type` is present on 0.5.0+ server responses, absent on
|
|
202
|
+
older servers. Inherited by every list/add/bulk-added/delete entry that
|
|
203
|
+
can now carry a domain pattern."""
|
|
204
|
+
|
|
205
|
+
pattern_type: RecipientPatternType
|
|
206
|
+
|
|
207
|
+
# === Pagination ===
|
|
208
|
+
|
|
209
|
+
@dataclass
|
|
210
|
+
class Page:
|
|
211
|
+
data: list[dict[str, Any]]
|
|
212
|
+
has_more: bool
|
|
213
|
+
cursor: str | None
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# === Domains ===
|
|
217
|
+
|
|
218
|
+
DomainType = Literal["platform", "delegated"]
|
|
219
|
+
DomainVerificationStatus = Literal[
|
|
220
|
+
"pending", "verified", "failed",
|
|
221
|
+
"requested", "pending_dns", "provider_verified",
|
|
222
|
+
"pending_verification", "unhealthy", "rejected",
|
|
223
|
+
]
|
|
224
|
+
TransportMode = Literal["mailgun", "ses", "self_hosted"]
|
|
225
|
+
AdminReviewStatus = Literal["pending_review", "approved", "rejected"]
|
|
226
|
+
SelfHostedSecurity = Literal["starttls", "tls"]
|
|
227
|
+
SelfHostedNetworkMode = Literal["public", "tailnet"]
|
|
228
|
+
SelfHostedProbeGate = Literal["ownership_txt", "imap_auth", "smtp_auth", "folder_missing"]
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class SelfHostedEndpointConfig(TypedDict):
|
|
232
|
+
host: str
|
|
233
|
+
port: int
|
|
234
|
+
security: SelfHostedSecurity
|
|
235
|
+
username: str
|
|
236
|
+
password: str
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class SelfHostedConfig(TypedDict):
|
|
240
|
+
smtp: SelfHostedEndpointConfig
|
|
241
|
+
imap: SelfHostedEndpointConfig
|
|
242
|
+
network_mode: SelfHostedNetworkMode
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class SelfHostedProbeResult(TypedDict):
|
|
246
|
+
gate: SelfHostedProbeGate
|
|
247
|
+
ok: bool
|
|
248
|
+
error: str | None
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class DnsRecord(TypedDict, total=False):
|
|
252
|
+
type: Literal["TXT", "CNAME", "MX"]
|
|
253
|
+
name: str
|
|
254
|
+
value: str
|
|
255
|
+
priority: int
|
|
256
|
+
purpose: Literal["ownership_proof", "spf", "dkim", "tracking", "inbound_mx", "dmarc"]
|
|
257
|
+
mailgun_valid: bool | None
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class Domain(TypedDict, total=False):
|
|
261
|
+
id: str
|
|
262
|
+
domain_name: str
|
|
263
|
+
domain_type: DomainType
|
|
264
|
+
transport_mode: TransportMode
|
|
265
|
+
is_default: bool
|
|
266
|
+
verification_status: DomainVerificationStatus
|
|
267
|
+
dns_records_json: list[DnsRecord] | None
|
|
268
|
+
admin_review_status: AdminReviewStatus | None
|
|
269
|
+
suspended_at: str | None
|
|
270
|
+
created_at: str
|
|
271
|
+
verified_at: str | None
|
|
272
|
+
account_id: str
|
|
273
|
+
mailgun_domain_id: str | None
|
|
274
|
+
ses_verified: bool
|
|
275
|
+
claim_token: str | None
|
|
276
|
+
mailgun_route_id: str | None
|
|
277
|
+
https_tracking_enabled_at: str | None
|
|
278
|
+
admin_review_note: str | None
|
|
279
|
+
admin_reviewed_at: str | None
|
|
280
|
+
last_health_check_at: str | None
|
|
281
|
+
health_failure_count: int
|
|
282
|
+
suspended_by: str | None
|
|
283
|
+
suspension_reason: str | None
|
|
284
|
+
updated_at: str
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class CreateDomainRequest(TypedDict, total=False):
|
|
288
|
+
domain: str
|
|
289
|
+
transport_mode: TransportMode
|
|
290
|
+
self_hosted_config: SelfHostedConfig
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class UpdateSelfHostedDomainConfigRequest(TypedDict, total=False):
|
|
294
|
+
smtp: dict[str, Any]
|
|
295
|
+
imap: dict[str, Any]
|
|
296
|
+
network_mode: SelfHostedNetworkMode
|
|
297
|
+
tailnet_auth_key: str
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
class CreateDomainResponse(TypedDict, total=False):
|
|
301
|
+
id: str
|
|
302
|
+
domain_id: str
|
|
303
|
+
domain_name: str
|
|
304
|
+
domain_type: DomainType
|
|
305
|
+
transport_mode: TransportMode
|
|
306
|
+
verification_status: DomainVerificationStatus
|
|
307
|
+
dns_records_json: list[DnsRecord] | None
|
|
308
|
+
admin_review_status: AdminReviewStatus | None
|
|
309
|
+
is_default: bool
|
|
310
|
+
created_at: str
|
|
311
|
+
message: str
|
|
312
|
+
claim_token: str | None
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class ListDomainsResponse(TypedDict):
|
|
316
|
+
domains: list[Domain]
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class VerifyDomainResponse(TypedDict, total=False):
|
|
320
|
+
verification_status: DomainVerificationStatus
|
|
321
|
+
dns_records_json: list[DnsRecord] | None
|
|
322
|
+
probe_results: list[SelfHostedProbeResult]
|
|
323
|
+
failed_gate: SelfHostedProbeGate | None
|
|
324
|
+
failure_reason: str | None
|
|
325
|
+
message: str
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
class SelfHostedConfigResponse(TypedDict):
|
|
329
|
+
domain_id: str
|
|
330
|
+
network_mode: SelfHostedNetworkMode
|
|
331
|
+
smtp: dict[str, Any]
|
|
332
|
+
imap: dict[str, Any]
|
|
333
|
+
updated_at: str
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class DeleteDomainResponse(TypedDict):
|
|
337
|
+
deleted: bool
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class SetDefaultDomainResponse(TypedDict):
|
|
341
|
+
id: str
|
|
342
|
+
domain_name: str
|
|
343
|
+
is_default: bool
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
# === Mailboxes ===
|
|
347
|
+
|
|
348
|
+
# PR 6 — per-mailbox HITL trigger (migration 059). Defined before
|
|
349
|
+
# CreateMailboxResponse / Mailbox / UpdateMailboxResponse / UpdateMailboxRequest
|
|
350
|
+
# to keep Python module-load order valid (type aliases must precede use).
|
|
351
|
+
HitlMode = Literal["disabled", "all_outbound"]
|
|
352
|
+
|
|
353
|
+
# PR 7 — discriminator for which surface(s) routed an outbound message
|
|
354
|
+
# into the HITL review queue. Surfaced on Message.review_trigger_source
|
|
355
|
+
# (read-side responses) and on MessageReviewQueuedWebhookPayload.trigger_source
|
|
356
|
+
# (webhook payload). Persisted in messages.review_trigger_source
|
|
357
|
+
# (migration 061); RETAINED across approve/deny.
|
|
358
|
+
ReviewQueueTriggerSource = Literal["mailbox_policy", "scanner", "both"]
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
class ScannerPolicy(TypedDict, total=False):
|
|
362
|
+
language_mode: Literal["english_only", "allow_all_languages", "disabled"]
|
|
363
|
+
disabled_scanners: list[str]
|
|
364
|
+
disabled_proxy_criteria: list[str]
|
|
365
|
+
# PR 9 — per-type outbound PII send-safety tuning. Omitted keys use
|
|
366
|
+
# platform defaults; relaxed values are Pro+ gated by pii_advanced_controls.
|
|
367
|
+
outbound_pii_policy: OutboundPiiPolicy
|
|
368
|
+
# Approval workflow settings for outbound scanner-emitted review.
|
|
369
|
+
outbound_review_policy: OutboundReviewPolicy
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
class CreateMailboxResponse(TypedDict):
|
|
373
|
+
id: str
|
|
374
|
+
name: str
|
|
375
|
+
address: str
|
|
376
|
+
# Migration 036 — new mailboxes default to 'blocklist'.
|
|
377
|
+
recipient_policy_mode: RecipientPolicyMode
|
|
378
|
+
# Migration 047 — inbound firewall mode; new mailboxes default to 'blocklist'.
|
|
379
|
+
sender_policy_mode: SenderPolicyMode
|
|
380
|
+
# PR 6 — per-mailbox HITL trigger; new mailboxes default to 'disabled'.
|
|
381
|
+
hitl_mode: HitlMode
|
|
382
|
+
# Migration 085 — thread-scoped reply bypass flag; new mailboxes default True.
|
|
383
|
+
allow_thread_replies: bool
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
class Mailbox(TypedDict):
|
|
387
|
+
id: str
|
|
388
|
+
name: str
|
|
389
|
+
address: str
|
|
390
|
+
status: MailboxStatus
|
|
391
|
+
scanner_policy: ScannerPolicy | None
|
|
392
|
+
pii_mode: PiiMode
|
|
393
|
+
# Legacy attachment-access acceptance flag (migration 034). Retained for
|
|
394
|
+
# backward-compatible reads and audit history, but effective attachment
|
|
395
|
+
# exposure now comes from `attachment_exposure_mode` when set; otherwise
|
|
396
|
+
# the mailbox behaves as `metadata_only`.
|
|
397
|
+
attachment_access_enabled: bool
|
|
398
|
+
attachment_access_accepted_at: str | None
|
|
399
|
+
attachment_access_accepted_version: str | None
|
|
400
|
+
attachment_exposure_mode: AttachmentExposureMode
|
|
401
|
+
attachment_allowed_file_families: list[AttachmentAllowedFileFamily]
|
|
402
|
+
attachment_reauth_at: str | None
|
|
403
|
+
attachment_policy_version: str | None
|
|
404
|
+
image_raw_download_confirmed: bool
|
|
405
|
+
current_image_risk_version: str
|
|
406
|
+
attachment_image_access_accepted_at: str | None
|
|
407
|
+
attachment_image_access_accepted_version: str | None
|
|
408
|
+
# Always the current disclaimer version. Compare to accepted_version to
|
|
409
|
+
# detect when a "disclaimer updated" banner should render.
|
|
410
|
+
current_disclaimer_version: str
|
|
411
|
+
# True when the mailbox is on the legacy compat path
|
|
412
|
+
# (attachment_exposure_mode is null but attachment_access_enabled is true).
|
|
413
|
+
# Drives the dashboard re-acceptance banner; flips to false after the
|
|
414
|
+
# customer accepts an explicit mode. See
|
|
415
|
+
# docs/runbooks/legacy-attachment-access-migration.md.
|
|
416
|
+
legacy_wildcard_active: bool
|
|
417
|
+
# Migration 035 — default outbound sub-addressing rewrite mode.
|
|
418
|
+
default_subaddress_mode: SubaddressMode
|
|
419
|
+
# Migration 036 — recipient policy mode.
|
|
420
|
+
recipient_policy_mode: RecipientPolicyMode
|
|
421
|
+
# Migration 047 — inbound firewall mode (per-mailbox).
|
|
422
|
+
sender_policy_mode: SenderPolicyMode
|
|
423
|
+
# PR 6 — per-mailbox HITL trigger.
|
|
424
|
+
hitl_mode: HitlMode
|
|
425
|
+
# Migration 085 — thread-scoped reply bypass flag. Inert in blocklist mode;
|
|
426
|
+
# in allowlist mode permits a reply/follow-up to a visible inbound thread
|
|
427
|
+
# participant without a standing grant.
|
|
428
|
+
allow_thread_replies: bool
|
|
429
|
+
# PR 8.1 — per-detector redaction visibility. None = platform default
|
|
430
|
+
# (all 13 detectors redact with `<TYPE>` placeholder). Populated value
|
|
431
|
+
# is a partial map keyed by detector type; tier-gated Pro+ for any
|
|
432
|
+
# `redact: false` entry.
|
|
433
|
+
pii_redaction_config: PiiRedactionConfig | None
|
|
434
|
+
created_at: str
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
class ListMailboxesResponse(TypedDict):
|
|
438
|
+
mailboxes: list[Mailbox]
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
class UpdateMailboxResponse(TypedDict):
|
|
442
|
+
id: str
|
|
443
|
+
name: str
|
|
444
|
+
address: str
|
|
445
|
+
scanner_policy: ScannerPolicy | None
|
|
446
|
+
pii_mode: PiiMode
|
|
447
|
+
attachment_access_enabled: bool
|
|
448
|
+
attachment_access_accepted_at: str | None
|
|
449
|
+
attachment_access_accepted_version: str | None
|
|
450
|
+
attachment_exposure_mode: AttachmentExposureMode
|
|
451
|
+
attachment_allowed_file_families: list[AttachmentAllowedFileFamily]
|
|
452
|
+
attachment_reauth_at: str | None
|
|
453
|
+
attachment_policy_version: str | None
|
|
454
|
+
image_raw_download_confirmed: bool
|
|
455
|
+
current_image_risk_version: str
|
|
456
|
+
attachment_image_access_accepted_at: str | None
|
|
457
|
+
attachment_image_access_accepted_version: str | None
|
|
458
|
+
current_disclaimer_version: str
|
|
459
|
+
legacy_wildcard_active: bool
|
|
460
|
+
# Migration 035.
|
|
461
|
+
default_subaddress_mode: SubaddressMode
|
|
462
|
+
# Migration 036.
|
|
463
|
+
recipient_policy_mode: RecipientPolicyMode
|
|
464
|
+
# Migration 047.
|
|
465
|
+
sender_policy_mode: SenderPolicyMode
|
|
466
|
+
# PR 6 — per-mailbox HITL trigger.
|
|
467
|
+
hitl_mode: HitlMode
|
|
468
|
+
# Migration 085 — thread-scoped reply bypass flag.
|
|
469
|
+
allow_thread_replies: bool
|
|
470
|
+
# PR 8.1 — per-detector redaction visibility (None = platform default).
|
|
471
|
+
pii_redaction_config: PiiRedactionConfig | None
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
# Mailbox attachment-access route request shapes.
|
|
475
|
+
# Legacy `enable=False` remains the disable path. Legacy `enable=True` and
|
|
476
|
+
# explicit `raw_download_selected_types` enablement/widening requires Pro+
|
|
477
|
+
# entitlement plus a dashboard/session-cookie re-auth flow; Bearer-key SDK
|
|
478
|
+
# callers can still disable, use safer modes, and perform same-or-narrower
|
|
479
|
+
# raw-download writes.
|
|
480
|
+
class AttachmentAccessLegacyRequest(TypedDict, total=False):
|
|
481
|
+
enable: bool
|
|
482
|
+
accept_disclaimer_version: str
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
class AttachmentAccessPolicyRequest(TypedDict, total=False):
|
|
486
|
+
mode: AttachmentExposureMode
|
|
487
|
+
allowed_file_families: list[AttachmentAllowedFileFamilyRequest]
|
|
488
|
+
accept_disclaimer_version: str
|
|
489
|
+
accept_image_risk_version: str
|
|
490
|
+
reauth_token: str
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
class AttachmentAccessResponse(TypedDict):
|
|
494
|
+
mailbox_id: str
|
|
495
|
+
mode: AttachmentExposureMode
|
|
496
|
+
allowed_file_families: list[AttachmentAllowedFileFamily]
|
|
497
|
+
accepted_at: str | None
|
|
498
|
+
accepted_version: str | None
|
|
499
|
+
attachment_access_enabled: bool
|
|
500
|
+
attachment_access_accepted_at: str | None
|
|
501
|
+
attachment_access_accepted_version: str | None
|
|
502
|
+
image_raw_download_confirmed: bool
|
|
503
|
+
current_image_risk_version: str
|
|
504
|
+
attachment_image_access_accepted_at: str | None
|
|
505
|
+
attachment_image_access_accepted_version: str | None
|
|
506
|
+
current_disclaimer_version: str
|
|
507
|
+
# Mirrors Mailbox.legacy_wildcard_active; flips to false after a
|
|
508
|
+
# successful explicit-shape write.
|
|
509
|
+
legacy_wildcard_active: bool
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
# Migration 036 — allowlist entries.
|
|
513
|
+
class _AllowlistEntryRequired(TypedDict):
|
|
514
|
+
email: str
|
|
515
|
+
mailbox_id: str
|
|
516
|
+
created_at: str
|
|
517
|
+
added_by_actor_type: str | None
|
|
518
|
+
added_by_actor_id: str | None
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
class AllowlistEntry(_AllowlistEntryRequired, _HasPatternType):
|
|
522
|
+
"""Allowlist entry. `pattern_type` present on 0.5.0+ server responses."""
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
class ListAllowlistResponse(TypedDict):
|
|
526
|
+
allowlist: list[AllowlistEntry]
|
|
527
|
+
next_cursor: str | None
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
class _AddAllowlistResponseRequired(TypedDict):
|
|
531
|
+
email: str
|
|
532
|
+
mailbox_id: str
|
|
533
|
+
created_at: str | None
|
|
534
|
+
already_existed: bool
|
|
535
|
+
added_by_actor_type: str | None
|
|
536
|
+
added_by_actor_id: str | None
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
class AddAllowlistResponse(_AddAllowlistResponseRequired, _HasPatternType):
|
|
540
|
+
"""Response of POST /v1/mailboxes/:id/allowlist."""
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
class _BulkAllowlistAddedRequired(TypedDict):
|
|
544
|
+
email: str
|
|
545
|
+
created_at: str
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
class _BulkAllowlistAdded(_BulkAllowlistAddedRequired, _HasPatternType):
|
|
549
|
+
"""Bulk-added row — `pattern_type` present on 0.5.0+ servers."""
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
class _BulkAllowlistInvalid(TypedDict):
|
|
553
|
+
email: str
|
|
554
|
+
reason: str
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
class _BulkAllowlistCounts(TypedDict):
|
|
558
|
+
added: int
|
|
559
|
+
already_existed: int
|
|
560
|
+
invalid: int
|
|
561
|
+
total: int
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
class BulkAddAllowlistResponse(TypedDict):
|
|
565
|
+
added: list[_BulkAllowlistAdded]
|
|
566
|
+
already_existed: list[str]
|
|
567
|
+
invalid: list[_BulkAllowlistInvalid]
|
|
568
|
+
counts: _BulkAllowlistCounts
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
class DeleteAllowlistResponse(TypedDict, total=False):
|
|
572
|
+
status: str
|
|
573
|
+
email: str
|
|
574
|
+
mailbox_id: str
|
|
575
|
+
created_at: str
|
|
576
|
+
# Sprint 039 — `email` or `@domain.com`. Optional for older servers.
|
|
577
|
+
pattern_type: RecipientPatternType
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
# === Messages ===
|
|
581
|
+
|
|
582
|
+
class HoldContext(TypedDict):
|
|
583
|
+
"""inline-scan-verdict-on-send §2b — policy/HITL hold channel.
|
|
584
|
+
|
|
585
|
+
Present (non-null) only when applyHitlPolicy changed the scanner's
|
|
586
|
+
decision so delivery `status` diverges from `scan.verdict`; when present,
|
|
587
|
+
`summary_reasons` carries >=1 customer-safe line.
|
|
588
|
+
"""
|
|
589
|
+
trigger_source: Literal["mailbox_policy", "scanner", "both"]
|
|
590
|
+
summary_reasons: list[str]
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
class SendMessageResponse(TypedDict):
|
|
594
|
+
message_id: str
|
|
595
|
+
status: Literal["sent", "quarantined", "blocked", "pending_review"]
|
|
596
|
+
warning: str | None
|
|
597
|
+
# Customer-facing scanner verdict; null when no scan results.
|
|
598
|
+
scan: NotRequired["ScanSummary | None"]
|
|
599
|
+
# Policy/HITL hold reason; null when scan explains the hold or status='sent'.
|
|
600
|
+
hold_context: NotRequired["HoldContext | None"]
|
|
601
|
+
daily_limit: int
|
|
602
|
+
sends_remaining: int
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
class MessageSummary(TypedDict, total=False):
|
|
606
|
+
"""List-response item shape.
|
|
607
|
+
|
|
608
|
+
PR 7 marked the type total=False so the additive review_trigger_source
|
|
609
|
+
+ body_preview fields can be modeled as optional without breaking
|
|
610
|
+
existing total=True consumers. The four core required fields below
|
|
611
|
+
are still emitted by the server on every response.
|
|
612
|
+
"""
|
|
613
|
+
id: Required[str]
|
|
614
|
+
direction: Required[MessageDirection]
|
|
615
|
+
state: Required[MessageState]
|
|
616
|
+
sender: Required[str]
|
|
617
|
+
recipient: Required[str]
|
|
618
|
+
subject: Required[str]
|
|
619
|
+
# Owning mailbox name (mailboxes.name), resolved so agents don't see a
|
|
620
|
+
# bare mailbox UUID.
|
|
621
|
+
mailbox_name: NotRequired[str | None]
|
|
622
|
+
# Customer-facing scan summary; null when scan_result_json was null at-rest.
|
|
623
|
+
# Replaces the legacy per-row scan_results array.
|
|
624
|
+
scan: NotRequired["ScanSummary | None"]
|
|
625
|
+
# Migration 035 — may be `<REDACTED>` under mailbox pii_mode=redacted.
|
|
626
|
+
subaddress_instance_id: Required[str | None]
|
|
627
|
+
# Migration 047 — present (non-null) only on state='firewall_blocked'.
|
|
628
|
+
firewall_block: Required[FirewallBlock | None]
|
|
629
|
+
# S7a — thread key. Stripped Message-Id for threaded mail; null for
|
|
630
|
+
# standalone rows where messages.thread_id IS NULL. Surfaced so search-
|
|
631
|
+
# result navigation can route inbound results to the correct thread
|
|
632
|
+
# without a follow-up GET.
|
|
633
|
+
thread_id: Required[str | None]
|
|
634
|
+
created_at: Required[str]
|
|
635
|
+
read_at: Required[str | None]
|
|
636
|
+
# PR 7 (migration 061) — discriminator for which surface routed this
|
|
637
|
+
# row into pending_review. None for any row that never went through
|
|
638
|
+
# pending_review, AND for legacy pre-PR-7.1 pending_review rows
|
|
639
|
+
# (those came from PR 6 mailbox-policy promotion only — dashboards
|
|
640
|
+
# render None → "Policy" by inference). RETAINED across approve/deny.
|
|
641
|
+
review_trigger_source: NotRequired[ReviewQueueTriggerSource | None]
|
|
642
|
+
# Plaintext, whitespace-normalized, 200-character list excerpt.
|
|
643
|
+
body_preview: NotRequired[str | None]
|
|
644
|
+
# S7 NTH-003 — server-computed deep link into the web inbox shell, or None
|
|
645
|
+
# when PUBLIC_LINK_BASE_URL is unset on the server (fail-closed).
|
|
646
|
+
dashboard_url: NotRequired[str | None]
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
class MarkMessageReadResponse(TypedDict):
|
|
650
|
+
"""S7a — POST /v1/messages/:id/read response."""
|
|
651
|
+
message_id: str
|
|
652
|
+
# The row's read_at after the call. For ineligible rows (outbound,
|
|
653
|
+
# deleted, firewall_blocked) this is the existing value (possibly null).
|
|
654
|
+
read_at: str | None
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
class MarkThreadReadResponse(TypedDict):
|
|
658
|
+
"""S7a — POST /v1/mailboxes/:id/threads/:thread_id/read response."""
|
|
659
|
+
thread_id: str
|
|
660
|
+
# Number of rows newly stamped this call (excludes already-read).
|
|
661
|
+
marked_count: int
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
class AttachmentMeta(TypedDict, total=False):
|
|
665
|
+
filename: str
|
|
666
|
+
content_type: str
|
|
667
|
+
size: int
|
|
668
|
+
hash: str
|
|
669
|
+
policy_action: AttachmentPolicyAction
|
|
670
|
+
sniffed_mime_type: str
|
|
671
|
+
declared_mime_type: str
|
|
672
|
+
mime_mismatch: bool
|
|
673
|
+
av_verdict: AvVerdict
|
|
674
|
+
av_signature: str
|
|
675
|
+
av_scanned_at: str
|
|
676
|
+
stored: bool
|
|
677
|
+
content_id: str
|
|
678
|
+
content_disposition: str
|
|
679
|
+
inline: bool
|
|
680
|
+
raw_retention: AttachmentRawRetentionStatus
|
|
681
|
+
raw_deleted_at: str
|
|
682
|
+
raw_retention_reason: str
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
class AttachmentPreviewSummary(TypedDict):
|
|
686
|
+
preview_status: AttachmentDerivativeStatus | None
|
|
687
|
+
preview_kind: AttachmentPreviewKind | None
|
|
688
|
+
preview_reason_code: str | None
|
|
689
|
+
preview_char_count: int | None
|
|
690
|
+
preview_page_count: int | None
|
|
691
|
+
preview_truncated: bool | None
|
|
692
|
+
preview_generated_at: str | None
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
class MessageAttachment(AttachmentMeta, total=False):
|
|
696
|
+
preview_status: AttachmentDerivativeStatus | None
|
|
697
|
+
preview_kind: AttachmentPreviewKind | None
|
|
698
|
+
preview_reason_code: str | None
|
|
699
|
+
preview_char_count: int | None
|
|
700
|
+
preview_page_count: int | None
|
|
701
|
+
preview_truncated: bool | None
|
|
702
|
+
preview_generated_at: str | None
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
# === URL reputation (Google Web Risk) — additive ScanResult fields ===
|
|
706
|
+
#
|
|
707
|
+
# Mirror of packages/sdk/src/types.ts WebRiskLearnMore + WebRiskWarning +
|
|
708
|
+
# the additive ScanResult.expires_at / ScanResult.warning fields.
|
|
709
|
+
# Populated only on unexpired url-reputation malicious verdicts; absent
|
|
710
|
+
# everywhere else (the server-side sanitizer scrubs these fields on
|
|
711
|
+
# expiry, so consumers don't need to implement client-side expiry logic).
|
|
712
|
+
|
|
713
|
+
# Optional documentation literal — `WebRiskLearnMore.threat_type` keeps
|
|
714
|
+
# the wider `str` type (matches the TS SDK and is forward-compatible
|
|
715
|
+
# with future Web Risk threat types). Use this Literal in code that
|
|
716
|
+
# wants exhaustive checking of the four currently-defined threat types.
|
|
717
|
+
WebRiskThreatType = Literal[
|
|
718
|
+
"MALWARE",
|
|
719
|
+
"SOCIAL_ENGINEERING",
|
|
720
|
+
"UNWANTED_SOFTWARE",
|
|
721
|
+
"SOCIAL_ENGINEERING_EXTENDED_COVERAGE",
|
|
722
|
+
]
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
class WebRiskLearnMore(TypedDict):
|
|
726
|
+
"""Per-threat-type learn-more link inside `WebRiskWarning.learn_more`.
|
|
727
|
+
|
|
728
|
+
`threat_type` is `str` (not the `WebRiskThreatType` literal) to match
|
|
729
|
+
the TS SDK contract and stay forward-compatible with new threat
|
|
730
|
+
classes Google may add. Render the URL as an explicit `<a>` element
|
|
731
|
+
when surfacing the warning to end users.
|
|
732
|
+
"""
|
|
733
|
+
|
|
734
|
+
threat_type: str
|
|
735
|
+
url: str
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
class WebRiskWarning(TypedDict):
|
|
739
|
+
"""Structured Google Web Risk warning payload.
|
|
740
|
+
|
|
741
|
+
Present on `ScanResult.warning` only for unexpired url-reputation
|
|
742
|
+
malicious verdicts. The server-side sanitizer clears this field
|
|
743
|
+
once `expires_at` passes, so any warning reaching the SDK is safe
|
|
744
|
+
to render without a client-side expiry re-check.
|
|
745
|
+
|
|
746
|
+
Render `attribution_url` and each `learn_more[].url` as explicit
|
|
747
|
+
`<a>` elements. Never auto-linkify the parent `ScanResult.reason`
|
|
748
|
+
string — it's the plain-text fallback for surfaces that can't
|
|
749
|
+
render structured links (admin CLI, plain-text logs).
|
|
750
|
+
"""
|
|
751
|
+
|
|
752
|
+
qualified_wording: str
|
|
753
|
+
attribution_text: str
|
|
754
|
+
attribution_url: str
|
|
755
|
+
learn_more: list[WebRiskLearnMore]
|
|
756
|
+
affected_urls: list[str]
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
class _ScanResultRequired(TypedDict):
|
|
760
|
+
scanner: str
|
|
761
|
+
decision: PolicyDecision
|
|
762
|
+
confidence: float
|
|
763
|
+
reason: str
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
class ScanResult(_ScanResultRequired, total=False):
|
|
767
|
+
"""Per-scanner verdict on a message.
|
|
768
|
+
|
|
769
|
+
The four required fields (scanner, decision, confidence, reason)
|
|
770
|
+
are emitted by every scanner. `expires_at` and `warning` are
|
|
771
|
+
additive and only appear on url-reputation results — see
|
|
772
|
+
`WebRiskWarning` for the contract. `pii_type` (PR 9) is present only
|
|
773
|
+
on local outbound `scanner="pii"` results and identifies the stable
|
|
774
|
+
local category (`ssn`, `credit_card`, or `phone_number`).
|
|
775
|
+
"""
|
|
776
|
+
|
|
777
|
+
# Vendor-mandated lifetime (currently only url-reputation carries
|
|
778
|
+
# this via Google Web Risk `expireTime`). The server-side sanitizer
|
|
779
|
+
# clears it alongside `warning` + rewrites `reason` to a neutral
|
|
780
|
+
# placeholder once the timestamp passes. Absence means either the
|
|
781
|
+
# entry is unexpiring (non-url-reputation) or it was already
|
|
782
|
+
# scrubbed by the sanitizer.
|
|
783
|
+
expires_at: str
|
|
784
|
+
pii_type: OutboundPiiType
|
|
785
|
+
# Structured Web Risk warning. Populated only on unexpired
|
|
786
|
+
# url-reputation malicious verdicts. See `WebRiskWarning`.
|
|
787
|
+
warning: WebRiskWarning
|
|
788
|
+
# Structural pointer into attachments[] for per-attachment findings (e.g.
|
|
789
|
+
# the attachment-type-mismatch scanner). Surfaced as
|
|
790
|
+
# ScanFinding.attachment_index; never parsed from reason.
|
|
791
|
+
attachment_index: int
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
# === Customer-facing scan summary (renderScanSummary output) ===
|
|
795
|
+
#
|
|
796
|
+
# Mirror of packages/shared/src/scan-summary.ts. Replaces the legacy
|
|
797
|
+
# `scan_results: list[ScanResult]` shape on every read endpoint. Default
|
|
798
|
+
# view returns non-allow findings inline with typed subfields; `view='verbose'`
|
|
799
|
+
# adds clean-allow findings for the full audit trail. Vendor names never
|
|
800
|
+
# appear in any field; Web Risk attribution survives inside findings[i].warning.
|
|
801
|
+
ScanVerdict = Literal["clean", "warning", "review_required", "blocked", "quarantined"]
|
|
802
|
+
|
|
803
|
+
ScanCategory = Literal[
|
|
804
|
+
"prompt_injection",
|
|
805
|
+
"function_call_risk",
|
|
806
|
+
"harmful_content",
|
|
807
|
+
"liability_risk",
|
|
808
|
+
"pii",
|
|
809
|
+
"phishing_url",
|
|
810
|
+
"image_exfil",
|
|
811
|
+
"malware",
|
|
812
|
+
"attachment_policy",
|
|
813
|
+
"mime_mismatch",
|
|
814
|
+
"attachment_type_mismatch",
|
|
815
|
+
"spam",
|
|
816
|
+
"language_policy",
|
|
817
|
+
"recipient_policy",
|
|
818
|
+
"secret_detected",
|
|
819
|
+
"content_similarity",
|
|
820
|
+
"scan_incomplete",
|
|
821
|
+
]
|
|
822
|
+
|
|
823
|
+
ScanSubtype = Literal[
|
|
824
|
+
# prompt_injection
|
|
825
|
+
"jailbreak",
|
|
826
|
+
"instruction_injection",
|
|
827
|
+
# harmful_content
|
|
828
|
+
"toxicity",
|
|
829
|
+
"violence",
|
|
830
|
+
"sexual_content",
|
|
831
|
+
"hate_speech",
|
|
832
|
+
"harassment",
|
|
833
|
+
"self_harm",
|
|
834
|
+
"profanity",
|
|
835
|
+
# secret_detected
|
|
836
|
+
"secret_value",
|
|
837
|
+
"outbound_confidentiality_leak",
|
|
838
|
+
]
|
|
839
|
+
|
|
840
|
+
ScanAttachmentPolicyAction = Literal[
|
|
841
|
+
"quarantine_message",
|
|
842
|
+
"metadata_only_after_rewrite",
|
|
843
|
+
"derived_text_finding",
|
|
844
|
+
]
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
class ScanCategorySummary(TypedDict):
|
|
848
|
+
category: ScanCategory
|
|
849
|
+
decision: PolicyDecision
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
class ScanFinding(TypedDict, total=False):
|
|
853
|
+
category: Required[ScanCategory]
|
|
854
|
+
decision: Required[PolicyDecision]
|
|
855
|
+
reason: Required[str | None]
|
|
856
|
+
warning: WebRiskWarning
|
|
857
|
+
expires_at: str
|
|
858
|
+
pii_type: OutboundPiiType
|
|
859
|
+
subtype: ScanSubtype
|
|
860
|
+
attachment_index: int
|
|
861
|
+
# Filename of the attachment this finding refers to
|
|
862
|
+
# (attachments[attachment_index].filename). Stamped at scan time so it
|
|
863
|
+
# reaches list/wait/send/draft surfaces that carry no attachments[] array.
|
|
864
|
+
attachment_filename: str
|
|
865
|
+
attachment_policy_action: ScanAttachmentPolicyAction
|
|
866
|
+
# Inference-failure-transparency plan §5.5 — discriminates infrastructure
|
|
867
|
+
# failures from model judgments. Present only on non-allow findings backed
|
|
868
|
+
# by external inference.
|
|
869
|
+
failure_class: Literal["inference_error", "model_judgment"]
|
|
870
|
+
# RL-UAT-012/013 — structural handling guidance for the agent.
|
|
871
|
+
agent_instructions: list[str]
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
class ScanSummary(TypedDict):
|
|
875
|
+
verdict: ScanVerdict
|
|
876
|
+
categories: list[ScanCategorySummary]
|
|
877
|
+
findings: list[ScanFinding]
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
ScanView = Literal["summary", "verbose"]
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
class MessageBody(TypedDict):
|
|
884
|
+
format: Literal["text", "html"]
|
|
885
|
+
content: str | None
|
|
886
|
+
char_count: int
|
|
887
|
+
returned_char_count: int
|
|
888
|
+
truncated: bool
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
class GetMessageResponse(TypedDict, total=False):
|
|
892
|
+
"""GET /v1/messages/:id detail response.
|
|
893
|
+
|
|
894
|
+
total=False so PR 6 review-resolution metadata + PR 7 review trigger
|
|
895
|
+
source can be modeled as optional fields. The required-keys subset
|
|
896
|
+
(everything except review_outcome / reviewed_* / review_reason /
|
|
897
|
+
review_trigger_source) is enforced by the server.
|
|
898
|
+
"""
|
|
899
|
+
id: Required[str]
|
|
900
|
+
# Owning mailbox UUID (lets clients derive the mailbox, e.g. reply attachments).
|
|
901
|
+
mailbox_id: Required[str]
|
|
902
|
+
# Owning mailbox name (mailboxes.name), resolved so agents don't see a
|
|
903
|
+
# bare mailbox UUID.
|
|
904
|
+
mailbox_name: NotRequired[str | None]
|
|
905
|
+
direction: Required[MessageDirection]
|
|
906
|
+
state: Required[MessageState]
|
|
907
|
+
sender: Required[str]
|
|
908
|
+
recipient: Required[str]
|
|
909
|
+
subject: Required[str]
|
|
910
|
+
body: Required[MessageBody]
|
|
911
|
+
attachments: Required[list[MessageAttachment]]
|
|
912
|
+
scan: Required[ScanSummary | None]
|
|
913
|
+
thread_id: Required[str | None]
|
|
914
|
+
in_reply_to: Required[str | None]
|
|
915
|
+
# Migration 035 — may be `<REDACTED>` under mailbox pii_mode=redacted.
|
|
916
|
+
subaddress_instance_id: Required[str | None]
|
|
917
|
+
# Migration 047 — present (non-null) only on state='firewall_blocked'.
|
|
918
|
+
firewall_block: Required[FirewallBlock | None]
|
|
919
|
+
read_at: Required[str | None]
|
|
920
|
+
created_at: Required[str]
|
|
921
|
+
# S7 NTH-003 — server-computed deep link into the web inbox shell, or None
|
|
922
|
+
# when PUBLIC_LINK_BASE_URL is unset on the server (fail-closed).
|
|
923
|
+
dashboard_url: NotRequired[str | None]
|
|
924
|
+
# PR 6 — review-resolution metadata. Populated only after a row passes
|
|
925
|
+
# through approve/deny; None on pending_review and pre-PR-6 rows.
|
|
926
|
+
review_outcome: NotRequired[Literal["approved", "denied"] | None]
|
|
927
|
+
reviewed_at: NotRequired[str | None]
|
|
928
|
+
reviewed_by_type: NotRequired[str | None]
|
|
929
|
+
reviewed_by_id: NotRequired[str | None]
|
|
930
|
+
review_reason: NotRequired[str | None]
|
|
931
|
+
# PR 7 (migration 061) — discriminator for which surface routed this
|
|
932
|
+
# row into pending_review. See MessageSummary above for the None-fallback
|
|
933
|
+
# semantics (legacy → "Policy" by inference). RETAINED across approve/deny.
|
|
934
|
+
review_trigger_source: NotRequired[ReviewQueueTriggerSource | None]
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
# === message.received webhook payload (with optional URL-reputation safety) ===
|
|
938
|
+
#
|
|
939
|
+
# Mirror of packages/sdk/src/types.ts MessageReceivedSafetySignal +
|
|
940
|
+
# MessageReceivedWebhookPayload. The `safety` field is additive — it
|
|
941
|
+
# appears only when the url-reputation scanner returned
|
|
942
|
+
# `allow_with_warning` (typically because Web Risk's lists were stale,
|
|
943
|
+
# the confirm API timed out, or the kill switch was on). It is NOT
|
|
944
|
+
# populated from worstDecision across all scanners — other warning
|
|
945
|
+
# scanners' reasons embed customer content that's kept off the
|
|
946
|
+
# message.received wire. `reason` is a ReplyLayer-authored count-only
|
|
947
|
+
# string ("Could not verify reputation for N URL(s) — treat with
|
|
948
|
+
# caution"); it never contains customer URLs.
|
|
949
|
+
|
|
950
|
+
class MessageReceivedSafetySignal(TypedDict):
|
|
951
|
+
"""Optional `data.safety` block on `message.received` webhook events."""
|
|
952
|
+
|
|
953
|
+
decision: Literal["allow_with_warning"]
|
|
954
|
+
reason: str
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
class _MessageReceivedWebhookPayloadRequired(TypedDict):
|
|
958
|
+
message_id: str
|
|
959
|
+
mailbox_id: str
|
|
960
|
+
sender: str
|
|
961
|
+
recipient: str
|
|
962
|
+
subject: str
|
|
963
|
+
received_at: str
|
|
964
|
+
# Migration 035 — `<REDACTED>` under mailbox pii_mode=redacted.
|
|
965
|
+
subaddress_instance_id: str | None
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
class MessageReceivedWebhookPayload(
|
|
969
|
+
_MessageReceivedWebhookPayloadRequired, total=False,
|
|
970
|
+
):
|
|
971
|
+
"""Body of a `message.received` webhook event.
|
|
972
|
+
|
|
973
|
+
`safety` appears only when a url-reputation `allow_with_warning`
|
|
974
|
+
ScanResult was present on the message. Absent for "all clean" or
|
|
975
|
+
when only non-url-reputation scanners returned warnings.
|
|
976
|
+
"""
|
|
977
|
+
|
|
978
|
+
safety: MessageReceivedSafetySignal
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
class AttachmentPreviewWebhookPayloadBase(TypedDict):
|
|
982
|
+
message_id: str
|
|
983
|
+
mailbox_id: str
|
|
984
|
+
attachment_index: int
|
|
985
|
+
variant: AttachmentDerivativeVariant
|
|
986
|
+
|
|
987
|
+
|
|
988
|
+
class AttachmentPreviewQueuedWebhookPayload(AttachmentPreviewWebhookPayloadBase):
|
|
989
|
+
queued_at: str
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
class AttachmentPreviewReadyWebhookPayload(AttachmentPreviewWebhookPayloadBase):
|
|
993
|
+
ready_at: str
|
|
994
|
+
kind: AttachmentPreviewKind
|
|
995
|
+
char_count: int | None
|
|
996
|
+
page_count: int | None
|
|
997
|
+
truncated: bool
|
|
998
|
+
|
|
999
|
+
|
|
1000
|
+
class AttachmentPreviewBlockedWebhookPayload(AttachmentPreviewWebhookPayloadBase):
|
|
1001
|
+
blocked_at: str
|
|
1002
|
+
reason_code: str | None
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
class AttachmentPreviewFailedWebhookPayload(AttachmentPreviewWebhookPayloadBase):
|
|
1006
|
+
failed_at: str
|
|
1007
|
+
reason_code: str | None
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
# PR 6 — HITL review queue webhook payloads. Source-of-truth contract:
|
|
1011
|
+
# docs/webhooks.md (PR 6 rows) + plans/tier-feature-gating-pr6-hitl-review-queue.md §9.
|
|
1012
|
+
#
|
|
1013
|
+
# `message.review.queued` carries subject + recipient (subject to PII
|
|
1014
|
+
# redaction when the source mailbox's pii_mode is 'redacted'); `summary_reasons`
|
|
1015
|
+
# is always non-empty (the helper synthesizes a policy reason when the trigger
|
|
1016
|
+
# is mailbox-policy and there are no scanner findings). Raw `scan_results`
|
|
1017
|
+
# is intentionally NOT included.
|
|
1018
|
+
MessageReviewOrigin = Literal["fresh_send", "reply", "draft"]
|
|
1019
|
+
|
|
1020
|
+
|
|
1021
|
+
class _MessageReviewQueuedWebhookPayloadRequired(TypedDict):
|
|
1022
|
+
"""Required keys for the queued review webhook payload.
|
|
1023
|
+
|
|
1024
|
+
Round-2 audit P3 fix — TS SDK and the runtime contract require all
|
|
1025
|
+
these fields on every emit; only `trigger_source` is optional for
|
|
1026
|
+
wire-additive compatibility with pre-PR-7 emit sites. Mirrors the
|
|
1027
|
+
`MessageReceivedWebhookPayload` required-base pattern.
|
|
1028
|
+
"""
|
|
1029
|
+
message_id: str
|
|
1030
|
+
mailbox_id: str
|
|
1031
|
+
direction: Literal["outbound"]
|
|
1032
|
+
subject: str
|
|
1033
|
+
recipient: str
|
|
1034
|
+
summary_reasons: list[str]
|
|
1035
|
+
origin: MessageReviewOrigin
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
class MessageReviewQueuedWebhookPayload(
|
|
1039
|
+
_MessageReviewQueuedWebhookPayloadRequired, total=False,
|
|
1040
|
+
):
|
|
1041
|
+
"""v1 outbound-only HITL review queue webhook payload.
|
|
1042
|
+
|
|
1043
|
+
PR 7 added the optional `trigger_source` discriminator (which
|
|
1044
|
+
surface(s) routed the message into pending_review). Distinct from
|
|
1045
|
+
`origin` (call-site valued); they are independent dimensions.
|
|
1046
|
+
PR 7.1 + PR 7.3 always populate `trigger_source` server-side, but
|
|
1047
|
+
the type stays NotRequired for wire-additive compatibility with
|
|
1048
|
+
pre-PR-7 emit sites a customer may still receive during a deploy
|
|
1049
|
+
rollout window.
|
|
1050
|
+
"""
|
|
1051
|
+
trigger_source: ReviewQueueTriggerSource
|
|
1052
|
+
|
|
1053
|
+
|
|
1054
|
+
# `.approved` and `.denied` carry no PII fields — only operator-context.
|
|
1055
|
+
# Both fire regardless of dispatch outcome (action is the signal; Mailgun
|
|
1056
|
+
# result is captured by message.delivered / .bounced / .dispatch_failed).
|
|
1057
|
+
class MessageReviewApprovedWebhookPayload(TypedDict):
|
|
1058
|
+
message_id: str
|
|
1059
|
+
mailbox_id: str
|
|
1060
|
+
reviewed_by_type: Literal["admin", "user"]
|
|
1061
|
+
reviewed_at: str # ISO-8601
|
|
1062
|
+
reason: str | None
|
|
1063
|
+
prior_state: Literal["pending_review"]
|
|
1064
|
+
|
|
1065
|
+
|
|
1066
|
+
class MessageReviewDeniedWebhookPayload(TypedDict):
|
|
1067
|
+
message_id: str
|
|
1068
|
+
mailbox_id: str
|
|
1069
|
+
reviewed_by_type: Literal["admin", "user"]
|
|
1070
|
+
reviewed_at: str
|
|
1071
|
+
reason: str | None
|
|
1072
|
+
prior_state: Literal["pending_review"]
|
|
1073
|
+
|
|
1074
|
+
|
|
1075
|
+
class WaitResponse(TypedDict):
|
|
1076
|
+
message: MessageSummary | None
|
|
1077
|
+
|
|
1078
|
+
|
|
1079
|
+
class ReleaseResponse(TypedDict):
|
|
1080
|
+
status: Literal["released", "sent", "blocked"]
|
|
1081
|
+
message_id: str
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
class BlockResponse(TypedDict):
|
|
1085
|
+
status: Literal["blocked"]
|
|
1086
|
+
message_id: str
|
|
1087
|
+
|
|
1088
|
+
|
|
1089
|
+
# === Drafts ===
|
|
1090
|
+
#
|
|
1091
|
+
# A draft is a message in state='draft'. Scanner runs at create + update and
|
|
1092
|
+
# authoritatively re-runs on send. See SyncDrafts / AsyncDrafts in
|
|
1093
|
+
# resources/drafts.py.
|
|
1094
|
+
|
|
1095
|
+
class CreateDraftRequest(TypedDict, total=False):
|
|
1096
|
+
# Fresh mode requires mailbox_id/to/subject/body; thread mode (thread_id
|
|
1097
|
+
# present, migration 085) derives mailbox_id/subject and makes `to` an
|
|
1098
|
+
# optional participant selector. TypedDict total=False marks everything
|
|
1099
|
+
# optional for IDE ergonomics — the server enforces the mode rules.
|
|
1100
|
+
mailbox_id: str
|
|
1101
|
+
to: str
|
|
1102
|
+
subject: str
|
|
1103
|
+
body: str
|
|
1104
|
+
html: str
|
|
1105
|
+
in_reply_to_message_id: str
|
|
1106
|
+
# Migration 085 — continue an existing thread; mutually exclusive with
|
|
1107
|
+
# in_reply_to_message_id.
|
|
1108
|
+
thread_id: str
|
|
1109
|
+
# Migration 035 — sub-addressing routing hint, persisted on the draft.
|
|
1110
|
+
subaddress_instance_id: str
|
|
1111
|
+
subaddress_mode: SubaddressMode
|
|
1112
|
+
# Outbound attachment handles (plans/outbound-attachment-ux.md) — held on
|
|
1113
|
+
# the draft and consumed once at dispatch. Requires the mailbox to have
|
|
1114
|
+
# outbound attachments enabled (Pro+).
|
|
1115
|
+
attachment_ids: list[str]
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
class UpdateDraftRequest(TypedDict, total=False):
|
|
1119
|
+
# All optional — server rejects an empty body with 400.
|
|
1120
|
+
to: str
|
|
1121
|
+
subject: str
|
|
1122
|
+
body: str
|
|
1123
|
+
html: str
|
|
1124
|
+
# Migration 035 — 3-way PATCH sigil: str sets/updates, None clears
|
|
1125
|
+
# (auto-clears subaddress_mode when instance is cleared), omitted is
|
|
1126
|
+
# no-change. Type parity with TS SDK UpdateDraftRequest.
|
|
1127
|
+
subaddress_instance_id: str | None
|
|
1128
|
+
subaddress_mode: SubaddressMode | None
|
|
1129
|
+
# Outbound attachment handles: list replaces the set, None clears all,
|
|
1130
|
+
# omitted is no-change (mirror of the JSON-schema ['array','null']).
|
|
1131
|
+
attachment_ids: list[str] | None
|
|
1132
|
+
|
|
1133
|
+
|
|
1134
|
+
class ListDraftsParams(TypedDict, total=False):
|
|
1135
|
+
limit: int
|
|
1136
|
+
before: str
|
|
1137
|
+
auto_paginate: bool
|
|
1138
|
+
|
|
1139
|
+
|
|
1140
|
+
class DraftResponse(TypedDict):
|
|
1141
|
+
id: str
|
|
1142
|
+
mailbox_id: str
|
|
1143
|
+
# Owning mailbox name (mailboxes.name). Additive/optional.
|
|
1144
|
+
mailbox_name: NotRequired[str | None]
|
|
1145
|
+
state: Literal["draft", "available", "deleted"]
|
|
1146
|
+
sender: str
|
|
1147
|
+
recipient: str
|
|
1148
|
+
subject: str
|
|
1149
|
+
body: MessageBody
|
|
1150
|
+
scan: ScanSummary
|
|
1151
|
+
worst_decision: PolicyDecision
|
|
1152
|
+
thread_id: str | None
|
|
1153
|
+
in_reply_to: str | None
|
|
1154
|
+
# Migration 035 — may be `<REDACTED>` under mailbox pii_mode=redacted.
|
|
1155
|
+
subaddress_instance_id: str | None
|
|
1156
|
+
subaddress_mode: SubaddressMode | None
|
|
1157
|
+
created_at: str
|
|
1158
|
+
updated_at: str
|
|
1159
|
+
|
|
1160
|
+
|
|
1161
|
+
class DraftSummary(TypedDict):
|
|
1162
|
+
id: str
|
|
1163
|
+
mailbox_id: str
|
|
1164
|
+
# Owning mailbox name (mailboxes.name). Additive/optional.
|
|
1165
|
+
mailbox_name: NotRequired[str | None]
|
|
1166
|
+
state: Literal["draft"]
|
|
1167
|
+
sender: str
|
|
1168
|
+
recipient: str
|
|
1169
|
+
subject: str
|
|
1170
|
+
worst_decision: PolicyDecision
|
|
1171
|
+
# Migration 035 — may be `<REDACTED>` under mailbox pii_mode=redacted.
|
|
1172
|
+
subaddress_instance_id: str | None
|
|
1173
|
+
created_at: str
|
|
1174
|
+
updated_at: str
|
|
1175
|
+
|
|
1176
|
+
|
|
1177
|
+
class ListDraftsResponse(TypedDict):
|
|
1178
|
+
drafts: list[DraftSummary]
|
|
1179
|
+
|
|
1180
|
+
|
|
1181
|
+
# === Threads ===
|
|
1182
|
+
|
|
1183
|
+
class ThreadSummary(TypedDict):
|
|
1184
|
+
id: str
|
|
1185
|
+
subject: str
|
|
1186
|
+
first_message_at: str
|
|
1187
|
+
last_message_at: str
|
|
1188
|
+
message_count: int
|
|
1189
|
+
unread_count: int
|
|
1190
|
+
participants: list[str]
|
|
1191
|
+
|
|
1192
|
+
|
|
1193
|
+
class GetThreadResponse(TypedDict):
|
|
1194
|
+
id: str
|
|
1195
|
+
mailbox_id: str
|
|
1196
|
+
# Owning mailbox name (mailboxes.name). Additive/optional.
|
|
1197
|
+
mailbox_name: NotRequired[str | None]
|
|
1198
|
+
subject: str
|
|
1199
|
+
message_count: int
|
|
1200
|
+
messages: list[GetMessageResponse]
|
|
1201
|
+
|
|
1202
|
+
|
|
1203
|
+
# === Attachments ===
|
|
1204
|
+
|
|
1205
|
+
class AttachmentDownloadResponse(TypedDict):
|
|
1206
|
+
url: str
|
|
1207
|
+
expires_at: str
|
|
1208
|
+
content_type: str
|
|
1209
|
+
filename: str
|
|
1210
|
+
size: int
|
|
1211
|
+
av_verdict: str | None
|
|
1212
|
+
|
|
1213
|
+
|
|
1214
|
+
class AttachmentPreviewMetadata(TypedDict):
|
|
1215
|
+
filename: str
|
|
1216
|
+
content_type: str
|
|
1217
|
+
size: int
|
|
1218
|
+
hash: str
|
|
1219
|
+
|
|
1220
|
+
|
|
1221
|
+
class AttachmentPreviewBody(TypedDict):
|
|
1222
|
+
kind: AttachmentPreviewKind
|
|
1223
|
+
content: str
|
|
1224
|
+
truncated: bool
|
|
1225
|
+
char_count: int | None
|
|
1226
|
+
page_count: int | None
|
|
1227
|
+
extractor: str
|
|
1228
|
+
generated_at: str
|
|
1229
|
+
|
|
1230
|
+
|
|
1231
|
+
class AttachmentPreviewResponse(TypedDict):
|
|
1232
|
+
attachment: AttachmentPreviewMetadata
|
|
1233
|
+
preview: AttachmentPreviewBody
|
|
1234
|
+
|
|
1235
|
+
|
|
1236
|
+
# Outbound-attachment content-scan lifecycle (POST /v1/attachments):
|
|
1237
|
+
# pending → async text content-scan not yet run; referencing on a send → 409
|
|
1238
|
+
# clean → no findings; sendable
|
|
1239
|
+
# flagged → secret/PII finding(s); still sendable, findings flow to the
|
|
1240
|
+
# message verdict (block/quarantine by severity, like a body finding)
|
|
1241
|
+
# error → scan failed; fail-closed (cannot be sent)
|
|
1242
|
+
OutboundAttachmentScanStatus = Literal["pending", "clean", "flagged", "error"]
|
|
1243
|
+
|
|
1244
|
+
|
|
1245
|
+
class UploadAttachmentResponse(TypedDict):
|
|
1246
|
+
"""201 body of POST /v1/attachments. ``id`` is the opaque handle to put in a
|
|
1247
|
+
send/reply/draft ``attachment_ids`` list. ``scan`` is present only when the
|
|
1248
|
+
synchronous filename scan produced findings. ``content_scan_status`` is
|
|
1249
|
+
always ``"pending"`` at upload time — poll ``get_upload(id)`` until terminal
|
|
1250
|
+
before referencing the handle."""
|
|
1251
|
+
id: str
|
|
1252
|
+
filename: str
|
|
1253
|
+
content_type: str
|
|
1254
|
+
size: int
|
|
1255
|
+
hash: str
|
|
1256
|
+
scan: NotRequired[ScanSummary]
|
|
1257
|
+
content_scan_status: OutboundAttachmentScanStatus
|
|
1258
|
+
|
|
1259
|
+
|
|
1260
|
+
class ConsumedAttachmentResponse(TypedDict):
|
|
1261
|
+
"""GET /v1/attachments/:id once the handle has been consumed by a send."""
|
|
1262
|
+
id: str
|
|
1263
|
+
status: Literal["consumed"]
|
|
1264
|
+
consumed_at: str
|
|
1265
|
+
consumed_message_id: str | None
|
|
1266
|
+
|
|
1267
|
+
|
|
1268
|
+
# GET /v1/attachments/:id returns the active-handle shape (UploadAttachmentResponse)
|
|
1269
|
+
# or, once consumed, ConsumedAttachmentResponse — discriminate on the `status` key.
|
|
1270
|
+
GetUploadAttachmentResponse = Union[UploadAttachmentResponse, ConsumedAttachmentResponse]
|
|
1271
|
+
|
|
1272
|
+
|
|
1273
|
+
# === Webhooks ===
|
|
1274
|
+
|
|
1275
|
+
class CreateWebhookResponse(TypedDict):
|
|
1276
|
+
id: str
|
|
1277
|
+
url: str
|
|
1278
|
+
description: str | None
|
|
1279
|
+
enabled: bool
|
|
1280
|
+
enabled_events: list[WebhookEventType]
|
|
1281
|
+
signing_secret: str
|
|
1282
|
+
created_at: str
|
|
1283
|
+
|
|
1284
|
+
|
|
1285
|
+
class WebhookSummary(TypedDict):
|
|
1286
|
+
id: str
|
|
1287
|
+
url: str
|
|
1288
|
+
description: str | None
|
|
1289
|
+
enabled: bool
|
|
1290
|
+
enabled_events: list[WebhookEventType]
|
|
1291
|
+
created_at: str
|
|
1292
|
+
updated_at: str
|
|
1293
|
+
# D6: health fields surfaced by the delivery worker
|
|
1294
|
+
consecutive_failures: int
|
|
1295
|
+
last_success_at: str | None
|
|
1296
|
+
last_failure_at: str | None
|
|
1297
|
+
last_error: str | None
|
|
1298
|
+
disabled_at: str | None
|
|
1299
|
+
disabled_reason: str | None
|
|
1300
|
+
|
|
1301
|
+
|
|
1302
|
+
class ListWebhooksResponse(TypedDict):
|
|
1303
|
+
webhooks: list[WebhookSummary]
|
|
1304
|
+
|
|
1305
|
+
|
|
1306
|
+
class RotateWebhookSecretResponse(TypedDict):
|
|
1307
|
+
signing_secret: str
|
|
1308
|
+
|
|
1309
|
+
|
|
1310
|
+
class TestWebhookResponse(TypedDict):
|
|
1311
|
+
delivery_id: str
|
|
1312
|
+
|
|
1313
|
+
|
|
1314
|
+
WebhookDeliveryStatus = Literal["pending", "delivering", "delivered", "failed"]
|
|
1315
|
+
|
|
1316
|
+
|
|
1317
|
+
class WebhookDeliverySummary(TypedDict):
|
|
1318
|
+
id: str
|
|
1319
|
+
event_type: str
|
|
1320
|
+
status: WebhookDeliveryStatus
|
|
1321
|
+
http_status: int | None
|
|
1322
|
+
attempt_count: int
|
|
1323
|
+
created_at: str
|
|
1324
|
+
delivered_at: str | None
|
|
1325
|
+
failed_at: str | None
|
|
1326
|
+
next_retry_at: str | None
|
|
1327
|
+
response_preview: str | None
|
|
1328
|
+
|
|
1329
|
+
|
|
1330
|
+
# deliveries + has_more are always present; cursor fields appear only
|
|
1331
|
+
# when has_more is True. Split into a required base + total=False extension
|
|
1332
|
+
# so consumers don't need `if 'deliveries' in resp` defensive checks.
|
|
1333
|
+
class _ListDeliveriesRequired(TypedDict):
|
|
1334
|
+
deliveries: list[WebhookDeliverySummary]
|
|
1335
|
+
has_more: bool
|
|
1336
|
+
|
|
1337
|
+
|
|
1338
|
+
class ListDeliveriesResponse(_ListDeliveriesRequired, total=False):
|
|
1339
|
+
next_before_at: str
|
|
1340
|
+
next_before_id: str
|
|
1341
|
+
|
|
1342
|
+
|
|
1343
|
+
class RetryDeliveryResponse(TypedDict):
|
|
1344
|
+
delivery_id: str
|
|
1345
|
+
status: Literal["pending"]
|
|
1346
|
+
|
|
1347
|
+
|
|
1348
|
+
# === Recipients ===
|
|
1349
|
+
|
|
1350
|
+
class AddRecipientResponse(TypedDict):
|
|
1351
|
+
id: str
|
|
1352
|
+
email: str
|
|
1353
|
+
status: RecipientStatus
|
|
1354
|
+
created_at: str
|
|
1355
|
+
|
|
1356
|
+
|
|
1357
|
+
class Recipient(TypedDict):
|
|
1358
|
+
id: str
|
|
1359
|
+
email: str
|
|
1360
|
+
status: RecipientStatus
|
|
1361
|
+
created_at: str
|
|
1362
|
+
confirmed_at: str | None
|
|
1363
|
+
|
|
1364
|
+
|
|
1365
|
+
class ListRecipientsResponse(TypedDict):
|
|
1366
|
+
recipients: list[Recipient]
|
|
1367
|
+
|
|
1368
|
+
|
|
1369
|
+
# === Suppressions ===
|
|
1370
|
+
|
|
1371
|
+
class _SuppressionRequired(TypedDict):
|
|
1372
|
+
email: str
|
|
1373
|
+
reason: SuppressionReason
|
|
1374
|
+
source: str
|
|
1375
|
+
created_at: str
|
|
1376
|
+
added_by_actor_type: str | None
|
|
1377
|
+
added_by_actor_id: str | None
|
|
1378
|
+
|
|
1379
|
+
|
|
1380
|
+
class _SuppressionOptional(TypedDict, total=False):
|
|
1381
|
+
# Migration 051 — complaint history columns.
|
|
1382
|
+
#
|
|
1383
|
+
# `latest_complaint_at` is the LOCK SIGNAL: the most recent moment a
|
|
1384
|
+
# complaint signal was recorded (worker-received provider event OR
|
|
1385
|
+
# Mailgun-sync observation). Customer DELETE on rows with non-null
|
|
1386
|
+
# latest_complaint_at is rejected with 409
|
|
1387
|
+
# RECIPIENT_BLOCKLIST_COMPLAINT_LOCKED. complaint_count does NOT
|
|
1388
|
+
# affect lock behavior.
|
|
1389
|
+
#
|
|
1390
|
+
# `complaint_count` is the count of distinct provider complaint
|
|
1391
|
+
# events received by the worker. Mailgun-sync observations do NOT
|
|
1392
|
+
# increment it — a row locked by sync alone may show
|
|
1393
|
+
# complaint_count=0 while latest_complaint_at is set.
|
|
1394
|
+
latest_complaint_at: str | None
|
|
1395
|
+
complaint_count: int
|
|
1396
|
+
|
|
1397
|
+
|
|
1398
|
+
class Suppression(_SuppressionRequired, _HasPatternType, _SuppressionOptional):
|
|
1399
|
+
"""Suppression entry. `pattern_type` present on 0.5.0+ server responses;
|
|
1400
|
+
`latest_complaint_at` and `complaint_count` present on migration-051+ servers."""
|
|
1401
|
+
|
|
1402
|
+
|
|
1403
|
+
class ListSuppressionsResponse(TypedDict):
|
|
1404
|
+
suppressions: list[Suppression]
|
|
1405
|
+
next_cursor: str | None
|
|
1406
|
+
|
|
1407
|
+
|
|
1408
|
+
class AddSuppressionRequest(TypedDict):
|
|
1409
|
+
email: str
|
|
1410
|
+
|
|
1411
|
+
|
|
1412
|
+
class _AddSuppressionResponseRequired(TypedDict):
|
|
1413
|
+
email: str
|
|
1414
|
+
reason: Literal["manual"]
|
|
1415
|
+
source: Literal["customer"]
|
|
1416
|
+
created_at: str | None
|
|
1417
|
+
already_existed: bool
|
|
1418
|
+
added_by_actor_type: str | None
|
|
1419
|
+
added_by_actor_id: str | None
|
|
1420
|
+
|
|
1421
|
+
|
|
1422
|
+
class AddSuppressionResponse(_AddSuppressionResponseRequired, _HasPatternType):
|
|
1423
|
+
"""Response of POST /v1/suppressions."""
|
|
1424
|
+
|
|
1425
|
+
|
|
1426
|
+
class BulkAddSuppressionsRequest(TypedDict):
|
|
1427
|
+
emails: list[str]
|
|
1428
|
+
|
|
1429
|
+
|
|
1430
|
+
class _BulkAddedRowRequired(TypedDict):
|
|
1431
|
+
email: str
|
|
1432
|
+
created_at: str
|
|
1433
|
+
|
|
1434
|
+
|
|
1435
|
+
class BulkAddedRow(_BulkAddedRowRequired, _HasPatternType):
|
|
1436
|
+
"""Bulk-added row. `pattern_type` present on 0.5.0+ server responses."""
|
|
1437
|
+
|
|
1438
|
+
|
|
1439
|
+
class BulkAddInvalidEntry(TypedDict):
|
|
1440
|
+
email: str
|
|
1441
|
+
reason: str
|
|
1442
|
+
|
|
1443
|
+
|
|
1444
|
+
class BulkAddCounts(TypedDict):
|
|
1445
|
+
added: int
|
|
1446
|
+
already_existed: int
|
|
1447
|
+
invalid: int
|
|
1448
|
+
total: int
|
|
1449
|
+
|
|
1450
|
+
|
|
1451
|
+
class BulkAddSuppressionsResponse(TypedDict):
|
|
1452
|
+
added: list[BulkAddedRow]
|
|
1453
|
+
already_existed: list[str]
|
|
1454
|
+
invalid: list[BulkAddInvalidEntry]
|
|
1455
|
+
counts: BulkAddCounts
|
|
1456
|
+
|
|
1457
|
+
|
|
1458
|
+
class _DeleteSuppressionResponseRequired(TypedDict):
|
|
1459
|
+
status: str
|
|
1460
|
+
email: str
|
|
1461
|
+
reason: str
|
|
1462
|
+
source: str
|
|
1463
|
+
created_at: str
|
|
1464
|
+
|
|
1465
|
+
|
|
1466
|
+
class DeleteSuppressionResponse(_DeleteSuppressionResponseRequired, _HasPatternType):
|
|
1467
|
+
"""Response of DELETE /v1/suppressions/:email."""
|
|
1468
|
+
|
|
1469
|
+
|
|
1470
|
+
# === API Keys ===
|
|
1471
|
+
|
|
1472
|
+
class CreateApiKeyResponse(TypedDict):
|
|
1473
|
+
id: str
|
|
1474
|
+
api_key: str
|
|
1475
|
+
role: ApiKeyRole
|
|
1476
|
+
label: str | None
|
|
1477
|
+
mailbox_ids: list[str]
|
|
1478
|
+
|
|
1479
|
+
|
|
1480
|
+
class ApiKeySummary(TypedDict):
|
|
1481
|
+
id: str
|
|
1482
|
+
prefix: str
|
|
1483
|
+
status: str
|
|
1484
|
+
role: str | None
|
|
1485
|
+
label: str | None
|
|
1486
|
+
mailbox_ids: list[str]
|
|
1487
|
+
created_at: str
|
|
1488
|
+
last_used_at: str | None
|
|
1489
|
+
|
|
1490
|
+
|
|
1491
|
+
class ListApiKeysResponse(TypedDict):
|
|
1492
|
+
keys: list[ApiKeySummary]
|
|
1493
|
+
|
|
1494
|
+
|
|
1495
|
+
class RotateKeyResponse(TypedDict):
|
|
1496
|
+
api_key: str
|
|
1497
|
+
|
|
1498
|
+
|
|
1499
|
+
# === Account ===
|
|
1500
|
+
|
|
1501
|
+
StorageUsageState = Literal["normal", "warning", "near_full", "over_limit"]
|
|
1502
|
+
|
|
1503
|
+
|
|
1504
|
+
class UsageToday(TypedDict):
|
|
1505
|
+
count: int
|
|
1506
|
+
limit: int
|
|
1507
|
+
day: str
|
|
1508
|
+
|
|
1509
|
+
|
|
1510
|
+
class StorageUsageBreakdown(TypedDict):
|
|
1511
|
+
raw_mime_bytes: int
|
|
1512
|
+
derivative_bytes: int
|
|
1513
|
+
|
|
1514
|
+
|
|
1515
|
+
class StorageUsage(TypedDict):
|
|
1516
|
+
used_bytes: int
|
|
1517
|
+
limit_bytes: int | None
|
|
1518
|
+
percent_used: float | None
|
|
1519
|
+
state: StorageUsageState
|
|
1520
|
+
breakdown: StorageUsageBreakdown
|
|
1521
|
+
|
|
1522
|
+
|
|
1523
|
+
class UsageResponse(TypedDict, total=False):
|
|
1524
|
+
today: UsageToday
|
|
1525
|
+
history: list[dict[str, Any]]
|
|
1526
|
+
mailbox_count: int
|
|
1527
|
+
mailbox_limit: int
|
|
1528
|
+
storage: Required[StorageUsage]
|
|
1529
|
+
# PR 6 — REQUIRED. Mirrors the TS SDK + API schema: a missing field
|
|
1530
|
+
# surfaces as schema validation failure (500) on the server, never
|
|
1531
|
+
# silent zero. Other fields stay total=False (optional) for backwards
|
|
1532
|
+
# compat with consumers that relied on partial-dict semantics.
|
|
1533
|
+
pending_review_count: Required[int]
|
|
1534
|
+
rates: dict[str, float]
|
|
1535
|
+
health: dict[str, Any]
|
|
1536
|
+
trust: dict[str, Any]
|
|
1537
|
+
|
|
1538
|
+
|
|
1539
|
+
# Agent-accessible send-budget preflight (GET /v1/accounts/quota, RL-UAT-015/D6).
|
|
1540
|
+
# `today.limit` is the EFFECTIVE (trust-derived) daily cap, not the raw tier cap.
|
|
1541
|
+
# The `scope` discriminator disambiguates the two []-mailbox cases: "admin" with
|
|
1542
|
+
# [] => ALL mailboxes; "agent" with [] => a zero-bound agent key with NO send
|
|
1543
|
+
# capability.
|
|
1544
|
+
class QuotaToday(TypedDict):
|
|
1545
|
+
count: int
|
|
1546
|
+
limit: int
|
|
1547
|
+
day: str
|
|
1548
|
+
|
|
1549
|
+
|
|
1550
|
+
class QuotaResponse(TypedDict):
|
|
1551
|
+
today: QuotaToday
|
|
1552
|
+
sends_remaining: int
|
|
1553
|
+
reset_at: str
|
|
1554
|
+
scope: Literal["admin", "agent"]
|
|
1555
|
+
bound_mailbox_ids: list[str]
|
|
1556
|
+
|
|
1557
|
+
|
|
1558
|
+
# === Health ===
|
|
1559
|
+
|
|
1560
|
+
class ProviderHealth(TypedDict, total=False):
|
|
1561
|
+
healthy: bool
|
|
1562
|
+
configured: bool
|
|
1563
|
+
events_configured: bool
|
|
1564
|
+
|
|
1565
|
+
|
|
1566
|
+
class HealthResponse(TypedDict, total=False):
|
|
1567
|
+
status: Literal["ok", "degraded", "down"]
|
|
1568
|
+
database: bool
|
|
1569
|
+
providers: dict[str, ProviderHealth]
|
|
1570
|
+
kms: bool
|
|
1571
|
+
scanner: bool
|
|
1572
|
+
spam_filter: bool
|
|
1573
|
+
queue_depth: int
|
|
1574
|
+
timestamp: str
|
|
1575
|
+
# Additive capability tokens advertised by the server (e.g.
|
|
1576
|
+
# "messages.has_attachment_filter"). Absent on older servers — treat a
|
|
1577
|
+
# missing token as "feature unsupported".
|
|
1578
|
+
capabilities: list[str]
|