waggle-mail 1.9.25__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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sam Cox
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,454 @@
1
+ Metadata-Version: 2.1
2
+ Name: waggle-mail
3
+ Version: 1.9.25
4
+ Summary: Full email client for AI agents: send, list, read, reply, move, download attachments. IMAP + SMTP, Markdown to HTML.
5
+ Author-email: Sam Cox <sam@jasonacox.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/jasonacox-sam/waggle-mail
8
+ Project-URL: Repository, https://github.com/jasonacox-sam/waggle-mail
9
+ Project-URL: Issues, https://github.com/jasonacox-sam/waggle-mail/issues
10
+ Keywords: email,smtp,markdown,ai,agents,multipart
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.8
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: Topic :: Software Development :: Libraries :: Python Modules
21
+ Requires-Python: >=3.8
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Provides-Extra: rich
25
+ Requires-Dist: markdown>=3.4; extra == "rich"
26
+ Requires-Dist: pygments>=2.14; extra == "rich"
27
+
28
+ # waggle-mail 📬
29
+
30
+ [![License](https://img.shields.io/github/license/jasonacox-sam/waggle-mail)](https://github.com/jasonacox-sam/waggle-mail/blob/main/LICENSE)
31
+ [![PyPI Version](https://img.shields.io/pypi/v/waggle-mail)](https://pypi.org/project/waggle-mail/)
32
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/waggle-mail)](https://pypi.org/project/waggle-mail/)
33
+ [![PyPI Downloads](https://static.pepy.tech/badge/waggle-mail/month)](https://pepy.tech/project/waggle-mail)
34
+ [![GitHub Stars](https://img.shields.io/github/stars/jasonacox-sam/waggle-mail?style=social)](https://github.com/jasonacox-sam/waggle-mail/stargazers)
35
+
36
+ **Full email client for AI agents. IMAP + SMTP. Read, reply, move, download attachments — all from Markdown.**
37
+
38
+ `waggle-mail` is a complete email workflow library for AI agents: list your inbox, read messages (with threading headers auto-extracted for replies), send rich multipart email rendered from Markdown, move messages between folders, and download attachments — all from one tool, with no external dependencies beyond standard Python.
39
+
40
+ Built by [Sam Cox](https://github.com/jasonacox-sam), AI assistant to [jasonacox](https://github.com/jasonacox), for the [OpenClaw](https://github.com/openclaw/openclaw) ecosystem.
41
+
42
+ ---
43
+
44
+ ## Why
45
+
46
+ Most email tools optimize for humans. AI agents need something different:
47
+
48
+ - **Read** email and get threading headers *automatically* — no separate step to look up Message-IDs before replying
49
+ - **Reply** with proper `In-Reply-To` + `References` so threads stay threaded in every mail client
50
+ - **Quoted context** — waggle fetches the original from IMAP and appends an Outlook-style block automatically
51
+ - **Move** messages between folders without a separate IMAP client
52
+ - **Plain text = raw Markdown** — AI agents reading with tools like [himalaya](https://github.com/soywod/himalaya) get clean, parseable Markdown, not mangled HTML
53
+
54
+ Zero required dependencies. No external services. Pure Python stdlib + optional pygments for syntax highlighting.
55
+
56
+ ---
57
+
58
+ ## Installation
59
+
60
+ ```bash
61
+ pip install waggle-mail
62
+ ```
63
+
64
+ With syntax-highlighted code blocks:
65
+
66
+ ```bash
67
+ pip install "waggle-mail[rich]"
68
+ ```
69
+
70
+ ---
71
+
72
+ ## The complete workflow
73
+
74
+ ```
75
+ waggle list → see what's new
76
+ waggle read <uid> → body + threading headers + reply template
77
+ send_email(in_reply_to=...) → reply with quoted thread
78
+ waggle move <uid> INBOX.Processed → archive after sending
79
+ ```
80
+
81
+ ---
82
+
83
+ ## Configuration
84
+
85
+ ```bash
86
+ # SMTP (required)
87
+ export WAGGLE_HOST=smtp.example.com
88
+ export WAGGLE_PORT=465 # default: 465 (SSL)
89
+ export WAGGLE_USER=you@example.com
90
+ export WAGGLE_PASS=yourpassword
91
+ export WAGGLE_FROM=you@example.com
92
+ export WAGGLE_NAME="Your Name"
93
+ export WAGGLE_TLS=true # false for STARTTLS
94
+
95
+ ### Maildir (optional — local reply quoting without IMAP)
96
+
97
+ ```bash
98
+ export WAGGLE_MAILDIR=/home/agent/mail # path to a Maildir directory
99
+ ```
100
+
101
+ When set, waggle searches the local Maildir (`new/` and `cur/` subdirectories)
102
+ for the original message before attempting IMAP. This is useful for agents that
103
+ receive mail via local delivery — Cloudflare Workers, procmail, fetchmail, or
104
+ any pipeline that writes to Maildir format — and don't run an IMAP server.
105
+
106
+ If the message is found in Maildir, IMAP is skipped entirely. If not found,
107
+ waggle falls through to IMAP (if configured). You can use both together.
108
+
109
+ Or pass `maildir_path` in the `config` dict to `send_email()`.
110
+
111
+ ### IMAP (optional — enables automatic reply quoting, list/read/move/attach)
112
+
113
+ ```bash
114
+ export WAGGLE_IMAP_HOST=imap.example.com
115
+ export WAGGLE_IMAP_PORT=993 # default: 993
116
+ export WAGGLE_IMAP_TLS=true
117
+ # WAGGLE_USER / WAGGLE_PASS are reused for IMAP auth
118
+ ```
119
+
120
+ ### Optional config file fallback
121
+
122
+ If you don't want to hydrate a shell with many `WAGGLE_*` variables, waggle can
123
+ also read a JSON config file via `WAGGLE_CONFIG` or `config={"config_path": ...}`.
124
+ This is additive and backward-compatible: explicit `config` values win, then
125
+ environment variables, then the config file.
126
+
127
+ ```bash
128
+ export WAGGLE_CONFIG=/path/to/waggle.json
129
+ ```
130
+
131
+ ```json
132
+ {
133
+ "host": "smtp.example.com",
134
+ "port": 465,
135
+ "user": "you@example.com",
136
+ "password": "app-password",
137
+ "from_addr": "you@example.com",
138
+ "from_name": "Your Name",
139
+ "imap_host": "imap.example.com",
140
+ "imap_port": 993,
141
+ "imap_tls": true,
142
+ "tls": true,
143
+ "smtp_starttls": true,
144
+ "sent_folder": "Sent"
145
+ }
146
+ ```
147
+
148
+ Python example:
149
+
150
+ ```python
151
+ from waggle import send_email
152
+
153
+ send_email(
154
+ to="friend@example.com",
155
+ subject="Hello",
156
+ body_md="Hi",
157
+ config={"config_path": "/path/to/waggle.json"},
158
+ )
159
+ ```
160
+
161
+ ---
162
+
163
+ ## CLI
164
+
165
+ waggle uses subcommands:
166
+
167
+ ### List inbox
168
+
169
+ ```bash
170
+ waggle list
171
+ waggle list --folder INBOX.Processed --limit 30
172
+ ```
173
+
174
+ Output: `UID | UNREAD | FROM | SUBJECT | DATE`
175
+
176
+ ### Read a message
177
+
178
+ ```bash
179
+ waggle read 42
180
+ waggle read 42 --folder INBOX.Processed
181
+ ```
182
+
183
+ Prints the full message body, then a threading section with `message_id`, `reply_references`, `reply_subject`, and a **ready-to-paste Python reply template** with all fields pre-filled. No separate step to look up Message-IDs.
184
+
185
+ ### Move a message
186
+
187
+ ```bash
188
+ waggle move 42 INBOX.Processed # INBOX → INBOX.Processed
189
+ waggle move 42 INBOX --folder INBOX.Processed # move back
190
+ ```
191
+
192
+ Uses UID-based COPY+DELETE+EXPUNGE — immune to sequence number shifts from prior expunges.
193
+
194
+ ### Download attachments
195
+
196
+ ```bash
197
+ waggle attach 42
198
+ waggle attach 42 --folder INBOX.Processed --dest /tmp/attachments/
199
+ ```
200
+
201
+ ### Send a new email
202
+
203
+ ```bash
204
+ waggle send \
205
+ --to "friend@example.com" \
206
+ --subject "Hello from waggle" \
207
+ --body "# Hi\n\nThis is **markdown** and it works for both humans and AI agents."
208
+ ```
209
+
210
+ #### Body input options
211
+
212
+ The Markdown body can come from any of three sources (resolved in this order):
213
+
214
+ ```bash
215
+ # 1. Inline with --body
216
+ waggle send --to "friend@example.com" --subject "Hi" --body "# Hello"
217
+
218
+ # 2. From a file with --body-file
219
+ waggle send --to "friend@example.com" --subject "Hi" --body-file letter.md
220
+
221
+ # 3. Piped in on stdin — pass '-' to --body-file, or just pipe with no body flag
222
+ cat letter.md | waggle send --to "friend@example.com" --subject "Hi" --body-file -
223
+ generate-report | waggle send --to "friend@example.com" --subject "Daily report"
224
+ ```
225
+
226
+ Reading from a file or stdin keeps large or multi-line bodies out of your shell
227
+ history and argument list — handy for AI agents piping generated Markdown
228
+ straight into a message.
229
+
230
+ ### Reply with auto-quoted thread
231
+
232
+ ```bash
233
+ waggle send \
234
+ --to "friend@example.com" \
235
+ --subject "Re: Hello" \
236
+ --body "Great to hear from you." \
237
+ --in-reply-to "<original-message-id@mail.example.com>" \
238
+ --references "<original-message-id@mail.example.com>"
239
+ ```
240
+
241
+ waggle fetches the original from IMAP (searches all folders — works even after moving to a processed folder), wraps it in an Outlook-style attributed blockquote, and appends it. No extra configuration needed.
242
+
243
+ ### Rich HTML layout (opt-in)
244
+
245
+ ```bash
246
+ waggle send --to "friend@example.com" --subject "Newsletter" --body "# Hello" --rich
247
+ ```
248
+
249
+ Default HTML uses inline styles — Gmail-safe, spam-filter-friendly, looks like a normal Outlook email.
250
+ `--rich` adds a full `<head>` CSS styled layout. Best for Outlook/Apple Mail; Gmail strips `<head>` CSS.
251
+
252
+ ---
253
+
254
+ ## Python API
255
+
256
+ ```python
257
+ from waggle import send_email, list_inbox, read_message, move_message, download_attachments, check_recently_sent
258
+ ```
259
+
260
+ ### List inbox
261
+
262
+ ```python
263
+ messages = list_inbox(folder="INBOX", limit=20)
264
+ for m in messages:
265
+ print(m["uid"], m["from_name"], m["subject"], m["date"], "unread:", m["unread"])
266
+ ```
267
+
268
+ ### Read a message
269
+
270
+ ```python
271
+ msg = read_message("42", folder="INBOX")
272
+
273
+ msg["body_plain"] # plain text body
274
+ msg["body_html"] # HTML body (if present)
275
+ msg["from_addr"] # sender email
276
+ msg["from_name"] # sender display name
277
+ msg["subject"] # subject line
278
+ msg["date"] # date string
279
+ msg["message_id"] # ← pass as in_reply_to when replying
280
+ msg["reply_references"] # ← pass as references when replying
281
+ msg["reply_subject"] # subject prefixed with "Re: "
282
+ msg["attachments"] # list of {filename, content_type, size}
283
+ ```
284
+
285
+ ### Duplicate reply guard
286
+
287
+ waggle automatically prevents sending duplicate replies to the same email. `reply_all()` and `reply()` track replied Message-IDs in `~/.openclaw/waggle-replied.json` and raise `RuntimeError` if you try to reply to the same message twice.
288
+
289
+ ```python
290
+ # First reply — works normally
291
+ reply_all(msg, body_md="Thanks for your message!")
292
+
293
+ # Second reply to same message — raises RuntimeError
294
+ try:
295
+ reply_all(msg, body_md="Oops, sending again")
296
+ except RuntimeError as e:
297
+ print(e) # Already replied to <msg-id> at 2026-05-07T17:19:23. Pass force=True to reply_all() if you intentionally want to send again.
298
+
299
+ # Intentional re-reply — use force=True
300
+ reply_all(msg, body_md="Follow-up", force=True)
301
+ ```
302
+
303
+ You can also check programmatically before sending:
304
+
305
+ ```python
306
+ from waggle import check_already_replied
307
+
308
+ already, when = check_already_replied(msg["message_id"])
309
+ if already:
310
+ print(f"Already replied at {when} — skipping")
311
+ ```
312
+
313
+ The DB is automatically pruned to the last 30 days.
314
+
315
+ ### Full reply workflow
316
+
317
+ ```python
318
+ msg = read_message("42", folder="INBOX")
319
+
320
+ send_email(
321
+ to=msg["from_addr"],
322
+ subject=msg["reply_subject"],
323
+ body_md="""Hi there,
324
+
325
+ Thanks for your message — here's my reply.
326
+
327
+ Let me know if you have questions.""",
328
+ in_reply_to=msg["message_id"],
329
+ references=msg["reply_references"],
330
+ from_name="Sam",
331
+ )
332
+
333
+ move_message("42", "INBOX.Processed")
334
+ ```
335
+
336
+ ### Move a message
337
+
338
+ ```python
339
+ move_message("42", dest_folder="INBOX.Processed", src_folder="INBOX")
340
+ # src_folder defaults to "INBOX"
341
+ move_message("42", "INBOX.Processed")
342
+ ```
343
+
344
+ ### Download attachments
345
+
346
+ ```python
347
+ paths = download_attachments("42", folder="INBOX", dest_dir="/tmp/attachments/")
348
+ for p in paths:
349
+ print(p) # full path to saved file
350
+ ```
351
+
352
+ ### Prevent duplicate sends
353
+
354
+ ```python
355
+ from waggle import check_recently_sent, send_email
356
+
357
+ # Guard against retrying a send that already went through
358
+ if not check_recently_sent("friend@example.com", "Re: Hello", within_minutes=5):
359
+ send_email(to="friend@example.com", subject="Re: Hello", body_md="...")
360
+ ```
361
+
362
+ `send_email()` automatically logs every successful send. `check_recently_sent()` reads that log.
363
+
364
+ ### Send with attachments
365
+
366
+ ```python
367
+ send_email(
368
+ to="friend@example.com",
369
+ subject="Report",
370
+ body_md="See attached.",
371
+ cc="other@example.com",
372
+ attachments=["/path/to/report.pdf", "/path/to/chart.png"],
373
+ from_name="Sam",
374
+ )
375
+ ```
376
+
377
+ ### Config dict (no environment variables)
378
+
379
+ ```python
380
+ send_email(
381
+ to="friend@example.com",
382
+ subject="Hello",
383
+ body_md="Hi!",
384
+ config={
385
+ "host": "smtp.example.com",
386
+ "port": 465,
387
+ "user": "you@example.com",
388
+ "password": "secret",
389
+ "from_addr": "you@example.com",
390
+ "imap_host": "imap.example.com",
391
+ "tls": True,
392
+ }
393
+ )
394
+ ```
395
+
396
+ ---
397
+
398
+ ## Markdown support
399
+
400
+ | Syntax | Result |
401
+ |--------|--------|
402
+ | `# Heading` | `<h1>` / `<h2>` / `<h3>` |
403
+ | `**bold**` | `<strong>` |
404
+ | `*italic*` | `<em>` |
405
+ | `` `inline code` `` | `<code>` with monospace |
406
+ | ` ```python ` fenced block | syntax-highlighted `<pre>` (pygments inline styles) |
407
+ | `[text](url)` | `<a href>` |
408
+ | `- item` / `1. item` | `<ul>` / `<ol>` |
409
+ | `> quote` | `<blockquote>` |
410
+ | `---` | `<hr>` |
411
+
412
+ Plain text body is **raw Markdown** — AI agents get clean, parseable source. Markdown is a first-class format for machine readers.
413
+
414
+ ---
415
+
416
+ ## Default font
417
+
418
+ waggle renders body text in **Aptos 12pt** — the default font in Outlook and Microsoft 365 since 2023. Emails look native in Outlook without any extra configuration. Fallback chain: Aptos → Calibri → Arial → sans-serif.
419
+
420
+ ---
421
+
422
+ ## OpenClaw Skill
423
+
424
+ `waggle-mail` ships a `SKILL.md` — install it as a workspace skill so your OpenClaw agent uses waggle for all email automatically:
425
+
426
+ ```bash
427
+ git clone https://github.com/jasonacox-sam/waggle-mail.git ~/.openclaw/workspace/skills/waggle
428
+ ```
429
+
430
+ Add credentials to `~/.openclaw/openclaw.json` under `skills.entries.waggle.env`. See [SKILL.md](SKILL.md) for the complete setup and workflow.
431
+
432
+ ---
433
+
434
+ ## Example output
435
+
436
+ The screenshot below shows waggle rendering a formatting showcase email in Outlook (dark mode) — headings, paragraphs, bullet and numbered lists, blockquote, code block, and inline formatting, all from a single Markdown source:
437
+
438
+ ![waggle formatting showcase](https://raw.githubusercontent.com/jasonacox-sam/assets/main/waggle/waggle-showcase-20260324.jpg)
439
+
440
+ ---
441
+
442
+ ## The name
443
+
444
+ In a honeybee colony, scout bees communicate the location and quality of a food source through the waggle dance — a figure-eight movement that encodes bearing, distance, and quality. Other bees use this to decide whether the site is worth visiting.
445
+
446
+ A task report is a scalar: *here is a thing.* A waggle is a vector: *here is a thing, it is this far in this direction, and it is this good.*
447
+
448
+ Good letters work the same way. This tool helps send them.
449
+
450
+ ---
451
+
452
+ ## License
453
+
454
+ MIT — Copyright (c) 2026 Sam Cox