first commit
This commit is contained in:
93
email/himalaya/references/clawemail-skills.md
Normal file
93
email/himalaya/references/clawemail-skills.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# ClawEmail Skills Ecosystem
|
||||
|
||||
ClawEmail (claw.163.com) provides pre-built "skills" for common email automation patterns.
|
||||
|
||||
## Installing Skills
|
||||
|
||||
```bash
|
||||
# Interactive (prompts for agent selection)
|
||||
npx skills add https://claw.163.com/s/<skill-name>.git
|
||||
|
||||
# Non-interactive (install to all agents globally)
|
||||
npx skills add https://claw.163.com/s/<skill-name>.git -y -g
|
||||
```
|
||||
|
||||
Skills install to `~/.agents/skills/<skill-name>/` with a SKILL.md and optional `scripts/` directory.
|
||||
|
||||
## Available Skills (as of 2026-05)
|
||||
|
||||
| Skill | Description | Token Cost |
|
||||
|-------|-------------|------------|
|
||||
| github-triage | GitHub notifications auto-triage by priority | Zero (CLI mode) |
|
||||
| daily-report | Multi-mailbox health inspection report | Zero (CLI mode) |
|
||||
| support-router | AI customer service email classifier + auto-reply | Yes (Channel mode) |
|
||||
| notify-hub | Multi-platform notification aggregator | Zero for triage, yes for AI summaries |
|
||||
| freelance-inbox | Freelancer intake auto-reply (coming soon) | TBA |
|
||||
| event-signup | Event registration auto-receipt + attachment archive (coming soon) | TBA |
|
||||
|
||||
**Key distinction:** CLI-mode skills (scripts, data operations) = zero token. Channel-mode skills (AI understands email content) = token consumption.
|
||||
|
||||
## mail-cli Package Confusion
|
||||
|
||||
There are TWO different npm packages named similarly:
|
||||
|
||||
| Package | Scope | Commands |
|
||||
|---------|-------|----------|
|
||||
| `mail-cli` | Generic email CLI | `--setup`, `--draft`, basic SMTP |
|
||||
| `@clawemail/mail-cli` | ClawEmail-specific | `clawemail list`, `folder list`, `compose send`, profile management |
|
||||
|
||||
ClawEmail skills require `@clawemail/mail-cli`:
|
||||
```bash
|
||||
sudo npm install -g @clawemail/mail-cli
|
||||
mail-cli --version # should show 0.2.x
|
||||
mail-cli auth apikey set <your-key>
|
||||
|
||||
# Also configure IMAP/SMTP profile (required for folder list to work):
|
||||
mail-cli auth login \
|
||||
--user ephronren@claw.163.com \
|
||||
--auth-method password \
|
||||
--password <password> \
|
||||
--imap-host claw.163.com --imap-port 993 \
|
||||
--smtp-host claw.163.com --smtp-port 465
|
||||
```
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- **`mail-cli clawemail list`** works with just the API key, but `mail-cli folder list` requires an auth profile. Without `auth login`, folder queries return `PROFILE_NOT_FOUND`.
|
||||
- **`mail-cli clawemail master-user --json`** returns `data.masterUser`, NOT `data.userEmail`. The daily-report skill's `inspect.js` originally expected `userEmail` and silently failed. Fix: `const email = data?.data?.userEmail || data?.data?.masterUser;`
|
||||
- **Reading email auto-marks as read** — IMAP `fetch(RFC822)` sets `\Seen` flag. Check `UNSEEN` count BEFORE reading content if you need accurate unread counts.
|
||||
|
||||
## Modifying Installed Skills
|
||||
|
||||
Skills are plain JS files in `~/.agents/skills/<name>/scripts/`. Edit directly with `patch` tool.
|
||||
|
||||
### daily-report: Silent Mode
|
||||
|
||||
Added to `inspect.js` before output section — exits quietly when no unread mail and no alerts:
|
||||
```javascript
|
||||
if (totalUnread === 0 && totalAlerts === 0) {
|
||||
if (outputJson) console.log(JSON.stringify({ silent: true, totalUnread: 0, totalAlerts: 0 }));
|
||||
process.exit(0);
|
||||
}
|
||||
```
|
||||
|
||||
### daily-report: masterUser Field Fix
|
||||
|
||||
Original code: `const email = data?.data?.userEmail;` → fails silently.
|
||||
Fixed: `const email = data?.data?.userEmail || data?.data?.masterUser;`
|
||||
|
||||
## SPA Documentation Pages
|
||||
|
||||
claw.163.com docs are SPAs (React). `curl` only gets empty HTML shell. Use Playwright:
|
||||
```bash
|
||||
NODE_PATH=~/.hermes/hermes-agent/node_modules node -e "
|
||||
const { chromium } = require('playwright-core');
|
||||
(async () => {
|
||||
const browser = await chromium.launch({ headless: true, args: ['--no-sandbox'] });
|
||||
const page = await browser.newPage();
|
||||
await page.goto('https://claw.163.com/projects/doc/', { waitUntil: 'networkidle' });
|
||||
console.log(await page.textContent('body'));
|
||||
await browser.close();
|
||||
})();
|
||||
"
|
||||
```
|
||||
184
email/himalaya/references/configuration.md
Normal file
184
email/himalaya/references/configuration.md
Normal file
@@ -0,0 +1,184 @@
|
||||
# Himalaya Configuration Reference
|
||||
|
||||
Configuration file location: `~/.config/himalaya/config.toml`
|
||||
|
||||
## Minimal IMAP + SMTP Setup
|
||||
|
||||
```toml
|
||||
[accounts.default]
|
||||
email = "user@example.com"
|
||||
display-name = "Your Name"
|
||||
default = true
|
||||
|
||||
# IMAP backend for reading emails
|
||||
backend.type = "imap"
|
||||
backend.host = "imap.example.com"
|
||||
backend.port = 993
|
||||
backend.encryption.type = "tls"
|
||||
backend.login = "user@example.com"
|
||||
backend.auth.type = "password"
|
||||
backend.auth.raw = "your-password"
|
||||
|
||||
# SMTP backend for sending emails
|
||||
message.send.backend.type = "smtp"
|
||||
message.send.backend.host = "smtp.example.com"
|
||||
message.send.backend.port = 587
|
||||
message.send.backend.encryption.type = "start-tls"
|
||||
message.send.backend.login = "user@example.com"
|
||||
message.send.backend.auth.type = "password"
|
||||
message.send.backend.auth.raw = "your-password"
|
||||
```
|
||||
|
||||
## Password Options
|
||||
|
||||
### Raw password (testing only, not recommended)
|
||||
|
||||
```toml
|
||||
backend.auth.raw = "your-password"
|
||||
```
|
||||
|
||||
### Password from command (recommended)
|
||||
|
||||
```toml
|
||||
backend.auth.cmd = "pass show email/imap"
|
||||
# backend.auth.cmd = "security find-generic-password -a user@example.com -s imap -w"
|
||||
```
|
||||
|
||||
### System keyring (requires keyring feature)
|
||||
|
||||
```toml
|
||||
backend.auth.keyring = "imap-example"
|
||||
```
|
||||
|
||||
Then run `himalaya account configure <account>` to store the password.
|
||||
|
||||
## Gmail Configuration
|
||||
|
||||
```toml
|
||||
[accounts.gmail]
|
||||
email = "you@gmail.com"
|
||||
display-name = "Your Name"
|
||||
default = true
|
||||
|
||||
backend.type = "imap"
|
||||
backend.host = "imap.gmail.com"
|
||||
backend.port = 993
|
||||
backend.encryption.type = "tls"
|
||||
backend.login = "you@gmail.com"
|
||||
backend.auth.type = "password"
|
||||
backend.auth.cmd = "pass show google/app-password"
|
||||
|
||||
message.send.backend.type = "smtp"
|
||||
message.send.backend.host = "smtp.gmail.com"
|
||||
message.send.backend.port = 587
|
||||
message.send.backend.encryption.type = "start-tls"
|
||||
message.send.backend.login = "you@gmail.com"
|
||||
message.send.backend.auth.type = "password"
|
||||
message.send.backend.auth.cmd = "pass show google/app-password"
|
||||
```
|
||||
|
||||
**Note:** Gmail requires an App Password if 2FA is enabled.
|
||||
|
||||
## iCloud Configuration
|
||||
|
||||
```toml
|
||||
[accounts.icloud]
|
||||
email = "you@icloud.com"
|
||||
display-name = "Your Name"
|
||||
|
||||
backend.type = "imap"
|
||||
backend.host = "imap.mail.me.com"
|
||||
backend.port = 993
|
||||
backend.encryption.type = "tls"
|
||||
backend.login = "you@icloud.com"
|
||||
backend.auth.type = "password"
|
||||
backend.auth.cmd = "pass show icloud/app-password"
|
||||
|
||||
message.send.backend.type = "smtp"
|
||||
message.send.backend.host = "smtp.mail.me.com"
|
||||
message.send.backend.port = 587
|
||||
message.send.backend.encryption.type = "start-tls"
|
||||
message.send.backend.login = "you@icloud.com"
|
||||
message.send.backend.auth.type = "password"
|
||||
message.send.backend.auth.cmd = "pass show icloud/app-password"
|
||||
```
|
||||
|
||||
**Note:** Generate an app-specific password at appleid.apple.com
|
||||
|
||||
## Folder Aliases
|
||||
|
||||
Map custom folder names:
|
||||
|
||||
```toml
|
||||
[accounts.default.folder.alias]
|
||||
inbox = "INBOX"
|
||||
sent = "Sent"
|
||||
drafts = "Drafts"
|
||||
trash = "Trash"
|
||||
```
|
||||
|
||||
## Multiple Accounts
|
||||
|
||||
```toml
|
||||
[accounts.personal]
|
||||
email = "personal@example.com"
|
||||
default = true
|
||||
# ... backend config ...
|
||||
|
||||
[accounts.work]
|
||||
email = "work@company.com"
|
||||
# ... backend config ...
|
||||
```
|
||||
|
||||
Switch accounts with `--account`:
|
||||
|
||||
```bash
|
||||
himalaya --account work envelope list
|
||||
```
|
||||
|
||||
## Notmuch Backend (local mail)
|
||||
|
||||
```toml
|
||||
[accounts.local]
|
||||
email = "user@example.com"
|
||||
|
||||
backend.type = "notmuch"
|
||||
backend.db-path = "~/.mail/.notmuch"
|
||||
```
|
||||
|
||||
## OAuth2 Authentication (for providers that support it)
|
||||
|
||||
```toml
|
||||
backend.auth.type = "oauth2"
|
||||
backend.auth.client-id = "your-client-id"
|
||||
backend.auth.client-secret.cmd = "pass show oauth/client-secret"
|
||||
backend.auth.access-token.cmd = "pass show oauth/access-token"
|
||||
backend.auth.refresh-token.cmd = "pass show oauth/refresh-token"
|
||||
backend.auth.auth-url = "https://provider.com/oauth/authorize"
|
||||
backend.auth.token-url = "https://provider.com/oauth/token"
|
||||
```
|
||||
|
||||
## Additional Options
|
||||
|
||||
### Signature
|
||||
|
||||
```toml
|
||||
[accounts.default]
|
||||
signature = "Best regards,\nYour Name"
|
||||
signature-delim = "-- \n"
|
||||
```
|
||||
|
||||
### Downloads directory
|
||||
|
||||
```toml
|
||||
[accounts.default]
|
||||
downloads-dir = "~/Downloads/himalaya"
|
||||
```
|
||||
|
||||
### Editor for composing
|
||||
|
||||
Set via environment variable:
|
||||
|
||||
```bash
|
||||
export EDITOR="vim"
|
||||
```
|
||||
199
email/himalaya/references/message-composition.md
Normal file
199
email/himalaya/references/message-composition.md
Normal file
@@ -0,0 +1,199 @@
|
||||
# Message Composition with MML (MIME Meta Language)
|
||||
|
||||
Himalaya uses MML for composing emails. MML is a simple XML-based syntax that compiles to MIME messages.
|
||||
|
||||
## Basic Message Structure
|
||||
|
||||
An email message is a list of **headers** followed by a **body**, separated by a blank line:
|
||||
|
||||
```
|
||||
From: sender@example.com
|
||||
To: recipient@example.com
|
||||
Subject: Hello World
|
||||
|
||||
This is the message body.
|
||||
```
|
||||
|
||||
## Headers
|
||||
|
||||
Common headers:
|
||||
|
||||
- `From`: Sender address
|
||||
- `To`: Primary recipient(s)
|
||||
- `Cc`: Carbon copy recipients
|
||||
- `Bcc`: Blind carbon copy recipients
|
||||
- `Subject`: Message subject
|
||||
- `Reply-To`: Address for replies (if different from From)
|
||||
- `In-Reply-To`: Message ID being replied to
|
||||
|
||||
### Address Formats
|
||||
|
||||
```
|
||||
To: user@example.com
|
||||
To: John Doe <john@example.com>
|
||||
To: "John Doe" <john@example.com>
|
||||
To: user1@example.com, user2@example.com, "Jane" <jane@example.com>
|
||||
```
|
||||
|
||||
## Plain Text Body
|
||||
|
||||
Simple plain text email:
|
||||
|
||||
```
|
||||
From: alice@localhost
|
||||
To: bob@localhost
|
||||
Subject: Plain Text Example
|
||||
|
||||
Hello, this is a plain text email.
|
||||
No special formatting needed.
|
||||
|
||||
Best,
|
||||
Alice
|
||||
```
|
||||
|
||||
## MML for Rich Emails
|
||||
|
||||
### Multipart Messages
|
||||
|
||||
Alternative text/html parts:
|
||||
|
||||
```
|
||||
From: alice@localhost
|
||||
To: bob@localhost
|
||||
Subject: Multipart Example
|
||||
|
||||
<#multipart type=alternative>
|
||||
This is the plain text version.
|
||||
<#part type=text/html>
|
||||
<html><body><h1>This is the HTML version</h1></body></html>
|
||||
<#/multipart>
|
||||
```
|
||||
|
||||
### Attachments
|
||||
|
||||
Attach a file:
|
||||
|
||||
```
|
||||
From: alice@localhost
|
||||
To: bob@localhost
|
||||
Subject: With Attachment
|
||||
|
||||
Here is the document you requested.
|
||||
|
||||
<#part filename=/path/to/document.pdf><#/part>
|
||||
```
|
||||
|
||||
Attachment with custom name:
|
||||
|
||||
```
|
||||
<#part filename=/path/to/file.pdf name=report.pdf><#/part>
|
||||
```
|
||||
|
||||
Multiple attachments:
|
||||
|
||||
```
|
||||
<#part filename=/path/to/doc1.pdf><#/part>
|
||||
<#part filename=/path/to/doc2.pdf><#/part>
|
||||
```
|
||||
|
||||
### Inline Images
|
||||
|
||||
Embed an image inline:
|
||||
|
||||
```
|
||||
From: alice@localhost
|
||||
To: bob@localhost
|
||||
Subject: Inline Image
|
||||
|
||||
<#multipart type=related>
|
||||
<#part type=text/html>
|
||||
<html><body>
|
||||
<p>Check out this image:</p>
|
||||
<img src="cid:image1">
|
||||
</body></html>
|
||||
<#part disposition=inline id=image1 filename=/path/to/image.png><#/part>
|
||||
<#/multipart>
|
||||
```
|
||||
|
||||
### Mixed Content (Text + Attachments)
|
||||
|
||||
```
|
||||
From: alice@localhost
|
||||
To: bob@localhost
|
||||
Subject: Mixed Content
|
||||
|
||||
<#multipart type=mixed>
|
||||
<#part type=text/plain>
|
||||
Please find the attached files.
|
||||
|
||||
Best,
|
||||
Alice
|
||||
<#part filename=/path/to/file1.pdf><#/part>
|
||||
<#part filename=/path/to/file2.zip><#/part>
|
||||
<#/multipart>
|
||||
```
|
||||
|
||||
## MML Tag Reference
|
||||
|
||||
### `<#multipart>`
|
||||
|
||||
Groups multiple parts together.
|
||||
|
||||
- `type=alternative`: Different representations of same content
|
||||
- `type=mixed`: Independent parts (text + attachments)
|
||||
- `type=related`: Parts that reference each other (HTML + images)
|
||||
|
||||
### `<#part>`
|
||||
|
||||
Defines a message part.
|
||||
|
||||
- `type=<mime-type>`: Content type (e.g., `text/html`, `application/pdf`)
|
||||
- `filename=<path>`: File to attach
|
||||
- `name=<name>`: Display name for attachment
|
||||
- `disposition=inline`: Display inline instead of as attachment
|
||||
- `id=<cid>`: Content ID for referencing in HTML
|
||||
|
||||
## Composing from CLI
|
||||
|
||||
### Interactive compose
|
||||
|
||||
Opens your `$EDITOR`:
|
||||
|
||||
```bash
|
||||
himalaya message write
|
||||
```
|
||||
|
||||
### Reply (opens editor with quoted message)
|
||||
|
||||
```bash
|
||||
himalaya message reply 42
|
||||
himalaya message reply 42 --all # reply-all
|
||||
```
|
||||
|
||||
### Forward
|
||||
|
||||
```bash
|
||||
himalaya message forward 42
|
||||
```
|
||||
|
||||
### Send from stdin
|
||||
|
||||
```bash
|
||||
cat message.txt | himalaya template send
|
||||
```
|
||||
|
||||
### Prefill headers from CLI
|
||||
|
||||
```bash
|
||||
himalaya message write \
|
||||
-H "To:recipient@example.com" \
|
||||
-H "Subject:Quick Message" \
|
||||
"Message body here"
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
- The editor opens with a template; fill in headers and body.
|
||||
- Save and exit the editor to send; exit without saving to cancel.
|
||||
- MML parts are compiled to proper MIME when sending.
|
||||
- Use `himalaya message export --full` to inspect the raw MIME structure of received emails.
|
||||
78
email/himalaya/references/python-email-fallback.md
Normal file
78
email/himalaya/references/python-email-fallback.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Python Email Fallback (when himalaya is not installed)
|
||||
|
||||
When `himalaya` CLI is unavailable, use Python's built-in `smtplib` and `imaplib` with credentials from `~/.hermes/config.yaml`.
|
||||
|
||||
## Sending Email
|
||||
|
||||
```python
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
smtp_host = "claw.163.com"
|
||||
sender = "ephronren@claw.163.com"
|
||||
password = "<from config>"
|
||||
recipient = "recipient@example.com"
|
||||
|
||||
msg = MIMEText("Body text", "plain", "utf-8")
|
||||
msg["From"] = sender
|
||||
msg["To"] = recipient
|
||||
msg["Subject"] = "Subject"
|
||||
|
||||
with smtplib.SMTP_SSL(smtp_host, 465) as server:
|
||||
server.login(sender, password)
|
||||
server.sendmail(sender, recipient, msg.as_string())
|
||||
```
|
||||
|
||||
## Reading Email
|
||||
|
||||
```python
|
||||
import imaplib
|
||||
from email import message_from_bytes
|
||||
from email.header import decode_header
|
||||
|
||||
imap_host = "claw.163.com"
|
||||
imap_port = 993
|
||||
user = "ephronren@claw.163.com"
|
||||
password = "<from config>"
|
||||
|
||||
with imaplib.IMAP4_SSL(imap_host, imap_port) as mail:
|
||||
mail.login(user, password)
|
||||
mail.select("INBOX")
|
||||
_, data = mail.search(None, "ALL")
|
||||
ids = data[0].split()
|
||||
eid = ids[-1] # latest
|
||||
|
||||
_, msg_data = mail.fetch(eid, "(RFC822)")
|
||||
msg = message_from_bytes(msg_data[0][1])
|
||||
|
||||
def decode_str(s):
|
||||
if s is None:
|
||||
return ""
|
||||
parts = decode_header(s)
|
||||
return " ".join(
|
||||
p.decode(c or "utf-8", errors="replace") if isinstance(p, bytes) else p
|
||||
for p, c in parts
|
||||
)
|
||||
|
||||
print(f"From: {decode_str(msg['From'])}")
|
||||
print(f"Subject: {decode_str(msg['Subject'])}")
|
||||
|
||||
if msg.is_multipart():
|
||||
for part in msg.walk():
|
||||
if part.get_content_type() == "text/plain":
|
||||
payload = part.get_payload(decode=True)
|
||||
charset = part.get_content_charset() or "utf-8"
|
||||
print(payload.decode(charset, errors="replace"))
|
||||
break
|
||||
else:
|
||||
payload = msg.get_payload(decode=True)
|
||||
charset = msg.get_content_charset() or "utf-8"
|
||||
print(payload.decode(charset, errors="replace"))
|
||||
```
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- **Port 25 times out** — 163 SMTP blocks plain SMTP. Use `SMTP_SSL` with port **465** instead.
|
||||
- **Port 465 config mismatch** — config.yaml says port 25, but actual working port is 465.
|
||||
- **send_message tool limitation** — `send_message(action='send', target='email:addr@domain')` does NOT resolve external addresses. Must use Python/terminal directly.
|
||||
- **IMAP fetch auto-marks as read** — `mail.fetch(eid, "(RFC822)")` and even `fetch(eid, "(BODY[HEADER.FIELDS ...])")` automatically set the `\Seen` flag. If you need to check unread count after reading, the count will drop. Use `mail.search(None, "UNSEEN")` before fetching to get accurate unread count.
|
||||
Reference in New Issue
Block a user