nornweave 0.1.2__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.
- nornweave/__init__.py +3 -0
- nornweave/adapters/__init__.py +1 -0
- nornweave/adapters/base.py +5 -0
- nornweave/adapters/mailgun.py +196 -0
- nornweave/adapters/resend.py +510 -0
- nornweave/adapters/sendgrid.py +492 -0
- nornweave/adapters/ses.py +824 -0
- nornweave/cli.py +186 -0
- nornweave/core/__init__.py +26 -0
- nornweave/core/config.py +172 -0
- nornweave/core/exceptions.py +25 -0
- nornweave/core/interfaces.py +390 -0
- nornweave/core/storage.py +192 -0
- nornweave/core/utils.py +23 -0
- nornweave/huginn/__init__.py +10 -0
- nornweave/huginn/client.py +296 -0
- nornweave/huginn/config.py +52 -0
- nornweave/huginn/resources.py +165 -0
- nornweave/huginn/server.py +202 -0
- nornweave/models/__init__.py +113 -0
- nornweave/models/attachment.py +136 -0
- nornweave/models/event.py +275 -0
- nornweave/models/inbox.py +33 -0
- nornweave/models/message.py +284 -0
- nornweave/models/thread.py +172 -0
- nornweave/muninn/__init__.py +14 -0
- nornweave/muninn/tools.py +207 -0
- nornweave/search/__init__.py +1 -0
- nornweave/search/embeddings.py +1 -0
- nornweave/search/vector_store.py +1 -0
- nornweave/skuld/__init__.py +1 -0
- nornweave/skuld/rate_limiter.py +1 -0
- nornweave/skuld/scheduler.py +1 -0
- nornweave/skuld/sender.py +25 -0
- nornweave/skuld/webhooks.py +1 -0
- nornweave/storage/__init__.py +20 -0
- nornweave/storage/database.py +165 -0
- nornweave/storage/gcs.py +144 -0
- nornweave/storage/local.py +152 -0
- nornweave/storage/s3.py +164 -0
- nornweave/urdr/__init__.py +14 -0
- nornweave/urdr/adapters/__init__.py +16 -0
- nornweave/urdr/adapters/base.py +385 -0
- nornweave/urdr/adapters/postgres.py +50 -0
- nornweave/urdr/adapters/sqlite.py +51 -0
- nornweave/urdr/migrations/env.py +94 -0
- nornweave/urdr/migrations/script.py.mako +26 -0
- nornweave/urdr/migrations/versions/.gitkeep +0 -0
- nornweave/urdr/migrations/versions/20260131_0001_initial_schema.py +182 -0
- nornweave/urdr/migrations/versions/20260131_0002_extended_schema.py +241 -0
- nornweave/urdr/orm.py +641 -0
- nornweave/verdandi/__init__.py +45 -0
- nornweave/verdandi/attachments.py +471 -0
- nornweave/verdandi/content.py +420 -0
- nornweave/verdandi/headers.py +404 -0
- nornweave/verdandi/parser.py +25 -0
- nornweave/verdandi/sanitizer.py +9 -0
- nornweave/verdandi/threading.py +359 -0
- nornweave/yggdrasil/__init__.py +1 -0
- nornweave/yggdrasil/app.py +86 -0
- nornweave/yggdrasil/dependencies.py +190 -0
- nornweave/yggdrasil/middleware/__init__.py +1 -0
- nornweave/yggdrasil/middleware/auth.py +1 -0
- nornweave/yggdrasil/middleware/logging.py +1 -0
- nornweave/yggdrasil/routes/__init__.py +1 -0
- nornweave/yggdrasil/routes/v1/__init__.py +1 -0
- nornweave/yggdrasil/routes/v1/inboxes.py +124 -0
- nornweave/yggdrasil/routes/v1/messages.py +200 -0
- nornweave/yggdrasil/routes/v1/search.py +84 -0
- nornweave/yggdrasil/routes/v1/threads.py +142 -0
- nornweave/yggdrasil/routes/webhooks/__init__.py +1 -0
- nornweave/yggdrasil/routes/webhooks/mailgun.py +136 -0
- nornweave/yggdrasil/routes/webhooks/resend.py +344 -0
- nornweave/yggdrasil/routes/webhooks/sendgrid.py +15 -0
- nornweave/yggdrasil/routes/webhooks/ses.py +15 -0
- nornweave-0.1.2.dist-info/METADATA +324 -0
- nornweave-0.1.2.dist-info/RECORD +80 -0
- nornweave-0.1.2.dist-info/WHEEL +4 -0
- nornweave-0.1.2.dist-info/entry_points.txt +5 -0
- nornweave-0.1.2.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""Event models for webhooks.
|
|
2
|
+
|
|
3
|
+
Event models for webhook notifications and internal event tracking.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from nornweave.models.message import Message
|
|
14
|
+
from nornweave.models.thread import ThreadItem
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EventType(str, Enum):
|
|
18
|
+
"""
|
|
19
|
+
Event types for webhooks.
|
|
20
|
+
|
|
21
|
+
Supported event types for webhooks and notifications.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
MESSAGE_RECEIVED = "message.received"
|
|
25
|
+
MESSAGE_SENT = "message.sent"
|
|
26
|
+
MESSAGE_DELIVERED = "message.delivered"
|
|
27
|
+
MESSAGE_BOUNCED = "message.bounced"
|
|
28
|
+
MESSAGE_COMPLAINED = "message.complained"
|
|
29
|
+
MESSAGE_REJECTED = "message.rejected"
|
|
30
|
+
DOMAIN_VERIFIED = "domain.verified"
|
|
31
|
+
# Legacy event types for backwards compatibility
|
|
32
|
+
THREAD_NEW_MESSAGE = "thread.new_message"
|
|
33
|
+
INBOX_CREATED = "inbox.created"
|
|
34
|
+
INBOX_DELETED = "inbox.deleted"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Recipient(BaseModel):
|
|
38
|
+
"""
|
|
39
|
+
Recipient with delivery status.
|
|
40
|
+
|
|
41
|
+
Recipient with delivery status information.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
address: str = Field(..., description="Recipient address")
|
|
45
|
+
status: str = Field(..., description="Recipient status")
|
|
46
|
+
|
|
47
|
+
model_config = {"extra": "forbid"}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class SendEvent(BaseModel):
|
|
51
|
+
"""
|
|
52
|
+
Event data for message sent.
|
|
53
|
+
|
|
54
|
+
Send event payload with recipient list.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
inbox_id: str
|
|
58
|
+
thread_id: str
|
|
59
|
+
message_id: str
|
|
60
|
+
timestamp: datetime
|
|
61
|
+
recipients: list[str] = Field(..., description="Sent recipients")
|
|
62
|
+
|
|
63
|
+
model_config = {"extra": "forbid"}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class DeliveryEvent(BaseModel):
|
|
67
|
+
"""
|
|
68
|
+
Event data for message delivered.
|
|
69
|
+
|
|
70
|
+
Delivery confirmation event payload.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
inbox_id: str
|
|
74
|
+
thread_id: str
|
|
75
|
+
message_id: str
|
|
76
|
+
timestamp: datetime
|
|
77
|
+
recipients: list[str] = Field(..., description="Delivered recipients")
|
|
78
|
+
|
|
79
|
+
model_config = {"extra": "forbid"}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class BounceEvent(BaseModel):
|
|
83
|
+
"""
|
|
84
|
+
Event data for message bounced.
|
|
85
|
+
|
|
86
|
+
Bounce event payload with type and affected recipients.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
inbox_id: str
|
|
90
|
+
thread_id: str
|
|
91
|
+
message_id: str
|
|
92
|
+
timestamp: datetime
|
|
93
|
+
type: str = Field(..., description="Bounce type (hard/soft)")
|
|
94
|
+
sub_type: str = Field(..., description="Bounce sub-type")
|
|
95
|
+
recipients: list[Recipient] = Field(..., description="Bounced recipients with status")
|
|
96
|
+
|
|
97
|
+
model_config = {"extra": "forbid"}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class ComplaintEvent(BaseModel):
|
|
101
|
+
"""
|
|
102
|
+
Event data for spam complaint.
|
|
103
|
+
|
|
104
|
+
Spam complaint event payload.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
inbox_id: str
|
|
108
|
+
thread_id: str
|
|
109
|
+
message_id: str
|
|
110
|
+
timestamp: datetime
|
|
111
|
+
type: str = Field(..., description="Complaint type")
|
|
112
|
+
sub_type: str = Field(..., description="Complaint sub-type")
|
|
113
|
+
recipients: list[str] = Field(..., description="Complained recipients")
|
|
114
|
+
|
|
115
|
+
model_config = {"extra": "forbid"}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class RejectEvent(BaseModel):
|
|
119
|
+
"""
|
|
120
|
+
Event data for message rejected.
|
|
121
|
+
|
|
122
|
+
Rejection event payload with reason.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
inbox_id: str
|
|
126
|
+
thread_id: str
|
|
127
|
+
message_id: str
|
|
128
|
+
timestamp: datetime
|
|
129
|
+
reason: str = Field(..., description="Reject reason")
|
|
130
|
+
|
|
131
|
+
model_config = {"extra": "forbid"}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class MessageReceivedEvent(BaseModel):
|
|
135
|
+
"""
|
|
136
|
+
Webhook event for message received.
|
|
137
|
+
|
|
138
|
+
Webhook payload for inbound message received.
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
type: Literal["event"] = "event"
|
|
142
|
+
event_type: Literal["message.received"] = "message.received"
|
|
143
|
+
event_id: str
|
|
144
|
+
message: Message
|
|
145
|
+
thread: ThreadItem
|
|
146
|
+
|
|
147
|
+
model_config = {"extra": "forbid"}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class MessageSentEvent(BaseModel):
|
|
151
|
+
"""
|
|
152
|
+
Webhook event for message sent.
|
|
153
|
+
|
|
154
|
+
Webhook payload for message sent.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
type: Literal["event"] = "event"
|
|
158
|
+
event_type: Literal["message.sent"] = "message.sent"
|
|
159
|
+
event_id: str
|
|
160
|
+
send: SendEvent
|
|
161
|
+
|
|
162
|
+
model_config = {"extra": "forbid"}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class MessageDeliveredEvent(BaseModel):
|
|
166
|
+
"""
|
|
167
|
+
Webhook event for message delivered.
|
|
168
|
+
|
|
169
|
+
Webhook payload for message delivered.
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
type: Literal["event"] = "event"
|
|
173
|
+
event_type: Literal["message.delivered"] = "message.delivered"
|
|
174
|
+
event_id: str
|
|
175
|
+
delivery: DeliveryEvent
|
|
176
|
+
|
|
177
|
+
model_config = {"extra": "forbid"}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class MessageBouncedEvent(BaseModel):
|
|
181
|
+
"""
|
|
182
|
+
Webhook event for message bounced.
|
|
183
|
+
|
|
184
|
+
Webhook payload for message bounced.
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
type: Literal["event"] = "event"
|
|
188
|
+
event_type: Literal["message.bounced"] = "message.bounced"
|
|
189
|
+
event_id: str
|
|
190
|
+
bounce: BounceEvent
|
|
191
|
+
|
|
192
|
+
model_config = {"extra": "forbid"}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class MessageComplainedEvent(BaseModel):
|
|
196
|
+
"""
|
|
197
|
+
Webhook event for spam complaint.
|
|
198
|
+
|
|
199
|
+
Webhook payload for spam complaint.
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
type: Literal["event"] = "event"
|
|
203
|
+
event_type: Literal["message.complained"] = "message.complained"
|
|
204
|
+
event_id: str
|
|
205
|
+
complaint: ComplaintEvent
|
|
206
|
+
|
|
207
|
+
model_config = {"extra": "forbid"}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class MessageRejectedEvent(BaseModel):
|
|
211
|
+
"""
|
|
212
|
+
Webhook event for message rejected.
|
|
213
|
+
|
|
214
|
+
Webhook payload for message rejected.
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
type: Literal["event"] = "event"
|
|
218
|
+
event_type: Literal["message.rejected"] = "message.rejected"
|
|
219
|
+
event_id: str
|
|
220
|
+
reject: RejectEvent
|
|
221
|
+
|
|
222
|
+
model_config = {"extra": "forbid"}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# Type alias for all webhook events
|
|
226
|
+
WebhookEvent = (
|
|
227
|
+
MessageReceivedEvent
|
|
228
|
+
| MessageSentEvent
|
|
229
|
+
| MessageDeliveredEvent
|
|
230
|
+
| MessageBouncedEvent
|
|
231
|
+
| MessageComplainedEvent
|
|
232
|
+
| MessageRejectedEvent
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# Legacy models for backwards compatibility
|
|
237
|
+
class EventCreate(BaseModel):
|
|
238
|
+
"""Payload to create an event (id and created_at set by storage)."""
|
|
239
|
+
|
|
240
|
+
type: EventType = Field(..., description="Event type")
|
|
241
|
+
payload: dict[str, Any] = Field(
|
|
242
|
+
default_factory=dict,
|
|
243
|
+
description="Event payload",
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
model_config = {"extra": "forbid"}
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class Event(BaseModel):
|
|
250
|
+
"""Event entity with id and timestamp."""
|
|
251
|
+
|
|
252
|
+
id: str = Field(..., description="Event id")
|
|
253
|
+
type: EventType = Field(..., description="Event type")
|
|
254
|
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
255
|
+
payload: dict[str, Any] = Field(
|
|
256
|
+
default_factory=dict,
|
|
257
|
+
description="Event payload",
|
|
258
|
+
)
|
|
259
|
+
# Additional fields for structured events
|
|
260
|
+
inbox_id: str | None = Field(None, description="Associated inbox ID")
|
|
261
|
+
thread_id: str | None = Field(None, description="Associated thread ID")
|
|
262
|
+
message_id: str | None = Field(None, description="Associated message ID")
|
|
263
|
+
|
|
264
|
+
model_config = {"extra": "forbid"}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class ListEventsResponse(BaseModel):
|
|
268
|
+
"""Response for listing events."""
|
|
269
|
+
|
|
270
|
+
count: int = Field(..., description="Total count of events")
|
|
271
|
+
limit: int | None = Field(None, description="Limit applied to results")
|
|
272
|
+
next_page_token: str | None = Field(None, description="Token for next page")
|
|
273
|
+
events: list[Event] = Field(..., description="Events ordered by timestamp descending")
|
|
274
|
+
|
|
275
|
+
model_config = {"extra": "forbid"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Inbox model."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class InboxBase(BaseModel):
|
|
9
|
+
"""Shared inbox fields."""
|
|
10
|
+
|
|
11
|
+
email_address: str = Field(..., description="Full email address for this inbox")
|
|
12
|
+
name: str | None = Field(None, description="Human-readable name")
|
|
13
|
+
provider_config: dict[str, Any] = Field(
|
|
14
|
+
default_factory=dict,
|
|
15
|
+
description="Provider-specific metadata (e.g. route_id, domain, webhook_id)",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class InboxCreate(BaseModel):
|
|
20
|
+
"""Payload to create an inbox."""
|
|
21
|
+
|
|
22
|
+
name: str = Field(..., min_length=1, description="Display name")
|
|
23
|
+
email_username: str = Field(..., min_length=1, description="Local part (e.g. support)")
|
|
24
|
+
|
|
25
|
+
model_config = {"extra": "forbid"}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Inbox(InboxBase):
|
|
29
|
+
"""Inbox entity with id."""
|
|
30
|
+
|
|
31
|
+
id: str = Field(..., description="Unique inbox id (e.g. UUID)")
|
|
32
|
+
|
|
33
|
+
model_config = {"extra": "forbid"}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""Message models.
|
|
2
|
+
|
|
3
|
+
Message models for email content and metadata.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from datetime import datetime # noqa: TC003 - needed at runtime for Pydantic
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from nornweave.models.attachment import Attachment, AttachmentMeta, SendAttachment
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MessageDirection(str, Enum):
|
|
17
|
+
"""Message direction."""
|
|
18
|
+
|
|
19
|
+
INBOUND = "inbound"
|
|
20
|
+
OUTBOUND = "outbound"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class MessageItem(BaseModel):
|
|
24
|
+
"""
|
|
25
|
+
Message summary for list views (without full body content).
|
|
26
|
+
|
|
27
|
+
Summary model for message list views.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
inbox_id: str = Field(..., description="ID of inbox")
|
|
31
|
+
thread_id: str = Field(..., description="ID of thread")
|
|
32
|
+
message_id: str = Field(..., description="ID of message")
|
|
33
|
+
labels: list[str] = Field(default_factory=list, description="Labels of message")
|
|
34
|
+
timestamp: datetime = Field(..., description="Time at which message was sent or drafted")
|
|
35
|
+
from_address: str = Field(..., alias="from", description="Sender address")
|
|
36
|
+
to: list[str] = Field(..., description="Recipient addresses")
|
|
37
|
+
cc: list[str] | None = Field(None, description="CC recipient addresses")
|
|
38
|
+
bcc: list[str] | None = Field(None, description="BCC recipient addresses")
|
|
39
|
+
subject: str | None = Field(None, description="Subject of message")
|
|
40
|
+
preview: str | None = Field(None, description="Text preview of message")
|
|
41
|
+
attachments: list[AttachmentMeta] | None = Field(None, description="Attachments in message")
|
|
42
|
+
in_reply_to: str | None = Field(None, description="Message-ID of message being replied to")
|
|
43
|
+
references: list[str] | None = Field(
|
|
44
|
+
None, description="Message-IDs of previous messages in thread"
|
|
45
|
+
)
|
|
46
|
+
headers: dict[str, str] | None = Field(None, description="Headers in message")
|
|
47
|
+
size: int = Field(..., description="Size of message in bytes")
|
|
48
|
+
updated_at: datetime = Field(..., description="Time at which message was last updated")
|
|
49
|
+
created_at: datetime = Field(..., description="Time at which message was created")
|
|
50
|
+
|
|
51
|
+
model_config = {"extra": "forbid", "populate_by_name": True}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Message(BaseModel):
|
|
55
|
+
"""
|
|
56
|
+
Full message model with body content.
|
|
57
|
+
|
|
58
|
+
Complete message model with body content.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
inbox_id: str = Field(..., description="ID of inbox")
|
|
62
|
+
thread_id: str = Field(..., description="ID of thread")
|
|
63
|
+
message_id: str = Field(..., alias="id", description="ID of message")
|
|
64
|
+
labels: list[str] = Field(default_factory=list, description="Labels of message")
|
|
65
|
+
timestamp: datetime | None = Field(
|
|
66
|
+
None, description="Time at which message was sent or drafted"
|
|
67
|
+
)
|
|
68
|
+
from_address: str | None = Field(None, alias="from", description="Sender address")
|
|
69
|
+
reply_to: list[str] | None = Field(None, description="Reply-to addresses")
|
|
70
|
+
to: list[str] = Field(default_factory=list, description="Recipient addresses")
|
|
71
|
+
cc: list[str] | None = Field(None, description="CC recipient addresses")
|
|
72
|
+
bcc: list[str] | None = Field(None, description="BCC recipient addresses")
|
|
73
|
+
subject: str | None = Field(None, description="Subject of message")
|
|
74
|
+
preview: str | None = Field(None, description="Text preview of message")
|
|
75
|
+
text: str | None = Field(None, alias="content_raw", description="Plain text body of message")
|
|
76
|
+
html: str | None = Field(None, description="HTML body of message")
|
|
77
|
+
extracted_text: str | None = Field(
|
|
78
|
+
None,
|
|
79
|
+
alias="content_clean",
|
|
80
|
+
description="Extracted new text content (without quoted replies)",
|
|
81
|
+
)
|
|
82
|
+
extracted_html: str | None = Field(
|
|
83
|
+
None, description="Extracted new HTML content (without quoted replies)"
|
|
84
|
+
)
|
|
85
|
+
attachments: list[Attachment] | None = Field(None, description="Attachments in message")
|
|
86
|
+
in_reply_to: str | None = Field(None, description="Message-ID of message being replied to")
|
|
87
|
+
references: list[str] | None = Field(
|
|
88
|
+
None, description="Message-IDs of previous messages in thread"
|
|
89
|
+
)
|
|
90
|
+
headers: dict[str, str] | None = Field(
|
|
91
|
+
None, alias="metadata", description="All headers in message"
|
|
92
|
+
)
|
|
93
|
+
size: int = Field(0, description="Size of message in bytes")
|
|
94
|
+
direction: MessageDirection = Field(
|
|
95
|
+
default=MessageDirection.INBOUND, description="Inbound or outbound"
|
|
96
|
+
)
|
|
97
|
+
provider_message_id: str | None = Field(None, description="Provider's Message-ID header")
|
|
98
|
+
updated_at: datetime | None = Field(None, description="Time at which message was last updated")
|
|
99
|
+
created_at: datetime | None = Field(None, description="Time at which message was created")
|
|
100
|
+
|
|
101
|
+
model_config = {"populate_by_name": True}
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def id(self) -> str:
|
|
105
|
+
"""Alias for message_id for compatibility."""
|
|
106
|
+
return self.message_id
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def content_raw(self) -> str | None:
|
|
110
|
+
"""Alias for text for backwards compatibility."""
|
|
111
|
+
return self.text
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def content_clean(self) -> str | None:
|
|
115
|
+
"""Alias for extracted_text for backwards compatibility."""
|
|
116
|
+
return self.extracted_text
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def metadata(self) -> dict[str, str] | None:
|
|
120
|
+
"""Alias for headers for backwards compatibility."""
|
|
121
|
+
return self.headers
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class ListMessagesResponse(BaseModel):
|
|
125
|
+
"""
|
|
126
|
+
Response for listing messages.
|
|
127
|
+
|
|
128
|
+
Paginated response for message listing.
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
count: int = Field(..., description="Total count of messages")
|
|
132
|
+
limit: int | None = Field(None, description="Limit applied to results")
|
|
133
|
+
next_page_token: str | None = Field(None, description="Token for next page")
|
|
134
|
+
messages: list[MessageItem] = Field(..., description="Messages ordered by timestamp descending")
|
|
135
|
+
|
|
136
|
+
model_config = {"extra": "forbid"}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class SendMessageRequest(BaseModel):
|
|
140
|
+
"""
|
|
141
|
+
Request to send a new message.
|
|
142
|
+
|
|
143
|
+
Request payload for sending a new message.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
labels: list[str] | None = None
|
|
147
|
+
reply_to: str | list[str] | None = Field(None, description="Reply-to address(es)")
|
|
148
|
+
to: str | list[str] | None = Field(None, description="Recipient address(es)")
|
|
149
|
+
cc: str | list[str] | None = Field(None, description="CC address(es)")
|
|
150
|
+
bcc: str | list[str] | None = Field(None, description="BCC address(es)")
|
|
151
|
+
subject: str | None = None
|
|
152
|
+
text: str | None = Field(None, description="Plain text body")
|
|
153
|
+
html: str | None = Field(None, description="HTML body")
|
|
154
|
+
attachments: list[SendAttachment] | None = None
|
|
155
|
+
headers: dict[str, str] | None = None
|
|
156
|
+
|
|
157
|
+
model_config = {"extra": "forbid"}
|
|
158
|
+
|
|
159
|
+
def get_to_list(self) -> list[str]:
|
|
160
|
+
"""Get 'to' as a list."""
|
|
161
|
+
if self.to is None:
|
|
162
|
+
return []
|
|
163
|
+
return [self.to] if isinstance(self.to, str) else list(self.to)
|
|
164
|
+
|
|
165
|
+
def get_cc_list(self) -> list[str]:
|
|
166
|
+
"""Get 'cc' as a list."""
|
|
167
|
+
if self.cc is None:
|
|
168
|
+
return []
|
|
169
|
+
return [self.cc] if isinstance(self.cc, str) else list(self.cc)
|
|
170
|
+
|
|
171
|
+
def get_bcc_list(self) -> list[str]:
|
|
172
|
+
"""Get 'bcc' as a list."""
|
|
173
|
+
if self.bcc is None:
|
|
174
|
+
return []
|
|
175
|
+
return [self.bcc] if isinstance(self.bcc, str) else list(self.bcc)
|
|
176
|
+
|
|
177
|
+
def get_reply_to_list(self) -> list[str]:
|
|
178
|
+
"""Get 'reply_to' as a list."""
|
|
179
|
+
if self.reply_to is None:
|
|
180
|
+
return []
|
|
181
|
+
return [self.reply_to] if isinstance(self.reply_to, str) else list(self.reply_to)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class SendMessageResponse(BaseModel):
|
|
185
|
+
"""
|
|
186
|
+
Response after sending a message.
|
|
187
|
+
|
|
188
|
+
Response after sending a message.
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
message_id: str
|
|
192
|
+
thread_id: str
|
|
193
|
+
|
|
194
|
+
model_config = {"extra": "forbid"}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class ReplyToMessageRequest(BaseModel):
|
|
198
|
+
"""
|
|
199
|
+
Request to reply to a specific message.
|
|
200
|
+
|
|
201
|
+
Request payload for replying to a message.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
labels: list[str] | None = None
|
|
205
|
+
reply_to: str | list[str] | None = None
|
|
206
|
+
to: str | list[str] | None = Field(None, description="Override recipients")
|
|
207
|
+
cc: str | list[str] | None = None
|
|
208
|
+
bcc: str | list[str] | None = None
|
|
209
|
+
reply_all: bool | None = Field(
|
|
210
|
+
None, description="Reply to all recipients of the original message"
|
|
211
|
+
)
|
|
212
|
+
text: str | None = None
|
|
213
|
+
html: str | None = None
|
|
214
|
+
attachments: list[SendAttachment] | None = None
|
|
215
|
+
headers: dict[str, str] | None = None
|
|
216
|
+
|
|
217
|
+
model_config = {"extra": "forbid"}
|
|
218
|
+
|
|
219
|
+
def get_to_list(self) -> list[str]:
|
|
220
|
+
"""Get 'to' as a list."""
|
|
221
|
+
if self.to is None:
|
|
222
|
+
return []
|
|
223
|
+
return [self.to] if isinstance(self.to, str) else list(self.to)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class UpdateMessageRequest(BaseModel):
|
|
227
|
+
"""
|
|
228
|
+
Request to update message labels.
|
|
229
|
+
|
|
230
|
+
Request payload for updating message labels.
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
add_labels: list[str] | None = Field(None, description="Labels to add to message")
|
|
234
|
+
remove_labels: list[str] | None = Field(None, description="Labels to remove from message")
|
|
235
|
+
|
|
236
|
+
model_config = {"extra": "forbid"}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# Legacy compatibility models (for existing codebase)
|
|
240
|
+
class MessageBase(BaseModel):
|
|
241
|
+
"""Shared message fields (legacy compatibility)."""
|
|
242
|
+
|
|
243
|
+
thread_id: str = Field(..., description="Thread id")
|
|
244
|
+
inbox_id: str = Field(..., description="Inbox id")
|
|
245
|
+
provider_message_id: str | None = Field(None, description="Provider Message-ID header")
|
|
246
|
+
direction: MessageDirection = Field(..., description="Inbound or outbound")
|
|
247
|
+
content_raw: str = Field("", description="Original HTML/plain text")
|
|
248
|
+
content_clean: str = Field("", description="LLM-ready Markdown")
|
|
249
|
+
metadata: dict[str, Any] = Field(
|
|
250
|
+
default_factory=dict,
|
|
251
|
+
description="Headers, token counts, sentiment, etc.",
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class MessageCreate(BaseModel):
|
|
256
|
+
"""Payload to send a message (outbound via API)."""
|
|
257
|
+
|
|
258
|
+
inbox_id: str
|
|
259
|
+
to: list[str] = Field(..., min_length=1)
|
|
260
|
+
subject: str = Field(..., min_length=1)
|
|
261
|
+
body: str = Field(..., description="Markdown body")
|
|
262
|
+
reply_to_thread_id: str | None = None
|
|
263
|
+
cc: list[str] | None = None
|
|
264
|
+
bcc: list[str] | None = None
|
|
265
|
+
attachments: list[SendAttachment] | None = None
|
|
266
|
+
|
|
267
|
+
model_config = {"extra": "forbid"}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class MessageInCreate(BaseModel):
|
|
271
|
+
"""Payload to create a message on ingestion (inbound webhook)."""
|
|
272
|
+
|
|
273
|
+
thread_id: str = Field(..., description="Thread id")
|
|
274
|
+
inbox_id: str = Field(..., description="Inbox id")
|
|
275
|
+
provider_message_id: str | None = Field(None, description="Provider Message-ID header")
|
|
276
|
+
direction: MessageDirection = Field(..., description="Inbound or outbound")
|
|
277
|
+
content_raw: str = Field("", description="Original HTML/plain text")
|
|
278
|
+
content_clean: str = Field("", description="LLM-ready Markdown")
|
|
279
|
+
metadata: dict[str, Any] = Field(
|
|
280
|
+
default_factory=dict,
|
|
281
|
+
description="Headers, token counts, sentiment, etc.",
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
model_config = {"extra": "forbid"}
|