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.
- waggle-mail-1.9.25/LICENSE +21 -0
- waggle-mail-1.9.25/PKG-INFO +454 -0
- waggle-mail-1.9.25/README.md +427 -0
- waggle-mail-1.9.25/pyproject.toml +40 -0
- waggle-mail-1.9.25/setup.cfg +4 -0
- waggle-mail-1.9.25/tests/test_build_cfg.py +98 -0
- waggle-mail-1.9.25/tests/test_check.py +164 -0
- waggle-mail-1.9.25/tests/test_cli_json.py +99 -0
- waggle-mail-1.9.25/tests/test_dedup_guard.py +82 -0
- waggle-mail-1.9.25/tests/test_flag_management.py +132 -0
- waggle-mail-1.9.25/tests/test_flynn_scenario.py +28 -0
- waggle-mail-1.9.25/tests/test_heading_levels.py +50 -0
- waggle-mail-1.9.25/tests/test_html_entities.py +99 -0
- waggle-mail-1.9.25/tests/test_html_to_plain.py +132 -0
- waggle-mail-1.9.25/tests/test_reply_guard.py +443 -0
- waggle-mail-1.9.25/tests/test_skip_quote_fetch.py +88 -0
- waggle-mail-1.9.25/tests/test_strikethrough.py +41 -0
- waggle-mail-1.9.25/tests/test_threading.py +51 -0
- waggle-mail-1.9.25/waggle.py +2561 -0
- waggle-mail-1.9.25/waggle_mail.egg-info/PKG-INFO +454 -0
- waggle-mail-1.9.25/waggle_mail.egg-info/SOURCES.txt +23 -0
- waggle-mail-1.9.25/waggle_mail.egg-info/dependency_links.txt +1 -0
- waggle-mail-1.9.25/waggle_mail.egg-info/entry_points.txt +2 -0
- waggle-mail-1.9.25/waggle_mail.egg-info/requires.txt +4 -0
- waggle-mail-1.9.25/waggle_mail.egg-info/top_level.txt +1 -0
|
@@ -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
|
+
[](https://github.com/jasonacox-sam/waggle-mail/blob/main/LICENSE)
|
|
31
|
+
[](https://pypi.org/project/waggle-mail/)
|
|
32
|
+
[](https://pypi.org/project/waggle-mail/)
|
|
33
|
+
[](https://pepy.tech/project/waggle-mail)
|
|
34
|
+
[](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
|
+

|
|
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
|