commune-mail 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
@@ -0,0 +1,577 @@
1
+ Metadata-Version: 2.4
2
+ Name: commune-mail
3
+ Version: 0.1.0
4
+ Summary: Python SDK for Commune – email infrastructure for agents. Threads, inboxes, domains, attachments, and sending.
5
+ Project-URL: Homepage, https://github.com/commune-ai/commune
6
+ Project-URL: Documentation, https://docs.commune.sh
7
+ Project-URL: Repository, https://github.com/commune-ai/commune
8
+ Author-email: Commune <hello@commune.sh>
9
+ License-Expression: MIT
10
+ Keywords: agent,ai,api,email,inbox,sdk,threads
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Communications :: Email
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.9
22
+ Requires-Dist: httpx>=0.25.0
23
+ Requires-Dist: pydantic>=2.0.0
24
+ Description-Content-Type: text/markdown
25
+
26
+ # Commune Python SDK
27
+
28
+ Python SDK for [Commune](https://commune.sh) — email infrastructure for AI agents.
29
+
30
+ ```bash
31
+ pip install commune-ai
32
+ ```
33
+
34
+ ---
35
+
36
+ ## Quickstart
37
+
38
+ From zero to a working email agent in 4 lines — no domain setup, no DNS:
39
+
40
+ ```python
41
+ from commune import CommuneClient
42
+
43
+ client = CommuneClient(api_key="comm_...")
44
+
45
+ # Create an inbox — domain is auto-assigned
46
+ inbox = client.inboxes.create(local_part="support")
47
+ print(f"Inbox ready: {inbox.address}") # → "support@agents.postking.io"
48
+
49
+ # List email threads
50
+ threads = client.threads.list(inbox_id=inbox.id, limit=5)
51
+ for t in threads.data:
52
+ print(f" [{t.message_count} msgs] {t.subject}")
53
+
54
+ # Send an email
55
+ client.messages.send(
56
+ to="user@example.com",
57
+ subject="Hello from my agent",
58
+ text="Hi there!",
59
+ )
60
+ ```
61
+
62
+ That's it. No domain verification, no DNS records. Just create an inbox and start sending/receiving.
63
+
64
+ ---
65
+
66
+ ## Concepts
67
+
68
+ Commune organizes email around four layers:
69
+
70
+ ```
71
+ Domain → Inbox → Thread → Message
72
+ ```
73
+
74
+ - **Domain** — A custom email domain you own (e.g. `example.com`). You verify it by adding DNS records.
75
+ - **Inbox** — A mailbox under a domain (e.g. `support@example.com`). Each inbox can have webhooks for real-time notifications.
76
+ - **Thread** — A conversation: a group of related messages sharing a subject/reply chain. Called `conversation_id` internally, exposed as `thread_id` in the SDK.
77
+ - **Message** — A single email (inbound or outbound) within a thread.
78
+
79
+ ---
80
+
81
+ ## Client
82
+
83
+ ```python
84
+ from commune import CommuneClient
85
+
86
+ client = CommuneClient(
87
+ api_key="comm_...", # Required. Your API key.
88
+ base_url=None, # Optional. Override API URL.
89
+ timeout=30.0, # Optional. Request timeout in seconds.
90
+ )
91
+ ```
92
+
93
+ Supports context manager:
94
+
95
+ ```python
96
+ with CommuneClient(api_key="comm_...") as client:
97
+ domains = client.domains.list()
98
+ # Connection closed automatically
99
+ ```
100
+
101
+ ---
102
+
103
+ ## Domains
104
+
105
+ Domains are the foundation. You register a domain, add DNS records, verify it, then create inboxes under it.
106
+
107
+ ### `client.domains.list()`
108
+
109
+ List all domains in your organization.
110
+
111
+ ```python
112
+ domains = client.domains.list()
113
+ # → [Domain(id="d_abc123", name="example.com", status="verified", ...)]
114
+ ```
115
+
116
+ **Returns:** `list[Domain]`
117
+
118
+ | Field | Type | Description |
119
+ |-------|------|-------------|
120
+ | `id` | `str` | Domain ID |
121
+ | `name` | `str` | Domain name |
122
+ | `status` | `str` | `"not_started"`, `"pending"`, `"verified"`, `"failed"` |
123
+ | `region` | `str` | AWS region |
124
+ | `records` | `list` | DNS records (MX, TXT, CNAME) |
125
+ | `inboxes` | `list[Inbox]` | Inboxes under this domain |
126
+
127
+ ### `client.domains.create(name, region=None)`
128
+
129
+ Register a new domain. After creating, you'll need to verify it.
130
+
131
+ ```python
132
+ domain = client.domains.create(name="example.com")
133
+ print(domain.id) # → "d_abc123"
134
+ print(domain.status) # → "not_started"
135
+ ```
136
+
137
+ | Parameter | Type | Required | Description |
138
+ |-----------|------|----------|-------------|
139
+ | `name` | `str` | Yes | Domain name (e.g. `"example.com"`) |
140
+ | `region` | `str` | No | AWS region (e.g. `"us-east-1"`) |
141
+
142
+ ### `client.domains.get(domain_id)`
143
+
144
+ Get full details for a single domain.
145
+
146
+ ```python
147
+ domain = client.domains.get("d_abc123")
148
+ ```
149
+
150
+ ### `client.domains.records(domain_id)`
151
+
152
+ Get the DNS records you need to add at your registrar.
153
+
154
+ ```python
155
+ records = client.domains.records("d_abc123")
156
+ for r in records:
157
+ print(f" {r['type']} {r['name']} → {r['value']}")
158
+ ```
159
+
160
+ **Returns:** `list[dict]` — each record has `type`, `name`, `value`, `status`, `ttl`.
161
+
162
+ ### `client.domains.verify(domain_id)`
163
+
164
+ Trigger verification after you've added the DNS records.
165
+
166
+ ```python
167
+ result = client.domains.verify("d_abc123")
168
+ ```
169
+
170
+ ### Typical flow
171
+
172
+ ```python
173
+ # 1. Create the domain
174
+ domain = client.domains.create(name="example.com")
175
+
176
+ # 2. Get DNS records to configure
177
+ records = client.domains.records(domain.id)
178
+ print("Add these DNS records at your registrar:")
179
+ for r in records:
180
+ print(f" {r['type']} {r['name']} → {r['value']}")
181
+
182
+ # 3. After adding records, verify
183
+ result = client.domains.verify(domain.id)
184
+
185
+ # 4. Check status
186
+ domain = client.domains.get(domain.id)
187
+ print(f"Status: {domain.status}") # → "verified"
188
+ ```
189
+
190
+ ---
191
+
192
+ ## Inboxes
193
+
194
+ Inboxes are mailboxes that receive and send email. Create one with just a `local_part` — the domain is auto-assigned.
195
+
196
+ ### `client.inboxes.create(local_part, *, domain_id=None, name=None, webhook=None)`
197
+
198
+ Create a new inbox. Domain is **auto-resolved** if not provided — no DNS setup needed.
199
+
200
+ ```python
201
+ # Simplest — domain auto-assigned
202
+ inbox = client.inboxes.create(local_part="support")
203
+ print(inbox.address) # → "support@agents.postking.io"
204
+
205
+ # Explicit domain (if you have a custom domain)
206
+ inbox = client.inboxes.create(local_part="billing", domain_id="d_abc123")
207
+ ```
208
+
209
+ | Parameter | Type | Required | Description |
210
+ |-----------|------|----------|-------------|
211
+ | `local_part` | `str` | Yes | Part before `@` (e.g. `"support"`, `"billing"`) |
212
+ | `domain_id` | `str` | No | Domain to create under. Auto-resolved if omitted. |
213
+ | `name` | `str` | No | Display name |
214
+ | `webhook` | `dict` | No | `{"endpoint": "https://...", "events": ["inbound"]}` |
215
+
216
+ **Returns:** `Inbox`
217
+
218
+ | Field | Type | Description |
219
+ |-------|------|-------------|
220
+ | `id` | `str` | Inbox ID |
221
+ | `local_part` | `str` | Part before `@` |
222
+ | `address` | `str` | Full email address |
223
+ | `webhook` | `InboxWebhook \| str \| None` | Webhook configuration |
224
+ | `status` | `str \| None` | Inbox status |
225
+ | `created_at` | `str \| None` | ISO timestamp |
226
+
227
+ ### `client.inboxes.list(domain_id=None)`
228
+
229
+ List inboxes. Without `domain_id`, lists all inboxes across all domains.
230
+
231
+ ```python
232
+ # All inboxes
233
+ inboxes = client.inboxes.list()
234
+
235
+ # Inboxes for a specific domain
236
+ inboxes = client.inboxes.list(domain_id="d_abc123")
237
+ ```
238
+
239
+ ### `client.inboxes.get(domain_id, inbox_id)`
240
+
241
+ ```python
242
+ inbox = client.inboxes.get("d_abc123", "i_xyz")
243
+ ```
244
+
245
+ ### `client.inboxes.update(domain_id, inbox_id, **fields)`
246
+
247
+ Update one or more fields. Only provided fields are changed.
248
+
249
+ ```python
250
+ inbox = client.inboxes.update("d_abc123", "i_xyz", local_part="help")
251
+ ```
252
+
253
+ ### `client.inboxes.set_webhook(domain_id, inbox_id, *, endpoint, events=None)`
254
+
255
+ Shortcut to set a webhook. You'll receive a POST when emails arrive.
256
+
257
+ ```python
258
+ client.inboxes.set_webhook(
259
+ "d_abc123", "i_xyz",
260
+ endpoint="https://your-app.com/webhook",
261
+ events=["inbound"],
262
+ )
263
+ ```
264
+
265
+ ### `client.inboxes.remove(domain_id, inbox_id)`
266
+
267
+ Delete an inbox permanently.
268
+
269
+ ```python
270
+ client.inboxes.remove("d_abc123", "i_xyz") # → True
271
+ ```
272
+
273
+ ---
274
+
275
+ ## Threads
276
+
277
+ A thread is a conversation — a group of related email messages. Threads are listed with **cursor-based pagination** for efficient browsing of large mailboxes.
278
+
279
+ ### `client.threads.list(*, inbox_id=None, domain_id=None, limit=20, cursor=None, order="desc")`
280
+
281
+ List threads for an inbox or domain. Returns newest first by default.
282
+
283
+ ```python
284
+ result = client.threads.list(inbox_id="i_xyz", limit=10)
285
+
286
+ for thread in result.data:
287
+ print(f"[{thread.message_count} msgs] {thread.subject}")
288
+ print(f" Last activity: {thread.last_message_at}")
289
+ print(f" Preview: {thread.snippet}")
290
+
291
+ # Paginate
292
+ if result.has_more:
293
+ page2 = client.threads.list(inbox_id="i_xyz", cursor=result.next_cursor)
294
+ ```
295
+
296
+ | Parameter | Type | Required | Description |
297
+ |-----------|------|----------|-------------|
298
+ | `inbox_id` | `str` | One of these | Filter by inbox |
299
+ | `domain_id` | `str` | required | Filter by domain |
300
+ | `limit` | `int` | No | 1–100, default 20 |
301
+ | `cursor` | `str` | No | Cursor from previous `next_cursor` |
302
+ | `order` | `str` | No | `"desc"` (newest first) or `"asc"` |
303
+
304
+ **Returns:** `ThreadList`
305
+
306
+ ```python
307
+ ThreadList(
308
+ data=[Thread(...)], # List of thread summaries
309
+ next_cursor="abc...", # Pass to next call for next page (None if no more)
310
+ has_more=True, # Whether more pages exist
311
+ )
312
+ ```
313
+
314
+ **Thread object:**
315
+
316
+ | Field | Type | Description |
317
+ |-------|------|-------------|
318
+ | `thread_id` | `str` | Thread identifier |
319
+ | `subject` | `str \| None` | Email subject |
320
+ | `last_message_at` | `str` | ISO timestamp of last message |
321
+ | `first_message_at` | `str \| None` | ISO timestamp of first message |
322
+ | `message_count` | `int` | Total messages in thread |
323
+ | `snippet` | `str \| None` | Preview of last message (up to 200 chars) |
324
+ | `last_direction` | `str \| None` | `"inbound"` or `"outbound"` |
325
+ | `inbox_id` | `str \| None` | Inbox this thread belongs to |
326
+ | `domain_id` | `str \| None` | Domain this thread belongs to |
327
+ | `has_attachments` | `bool` | Whether any message has attachments |
328
+
329
+ ### `client.threads.messages(thread_id, *, limit=50, order="asc")`
330
+
331
+ Get all messages in a thread. Returns oldest first by default (chronological reading order).
332
+
333
+ ```python
334
+ messages = client.threads.messages("conv_abc123")
335
+
336
+ for msg in messages:
337
+ sender = next((p.identity for p in msg.participants if p.role == "sender"), "unknown")
338
+ print(f" [{msg.direction}] From: {sender}")
339
+ print(f" Subject: {msg.metadata.subject}")
340
+ print(f" {msg.content[:200]}")
341
+ print()
342
+ ```
343
+
344
+ | Parameter | Type | Required | Description |
345
+ |-----------|------|----------|-------------|
346
+ | `thread_id` | `str` | Yes | Thread ID |
347
+ | `limit` | `int` | No | 1–1000, default 50 |
348
+ | `order` | `str` | No | `"asc"` (chronological) or `"desc"` |
349
+
350
+ **Returns:** `list[Message]`
351
+
352
+ **Message object:**
353
+
354
+ | Field | Type | Description |
355
+ |-------|------|-------------|
356
+ | `message_id` | `str` | Unique message identifier |
357
+ | `conversation_id` | `str` | Thread ID this message belongs to |
358
+ | `direction` | `str` | `"inbound"` or `"outbound"` |
359
+ | `participants` | `list[Participant]` | `[{role: "sender", identity: "user@..."}, ...]` |
360
+ | `content` | `str` | Plain text body |
361
+ | `content_html` | `str \| None` | HTML body |
362
+ | `attachments` | `list[str]` | Attachment IDs |
363
+ | `created_at` | `str` | ISO timestamp |
364
+ | `metadata.subject` | `str` | Subject line |
365
+ | `metadata.inbox_id` | `str` | Inbox ID |
366
+
367
+ ---
368
+
369
+ ## Messages
370
+
371
+ ### `client.messages.send(**kwargs)`
372
+
373
+ Send an email. Returns the sent message data.
374
+
375
+ ```python
376
+ result = client.messages.send(
377
+ to="user@example.com",
378
+ subject="Order Confirmation",
379
+ html="<h1>Thanks for your order!</h1><p>Your order #1234 is confirmed.</p>",
380
+ )
381
+ ```
382
+
383
+ | Parameter | Type | Required | Description |
384
+ |-----------|------|----------|-------------|
385
+ | `to` | `str \| list[str]` | Yes | Recipient(s) |
386
+ | `subject` | `str` | Yes | Subject line |
387
+ | `html` | `str` | No* | HTML body |
388
+ | `text` | `str` | No* | Plain text body |
389
+ | `from_address` | `str` | No | Sender (uses domain default) |
390
+ | `cc` | `list[str]` | No | CC recipients |
391
+ | `bcc` | `list[str]` | No | BCC recipients |
392
+ | `reply_to` | `str` | No | Reply-to address |
393
+ | `thread_id` | `str` | No | Reply in existing thread |
394
+ | `domain_id` | `str` | No | Send from specific domain |
395
+ | `inbox_id` | `str` | No | Send from specific inbox |
396
+ | `attachments` | `list[str]` | No | Attachment IDs |
397
+ | `headers` | `dict[str, str]` | No | Custom headers |
398
+
399
+ *At least one of `html` or `text` is required.
400
+
401
+ **Reply to a thread:**
402
+
403
+ ```python
404
+ client.messages.send(
405
+ to="customer@gmail.com",
406
+ subject="Re: Order Issue",
407
+ html="<p>We're looking into this for you.</p>",
408
+ thread_id="conv_abc123", # continues the thread
409
+ inbox_id="i_xyz",
410
+ )
411
+ ```
412
+
413
+ ### `client.messages.list(**kwargs)`
414
+
415
+ List messages with filters. Provide at least one of `inbox_id`, `domain_id`, or `sender`.
416
+
417
+ ```python
418
+ messages = client.messages.list(
419
+ inbox_id="i_xyz",
420
+ limit=20,
421
+ order="desc",
422
+ after="2025-01-01T00:00:00Z",
423
+ )
424
+ ```
425
+
426
+ | Parameter | Type | Required | Description |
427
+ |-----------|------|----------|-------------|
428
+ | `inbox_id` | `str` | One of | Filter by inbox |
429
+ | `domain_id` | `str` | these | Filter by domain |
430
+ | `sender` | `str` | required | Filter by sender email |
431
+ | `limit` | `int` | No | 1–1000, default 50 |
432
+ | `order` | `str` | No | `"asc"` or `"desc"` (default) |
433
+ | `before` | `str` | No | ISO date — messages before this time |
434
+ | `after` | `str` | No | ISO date — messages after this time |
435
+
436
+ ---
437
+
438
+ ## Attachments
439
+
440
+ Upload files, then reference them when sending emails.
441
+
442
+ ### `client.attachments.upload(content, filename, mime_type)`
443
+
444
+ Upload a file. Returns an `attachment_id` you pass to `messages.send()`.
445
+
446
+ ```python
447
+ import base64
448
+
449
+ with open("invoice.pdf", "rb") as f:
450
+ content = base64.b64encode(f.read()).decode()
451
+
452
+ upload = client.attachments.upload(
453
+ content=content,
454
+ filename="invoice.pdf",
455
+ mime_type="application/pdf",
456
+ )
457
+ print(upload.attachment_id) # → "att_abc123"
458
+ print(upload.size) # → 45230
459
+ ```
460
+
461
+ | Parameter | Type | Required | Description |
462
+ |-----------|------|----------|-------------|
463
+ | `content` | `str` | Yes | Base64-encoded file data |
464
+ | `filename` | `str` | Yes | Original filename |
465
+ | `mime_type` | `str` | Yes | MIME type |
466
+
467
+ **Returns:** `AttachmentUpload`
468
+
469
+ | Field | Type | Description |
470
+ |-------|------|-------------|
471
+ | `attachment_id` | `str` | ID to use in `messages.send()` |
472
+ | `filename` | `str` | Filename |
473
+ | `mime_type` | `str` | MIME type |
474
+ | `size` | `int` | Size in bytes |
475
+
476
+ ### `client.attachments.get(attachment_id)`
477
+
478
+ Get metadata for an uploaded attachment.
479
+
480
+ ```python
481
+ att = client.attachments.get("att_abc123")
482
+ print(att.filename, att.mime_type, att.size)
483
+ ```
484
+
485
+ ### `client.attachments.url(attachment_id, *, expires_in=3600)`
486
+
487
+ Get a temporary download URL.
488
+
489
+ ```python
490
+ url_info = client.attachments.url("att_abc123", expires_in=7200)
491
+ print(url_info.url) # → "https://..."
492
+ print(url_info.expires_in) # → 7200
493
+ ```
494
+
495
+ **Returns:** `AttachmentUrl`
496
+
497
+ | Field | Type | Description |
498
+ |-------|------|-------------|
499
+ | `url` | `str` | Temporary download URL |
500
+ | `expires_in` | `int` | Seconds until URL expires |
501
+ | `filename` | `str` | Filename |
502
+ | `mime_type` | `str` | MIME type |
503
+ | `size` | `int` | Size in bytes |
504
+
505
+ ### Full attachment flow
506
+
507
+ ```python
508
+ import base64
509
+
510
+ # 1. Upload the file
511
+ with open("report.pdf", "rb") as f:
512
+ content = base64.b64encode(f.read()).decode()
513
+
514
+ upload = client.attachments.upload(content, "report.pdf", "application/pdf")
515
+
516
+ # 2. Send email with attachment
517
+ client.messages.send(
518
+ to="user@example.com",
519
+ subject="Monthly Report",
520
+ html="<p>Please find the report attached.</p>",
521
+ attachments=[upload.attachment_id],
522
+ )
523
+
524
+ # 3. Later, get a download URL for that attachment
525
+ url_info = client.attachments.url(upload.attachment_id)
526
+ print(f"Download: {url_info.url}")
527
+ ```
528
+
529
+ ---
530
+
531
+ ## Error Handling
532
+
533
+ All errors inherit from `CommuneError`. Catch specific types or the base class.
534
+
535
+ ```python
536
+ from commune import (
537
+ CommuneClient,
538
+ CommuneError,
539
+ AuthenticationError,
540
+ NotFoundError,
541
+ ValidationError,
542
+ RateLimitError,
543
+ )
544
+
545
+ try:
546
+ client = CommuneClient(api_key="comm_...")
547
+ domain = client.domains.get("nonexistent")
548
+ except AuthenticationError:
549
+ # 401 — invalid or expired API key
550
+ print("Check your API key")
551
+ except NotFoundError:
552
+ # 404 — resource doesn't exist
553
+ print("Domain not found")
554
+ except ValidationError as e:
555
+ # 400 — bad request parameters
556
+ print(f"Invalid request: {e.message}")
557
+ except RateLimitError:
558
+ # 429 — too many requests
559
+ print("Slow down, try again in a moment")
560
+ except CommuneError as e:
561
+ # Catch-all for any API error
562
+ print(f"Error ({e.status_code}): {e.message}")
563
+ ```
564
+
565
+ | Exception | HTTP Status | When |
566
+ |-----------|-------------|------|
567
+ | `AuthenticationError` | 401 | Invalid/expired API key |
568
+ | `ValidationError` | 400 | Bad request parameters |
569
+ | `NotFoundError` | 404 | Resource doesn't exist |
570
+ | `RateLimitError` | 429 | Too many requests |
571
+ | `CommuneError` | Any | Base class for all errors |
572
+
573
+ ---
574
+
575
+ ## License
576
+
577
+ MIT