| name | waggle |
| description | Send multipart email (plain text + HTML) from Markdown. Use waggle for ALL outbound email — letters to the herd, notifications, reports, replies. Pairs with himalaya (reading) to form a complete email workflow.
|
| homepage | https://github.com/jasonacox-sam/waggle-mail |
| metadata | {"openclaw":{"emoji":"🐝","requires":{"bins":["waggle"],"env":["WAGGLE_HOST"]}}} |
waggle 🐝
Use waggle for every outbound email. No exceptions. Not raw SMTP, not email_helper.py.
waggle sends two parts from one Markdown source:
- Plain text = raw Markdown (AI agents reading with himalaya get clean, parseable source)
- HTML = rendered with pygments inline syntax highlighting — Gmail-safe, no
<head> CSS
Pairs with himalaya: himalaya reads, waggle sends.
Which interface to use
Use the Python API for almost everything — especially replies, multi-paragraph bodies, and anything with code blocks or special characters. The CLI --body flag requires escaping newlines as \n, backticks, and quotes, which is error-prone and hard to read.
Use the CLI only for short, simple one-liners (e.g. a quick notification with a single sentence body and no special characters).
If in doubt: Python API.
Sending a new email
waggle --to recipient@example.com \
--subject "Hello" \
--body "Just a quick note."
import sys
sys.path.insert(0, '/home/jason/.openclaw/workspace/projects/waggle')
from waggle import send_email
send_email(
to="recipient@example.com",
subject="Hello",
body_md="""# Hi
This is **markdown** with a code block:
```python
print('hello')
More prose here — no escaping needed.""",
from_name="Sam",
)
With CC and attachment (Python API):
```python
send_email(
to="recipient@example.com",
cc="other@example.com",
subject="Report",
body_md="See attached.",
attachments=["/path/to/file.pdf", "/path/to/image.png"],
)
Replying to an email (most common case)
This is the workflow: read with himalaya → get Message-ID → reply with waggle.
himalaya message read <id> --account sam
waggle --to sender@example.com \
--subject "Re: Original Subject" \
--body "Your reply text here in **markdown**." \
--in-reply-to "<message-id-from-step-1@example.com>" \
--references "<message-id-from-step-1@example.com>"
himalaya message move "INBOX.Processed" --account sam <id>
What waggle does automatically when --in-reply-to is provided:
- Connects to IMAP and searches ALL folders for the original message
- Fetches the full email (HTML + plain text)
- Appends an Outlook-style attributed quote block:
- HTML: original email HTML wrapped in
<div style="border-left:2px solid #ccc"> — snowballs naturally like Outlook
- Plain:
-----Original Message----- + attribution + full body
- Sends the reply with correct
In-Reply-To and References threading headers
Works even after moving the original to INBOX.Processed — waggle searches all folders.
Falls back gracefully — if IMAP isn't configured or message not found, sends without quote.
HTML rendering modes
Default (no flag): inline styles only — Gmail-safe, looks like a normal Outlook/Apple Mail email, code blocks syntax-highlighted via pygments inline <span style="...">.
--rich flag: full styled layout with <head> CSS — centered column, custom typography. Use for polished newsletters or herd letters where you want beautiful rendering in desktop mail clients. Note: Gmail strips <head> CSS, so --rich is best for Outlook/Apple Mail recipients.
waggle --to recipient@example.com \
--subject "Hello" \
--body "# Hi" \
--rich
Python API
from waggle import send_email
send_email(
to="recipient@example.com",
subject="Hello",
body_md="# Hi\n\nThis is **markdown**.",
cc="other@example.com",
from_name="Sam",
attachments=["file.pdf"],
rich=False,
)
send_email(
to="sender@example.com",
subject="Re: Topic",
body_md="Your reply here.",
in_reply_to="<msg-id@example.com>",
references="<msg-id@example.com>",
config={"imap_host": "imap.example.com", ...},
)
Configuration
All set via env vars (injected by openclaw.json — see Setup below):
| Env var | Required | Default | Description |
|---|
WAGGLE_HOST | ✅ | — | SMTP server hostname |
WAGGLE_PORT | No | 465 | SMTP port |
WAGGLE_USER | ✅ | — | SMTP username (also used for IMAP auth) |
WAGGLE_PASS | ✅ | — | SMTP password (also used for IMAP auth) |
WAGGLE_FROM | No | WAGGLE_USER | From address |
WAGGLE_NAME | No | — | Display name in From header |
WAGGLE_TLS | No | true | false for STARTTLS instead of SSL |
WAGGLE_IMAP_HOST | No | WAGGLE_HOST | IMAP server (enables auto-reply quoting) |
WAGGLE_IMAP_PORT | No | 993 | IMAP port |
WAGGLE_IMAP_TLS | No | true | IMAP SSL |
WAGGLE_CONFIG | No | — | Path to JSON config file fallback (lower precedence than explicit config/env) |
Setup
pip install waggle-mail
Add credentials to ~/.openclaw/openclaw.json:
{
"skills": {
"entries": {
"waggle": {
"env": {
"WAGGLE_HOST": "smtp.yourprovider.com",
"WAGGLE_PORT": "465",
"WAGGLE_USER": "you@example.com",
"WAGGLE_PASS": "your-password",
"WAGGLE_FROM": "you@example.com",
"WAGGLE_NAME": "Your Name",
"WAGGLE_IMAP_HOST": "imap.yourprovider.com"
}
}
}
}
}
Notes & gotchas
--body is Markdown source — use \n for newlines on the command line, or use the Python API for multi-line bodies
- Code blocks in
--body: escape backticks as ``` or use the Python API
- Plain text = raw Markdown — don't strip or post-process it; AI readers expect it
- Attachments:
--attach can be specified multiple times for multiple files
- Threading: always pass both
--in-reply-to AND --references with the same Message-ID when replying; this is what makes Outlook/Apple Mail thread correctly
- Message-ID format: include the angle brackets:
"<abc123@example.com>" not "abc123@example.com"