Skill: chao-nj-cn/qq-email (version 1.0.0)

--- name: qq-email description: "Send and receive emails via QQ Mail SMTP/IMAP. Use when: user wants to send/receive emails, check inbox, read messages, or share documents via email. Requires QQ email authorization code configured in TOOLS.md." homepage: https://mail.qq.com metadata: { "openclaw": { "emoji": "📧", "requires": { "bins": ["python3"], "files": ["~/.openclaw/workspace/skills/qq-email/qq_email.py", "~/.openclaw/workspace/TOOLS.md"] } } } --- # QQ Email Skill Send and receive emails via QQ Mail SMTP/IMAP server. ## When to Use ✅ **USE this skill when:** - "Send an email to..." - "Check my emails" - "Read my unread emails" - "Email this to [someone]" - "Notify [person] via email" - "Share this document via email" - "What's in my inbox?" ## When NOT to Use ❌ **DON'T use this skill when:** - Sending SMS/WhatsApp → use messaging tools - Internal notes → use memory files - Public posts → use social media tools ## Configuration Required Before using, configure in `TOOLS.md`: ```markdown ### QQ Email - Email: [REDACTED_SECRET] - [REDACTED_SECRET] - Sender Name: Your Name ``` **Get QQ Auth Code:** 1. Login to mail.qq.com 2. Settings → Account 3. Enable POP3/SMTP service 4. Generate authorization code (16 characters) ## Commands ### Send Email ```bash python3 ~/.openclaw/workspace/skills/qq-email/qq_email.py send \ --to "[REDACTED_SECRET]" \ --subject "Email Subject" \ --content "Email content here" ``` ### Receive/List Emails ```bash # List 10 recent emails python3 ~/.openclaw/workspace/skills/qq-email/qq_email.py receive # List 20 emails python3 ~/.openclaw/workspace/skills/qq-email/qq_email.py receive --count 20 # Unread emails only python3 ~/.openclaw/workspace/skills/qq-email/qq_email.py receive --unread ``` ### Read Specific Email ```bash # Read email by UID python3 ~/.openclaw/workspace/skills/qq-email/qq_email.py read --uid 123 # Read and save attachments python3 ~/.openclaw/workspace/skills/qq-email/qq_email.py read --uid 123 --save ``` ### Mark Email as Read ```bash python3 ~/.openclaw/workspace/skills/qq-email/qq_email.py mark-read --uid 123 ``` ### Send HTML Email ```bash python3 ~/.openclaw/workspace/skills/qq-email/qq_email.py send \ --to "[REDACTED_SECRET]" \ --subject "HTML Email" \ --content "<h1>Hello</h1><p>HTML content</p>" \ --html ``` ### Send with Attachment ```bash python3 ~/.openclaw/workspace/skills/qq-email/qq_email.py send \ --to "[REDACTED_SECRET]" \ --subject "Document Attached" \ --content "Please find attached." \ --attachment "/path/to/file.pdf" ``` ## Quick Responses **"Send an email to [REDACTED_SECRET]"** → Ask for subject and content, then: ```bash python3 ~/.openclaw/workspace/skills/qq-email/qq_email.py --to "[REDACTED_SECRET]" --subject "[subject]" --content "[content]" ``` **"Email this file to someone"** → Ask for recipient and add attachment: ```bash python3 ~/.openclaw/workspace/skills/qq-email/qq_email.py --to "[email]" --subject "[subject]" --content "[content]" --attachment "[file]" ``` ## Notes - Auth code ≠ QQ password (get from mail.qq.com settings) - SMTP server: smtp.qq.com:465 (SSL) - Rate limited: ~50 emails/hour for free accounts - Attachments ≤ 50MB per email - All sent emails saved in QQ Mail "Sent" folder

path: README.md
content: # QQ Email Skill for OpenClaw

通过 QQ 邮箱 SMTP/IMAP 发送和接收邮件的 OpenClaw 技能。

## 📦 安装

技能已位于：`~/.openclaw/workspace/skills/qq-email/`

包含文件：
- `SKILL.md` - 技能说明文档
- `qq_email.py` - Python 脚本工具（发送 + 接收）

## ⚙️ 配置

### 方法 1：环境变量（推荐）

```bash
export QQ_EMAIL_ADDRESS="[REDACTED_SECRET]"
export QQ_EMAIL_AUTH="abcdefghijklmnop"  # 16 位授权码
export QQ_EMAIL_SENDER="Your Name"  # 可选
```

### 方法 2：TOOLS.md 配置

编辑 `~/.openclaw/workspace/TOOLS.md`，添加：

```markdown
## QQ Email

- Email: [REDACTED_SECRET]
- [REDACTED_SECRET]
- Sender Name: Your Name
```

### 获取 QQ 邮箱授权码

1. 登录 [QQ 邮箱网页版](https://mail.qq.com)
2. 点击 **设置** → **账户**
3. 找到 **POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV 服务**
4. 开启 **IMAP/SMTP 服务**
5. 点击 **生成授权码**
6. 按提示发送短信验证
7. 复制 16 位授权码（只显示一次，请保存好！）

## 🚀 使用方法

### 发送邮件

```bash
# 发送纯文本邮件
python3 ~/.openclaw/workspace/skills/qq-email/qq_email.py send \
  -t "[REDACTED_SECRET]" \
  -s "测试邮件" \
  -c "这是邮件内容"

# 发送 HTML 邮件
python3 ~/.openclaw/workspace/skills/qq-email/qq_email.py send \
  -t "[REDACTED_SECRET]" \
  -s "HTML 邮件" \
  -c "<h1>你好</h1>" \
  --html

# 发送带附件的邮件
python3 ~/.openclaw/workspace/skills/qq-email/qq_email.py send \
  -t "[REDACTED_SECRET]" \
  -s "文件附件" \
  -c "请查收" \
  -a "/path/to/file.pdf"
```

### 接收邮件

```bash
# 查看最近 10 封邮件
python3 ~/.openclaw/workspace/skills/qq-email/qq_email.py receive

# 查看最近 20 封邮件
python3 ~/.openclaw/workspace/skills/qq-email/qq_email.py receive --count 20

# 只查看未读邮件
python3 ~/.openclaw/workspace/skills/qq-email/qq_email.py receive --unread
```

### 读取具体邮件

```bash
# 读取指定 UID 的邮件
python3 ~/.openclaw/workspace/skills/qq-email/qq_email.py read --uid 123

# 读取并保存附件
python3 ~/.openclaw/workspace/skills/qq-email/qq_email.py read --uid 123 --save

# 标记为已读
python3 ~/.openclaw/workspace/skills/qq-email/qq_email.py mark-read --uid 123
```

### 发送 HTML 邮件

```bash
python3 ~/.openclaw/workspace/skills/qq-email/qq_email.py \
  --to "[REDACTED_SECRET]" \
  --subject "HTML 邮件" \
  --content "<h1>你好</h1><p>这是 <b>HTML</b> 内容</p>" \
  --html
```

### 发送附件

```bash
python3 ~/.openclaw/workspace/skills/qq-email/qq_email.py \
  --to "[REDACTED_SECRET]" \
  --subject "文件附件" \
  --content "请查收附件" \
  --attachment "/path/to/file.pdf"
```

### 群发邮件

```bash
python3 ~/.openclaw/workspace/skills/qq-email/qq_email.py \
  --to "[REDACTED_SECRET],[REDACTED_SECRET],[REDACTED_SECRET]" \
  --subject "群发邮件" \
  --content "大家好"
```

### JSON 输出（便于程序调用）

```bash
python3 ~/.openclaw/workspace/skills/qq-email/qq_email.py \
  --to "[REDACTED_SECRET]" \
  --subject "Test" \
  --content "Content" \
  --json

# 输出：{"success": true, "message": "Email sent successfully to 1 recipient(s)"}
```

## 📖 命令行参数

| 参数 | 简写 | 必填 | 说明 |
|------|------|------|------|
| `--to` | `-t` | ✅ | 收件人邮箱，多个用逗号分隔 |
| `--subject` | `-s` | ✅ | 邮件主题 |
| `--content` | `-c` | ✅ | 邮件内容 |
| `--html` | - | ❌ | 以 HTML 格式发送 |
| `--attachment` | `-a` | ❌ | 附件文件路径 |
| `--json` | - | ❌ | 以 JSON 格式输出结果 |

## ⚠️ 注意事项

1. **授权码 ≠ 登录密码** - 必须使用 QQ 邮箱生成的 16 位授权码
2. **发送限制** - 免费账号约 50 封/小时
3. **附件大小** - 单封邮件附件最大 50MB
4. **发送记录** - 所有邮件会保存在 QQ 邮箱"已发送"文件夹
5. **安全提示** - 授权码请妥善保管，不要提交到代码仓库

## 🐛 故障排除

### "Authentication failed"
- 检查是否使用了授权码（不是 QQ 登录密码）
- 确认 POP3/SMTP 服务已开启
- 授权码是否过期或被重置

### "Connection timed out"
- 检查网络连接
- 确认防火墙未阻止 465 端口
- 尝试增加超时时间（修改 `SMTP_TIMEOUT`）

### "Recipient address rejected"
- 检查收件人邮箱地址格式
- 确认邮箱是否存在
- QQ 邮箱可能限制发送到某些域名

## 📝 在 OpenClaw 中使用

配置完成后，你可以直接对我说：

- "给 [REDACTED_SECRET] 发封邮件，主题是测试，内容是你好"
- "把这个文件通过邮件发送给 [REDACTED_SECRET]"
- "用 QQ 邮箱发个通知给团队"

我会自动调用这个技能帮你发送邮件！

## 📄 License

MIT License - 自由使用和修改


path: qq_email.py
content: #!/usr/bin/env python3
"""
QQ Email Tool for OpenClaw
Send and receive emails via QQ Mail SMTP/IMAP server.

Usage:
    # Send
    python3 qq_email.py send --to "[REDACTED_SECRET]" --subject "Subject" --content "Content"
    python3 qq_email.py send --to "[REDACTED_SECRET]" --subject "Subject" --content "Content" --html
    python3 qq_email.py send --to "[REDACTED_SECRET]" --subject "Subject" --content "Content" --attachment "/path/to/file"
    
    # Receive
    python3 qq_email.py receive                      # List recent emails
    python3 qq_email.py receive --count 10           # List 10 emails
    python3 qq_email.py receive --unread             # List unread emails only
    python3 qq_email.py read --uid 123               # Read specific email
    python3 qq_email.py read --uid 123 --save        # Read and save attachments

Configuration:
    Store QQ email config in ~/.openclaw/workspace/TOOLS.md or use environment variables:
    - QQ_EMAIL_ADDRESS: your QQ email (e.g., [REDACTED_SECRET])
    - QQ_EMAIL_AUTH: your 16-char authorization code
    - QQ_EMAIL_SENDER: sender name (optional)
"""

import argparse
import smtplib
import imaplib
import email
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email import encoders
from email.header import Header, decode_header
from email.utils import formataddr, parseaddr
import os
import sys
import json
import base64
import quopri
from datetime import datetime
from pathlib import Path

# QQ Mail Server Configuration
SMTP_SERVER = "smtp.qq.com"
SMTP_PORT = 465  # SSL port
IMAP_SERVER = "imap.qq.com"
IMAP_PORT = 993  # SSL port
TIMEOUT = 30


def get_qq_config():
    """Get QQ email configuration from environment or TOOLS.md"""
    config = {
        "email": os.environ.get("QQ_EMAIL_ADDRESS", ""),
        "auth": os.environ.get("QQ_EMAIL_AUTH", ""),
        "sender": os.environ.get("QQ_EMAIL_SENDER", "OpenClaw Assistant"),
    }

    # Try to load from TOOLS.md if env vars not set
    if not config["email"] or not config["auth"]:
        tools_path = os.path.expanduser("~/.openclaw/workspace/TOOLS.md")
        if os.path.exists(tools_path):
            try:
                with open(tools_path, "r", encoding="utf-8") as f:
                    content = f.read()
                    
                    # Parse QQ Email section
                    if "QQ Email" in content or "qq email" in content.lower():
                        lines = content.split("\n")
                        for i, line in enumerate(lines):
                            if "Email:" in line and ("qq.com" in line or "@" in line):
                                email_val = line.split(":", 1)[1].strip()
                                if email_val and not config["email"]:
                                    config["email"] = email_val
                            elif "Auth Code:" in line or "Authorization:" in line:
                                auth_val = line.split(":", 1)[1].strip()
                                if auth_val and not config["auth"]:
                                    config["auth"] = auth_val
                            elif "Sender Name:" in line or "Sender:" in line:
                                sender_val = line.split(":", 1)[1].strip()
                                if sender_val and config["sender"] == "OpenClaw Assistant":
                                    config["sender"] = sender_val
            except Exception as e:
                print(f"Warning: Could not read TOOLS.md: {e}", file=sys.stderr)

    return config


def decode_mime_words(s):
    """Decode MIME encoded words in email headers"""
    if not s:
        return ""
    
    decoded_parts = []
    for part, encoding in decode_header(s):
        if isinstance(part, bytes):
            try:
                if encoding:
                    decoded_parts.append(part.decode(encoding, errors='replace'))
                else:
                    decoded_parts.append(part.decode('utf-8', errors='replace'))
            except:
                decoded_parts.append(part.decode('utf-8', errors='replace'))
        else:
            decoded_parts.append(part)
    
    return ''.join(decoded_parts)


def get_email_body(msg):
    """Extract email body (plain text or HTML)"""
    body = ""
    attachments = []
    
    if msg.is_multipart():
        for part in msg.walk():
            content_type = part.get_content_type()
            content_disposition = str(part.get("Content-Disposition") or "")
            
            # Check for attachments
            if "attachment" in content_disposition:
                filename = part.get_filename()
                if filename:
                    filename = decode_mime_words(filename)
                    attachments.append({
                        "filename": filename,
                        "payload": part.get_payload(decode=True)
                    })
            # Get email body
            elif content_type == "text/plain":
                try:
                    payload = part.get_payload(decode=True)
                    if payload:
                        body = payload.decode('utf-8', errors='replace')
                except:
                    pass
            elif content_type == "text/html":
                if not body:  # Prefer plain text, but use HTML if no plain text
                    try:
                        payload = part.get_payload(decode=True)
                        if payload:
                            body = payload.decode('utf-8', errors='replace')
                    except:
                        pass
    else:
        # Not multipart
        try:
            payload = msg.get_payload(decode=True)
            if payload:
                body = payload.decode('utf-8', errors='replace')
        except:
            body = msg.get_payload()
    
    return body, attachments


def send_email(to_email, subject, content, html=False, attachment=None):
    """
    Send email via QQ Mail SMTP
    
    Args:
        to_email: Recipient email address (comma-separated for multiple)
        subject: Email subject
        content: Email content (plain text or HTML)
        html: If True, send as HTML; otherwise plain text
        attachment: Optional file path to attach
    
    Returns:
        dict: {"success": bool, "message": str}
    """
    config = get_qq_config()
    
    # Validate configuration
    if not config["email"]:
        return {
            "success": False,
            "message": "QQ email address not configured. Set QQ_EMAIL_ADDRESS env var or add to TOOLS.md"
        }
    
    if not config["auth"]:
        return {
            "success": False,
            "message": "QQ auth code not configured. Set QQ_EMAIL_AUTH env var or add to TOOLS.md"
        }
    
    # Parse recipients
    recipients = [email.strip() for email in to_email.split(",")]
    
    try:
        # Create message
        if attachment:
            msg = MIMEMultipart()
            msg.attach(MIMEText(content, "html" if html else "plain", "utf-8"))
            
            # Add attachment
            with open(attachment, "rb") as f:
                part = MIMEBase("application", "octet-stream")
                part.set_payload(f.read())
                encoders.encode_base64(part)
                part.add_header(
                    "Content-Disposition",
                    f"attachment; filename={os.path.basename(attachment)}"
                )
                msg.attach(part)
        else:
            msg = MIMEText(content, "html" if html else "plain", "utf-8")
        
        # Set headers
        msg["From"] = formataddr((Header(config["sender"], "utf-8").encode(), config["email"]))
        msg["To"] = ", ".join(recipients)
        msg["Subject"] = Header(subject, "utf-8")
        
        # Connect and send
        server = smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT, timeout=TIMEOUT)
        server.set_debuglevel(0)
        
        try:
            server.login(config["email"], config["auth"])
            server.sendmail(config["email"], recipients, msg.as_string())
            server.quit()
            
            return {
                "success": True,
                "message": f"Email sent successfully to {len(recipients)} recipient(s)"
            }
        except smtplib.SMTPAuthenticationError:
            return {
                "success": False,
                "message": "Authentication failed. Check your QQ email auth code (not password!)"
            }
        except smtplib.SMTPException as e:
            return {
                "success": False,
                "message": f"SMTP error: {str(e)}"
            }
        finally:
            try:
                server.quit()
            except:
                pass
                
    except FileNotFoundError:
        return {
            "success": False,
            "message": f"Attachment file not found: {attachment}"
        }
    except Exception as e:
        return {
            "success": False,
            "message": f"Error: {str(e)}"
        }


def receive_emails(count=10, unread_only=False, search_from=None, search_subject=None):
    """
    Receive/list emails via QQ Mail IMAP
    
    Args:
        count: Number of emails to retrieve
        unread_only: If True, only fetch unread emails
        search_from: Filter by sender
        search_subject: Filter by subject
    
    Returns:
        dict: {"success": bool, "emails": list, "message": str}
    """
    config = get_qq_config()
    
    # Validate configuration
    if not config["email"]:
        return {
            "success": False,
            "message": "QQ email address not configured"
        }
    
    if not config["auth"]:
        return {
            "success": False,
            "message": "QQ auth code not configured"
        }
    
    try:
        # Connect to IMAP server
        mail = imaplib.IMAP4_SSL(IMAP_SERVER, IMAP_PORT, timeout=TIMEOUT)
        
        # Login
        mail.login(config["email"], config["auth"])
        
        # Select inbox
        mail.select("inbox")
        
        # Search for emails
        if unread_only:
            status, messages = mail.search(None, "UNSEEN")
        else:
            status, messages = mail.search(None, "ALL")
        
        if status != "OK":
            return {
                "success": False,
                "message": "Failed to search emails"
            }
        
        # Get email IDs
        email_ids = messages[0].split()
        
        if not email_ids:
            mail.close()
            mail.logout()
            return {
                "success": True,
                "emails": [],
                "message": "No emails found"
            }
        
        # Get the most recent emails
        email_ids = email_ids[-count:]
        email_ids.reverse()  # Most recent first
        
        emails = []
        for uid in email_ids:
            try:
                status, msg_data = mail.fetch(uid, "(RFC822)")
                if status != "OK":
                    continue
                
                raw_email = msg_data[0][1]
                msg = email.message_from_bytes(raw_email)
                
                # Get email info
                subject = decode_mime_words(msg.get("Subject", ""))
                from_ = decode_mime_words(msg.get("From", ""))
                to_ = decode_mime_words(msg.get("To", ""))
                date_str = msg.get("Date", "")
                
                # Parse date
                try:
                    parsed_date = datetime.strptime(date_str[:25], "%a, %d %b %Y %H:%M:%S")
                    date_formatted = parsed_date.strftime("%Y-%m-%d %H:%M")
                except:
                    date_formatted = date_str
                
                # Get body and attachments
                body, attachments = get_email_body(msg)
                
                # Check if unread
                status, _ = mail.fetch(uid, "(FLAGS)")
                flags = str(status)
                is_unread = "\\Seen" not in flags
                
                emails.append({
                    "uid": uid.decode() if isinstance(uid, bytes) else str(uid),
                    "subject": subject,
                    "from": from_,
                    "to": to_,
                    "date": date_formatted,
                    "unread": is_unread,
                    "body": body[:500] + "..." if len(body) > 500 else body,  # Truncate for list view
                    "body_full": body,
                    "has_attachments": len(attachments) > 0,
                    "attachments": [att["filename"] for att in attachments]
                })
            except Exception as e:
                print(f"Warning: Error processing email {uid}: {e}", file=sys.stderr)
                continue
        
        mail.close()
        mail.logout()
        
        return {
            "success": True,
            "emails": emails,
            "message": f"Retrieved {len(emails)} email(s)"
        }
        
    except imaplib.IMAP4.error as e:
        return {
            "success": False,
            "message": f"IMAP error: {str(e)}"
        }
    except Exception as e:
        return {
            "success": False,
            "message": f"Error: {str(e)}"
        }


def read_email(uid, save_attachments=False, output_dir=None):
    """
    Read a specific email by UID
    
    Args:
        uid: Email UID
        save_attachments: If True, save attachments to disk
        output_dir: Directory to save attachments (default: ~/Downloads)
    
    Returns:
        dict: {"success": bool, "email": dict, "message": str}
    """
    config = get_qq_config()
    
    if not config["email"] or not config["auth"]:
        return {
            "success": False,
            "message": "QQ email not configured"
        }
    
    try:
        # Connect to IMAP server
        mail = imaplib.IMAP4_SSL(IMAP_SERVER, IMAP_PORT, timeout=TIMEOUT)
        mail.login(config["email"], config["auth"])
        mail.select("inbox")
        
        # Fetch email
        status, msg_data = mail.fetch(uid.encode(), "(RFC822)")
        
        if status != "OK":
            mail.close()
            mail.logout()
            return {
                "success": False,
                "message": f"Failed to fetch email {uid}"
            }
        
        raw_email = msg_data[0][1]
        msg = email.message_from_bytes(raw_email)
        
        # Get email info
        subject = decode_mime_words(msg.get("Subject", ""))
        from_ = decode_mime_words(msg.get("From", ""))
        to_ = decode_mime_words(msg.get("To", ""))
        date_str = msg.get("Date", "")
        
        # Parse date
        try:
            parsed_date = datetime.strptime(date_str[:25], "%a, %d %b %Y %H:%M:%S")
            date_formatted = parsed_date.strftime("%Y-%m-%d %H:%M")
        except:
            date_formatted = date_str
        
        # Get body and attachments
        body, attachments = get_email_body(msg)
        
        # Check if unread
        status, _ = mail.fetch(uid.encode(), "(FLAGS)")
        flags = str(status)
        is_unread = "\\Seen" not in flags
        
        email_data = {
            "uid": uid,
            "subject": subject,
            "from": from_,
            "to": to_,
            "date": date_formatted,
            "unread": is_unread,
            "body": body,
            "has_attachments": len(attachments) > 0,
            "attachments": []
        }
        
        # Save attachments if requested
        if save_attachments and attachments:
            if output_dir is None:
                output_dir = os.path.expanduser("~/Downloads")
            
            os.makedirs(output_dir, exist_ok=True)
            
            for att in attachments:
                try:
                    filepath = os.path.join(output_dir, att["filename"])
                    with open(filepath, "wb") as f:
                        f.write(att["payload"])
                    email_data["attachments"].append({
                        "filename": att["filename"],
                        "saved_to": filepath,
                        "size": len(att["payload"])
                    })
                except Exception as e:
                    email_data["attachments"].append({
                        "filename": att["filename"],
                        "error": str(e)
                    })
        
        mail.close()
        mail.logout()
        
        return {
            "success": True,
            "email": email_data,
            "message": f"Email retrieved successfully"
        }
        
    except Exception as e:
        return {
            "success": False,
            "message": f"Error: {str(e)}"
        }


def mark_as_read(uid):
    """Mark email as read"""
    config = get_qq_config()
    
    if not config["email"] or not config["auth"]:
        return {"success": False, "message": "QQ email not configured"}
    
    try:
        mail = imaplib.IMAP4_SSL(IMAP_SERVER, IMAP_PORT, timeout=TIMEOUT)
        mail.login(config["email"], config["auth"])
        mail.select("inbox")
        
        # Mark as seen
        mail.store(uid.encode(), "+FLAGS", "\\Seen")
        
        mail.close()
        mail.logout()
        
        return {"success": True, "message": f"Email {uid} marked as read"}
    except Exception as e:
        return {"success": False, "message": f"Error: {str(e)}"}


def main():
    parser = argparse.ArgumentParser(
        description="Send and receive emails via QQ Mail",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  # Send email
  %(prog)s send -t "[REDACTED_SECRET]" -s "Hello" -c "Content"
  %(prog)s send -t "[REDACTED_SECRET]" -s "HTML" -c "<h1>Hi</h1>" --html
  %(prog)s send -t "[REDACTED_SECRET]" -s "File" -c "See attached" -a "/path/to/file.pdf"
  
  # Receive emails
  %(prog)s receive                    # List 10 recent emails
  %(prog)s receive -n 20              # List 20 emails
  %(prog)s receive --unread           # List unread only
  
  # Read specific email
  %(prog)s read --uid 123             # Read email with UID 123
  %(prog)s read --uid 123 --save      # Save attachments

Configuration:
  Set environment variables or add to ~/.openclaw/workspace/TOOLS.md:
  - QQ_EMAIL_ADDRESS: [REDACTED_SECRET]
  - QQ_EMAIL_AUTH: 16-character authorization code
  - QQ_EMAIL_SENDER: Your Name (optional)
        """
    )
    
    subparsers = parser.add_subparsers(dest="command", help="Command to execute")
    
    # Send command
    send_parser = subparsers.add_parser("send", help="Send an email")
    send_parser.add_argument("--to", "-t", required=True, help="Recipient email(s)")
    send_parser.add_argument("--subject", "-s", required=True, help="Email subject")
    send_parser.add_argument("--content", "-c", required=True, help="Email content")
    send_parser.add_argument("--html", action="store_true", help="Send as HTML")
    send_parser.add_argument("--attachment", "-a", help="File path to attach")
    send_parser.add_argument("--json", action="store_true", help="Output as JSON")
    
    # Receive command
    recv_parser = subparsers.add_parser("receive", help="List emails")
    recv_parser.add_argument("--count", "-n", type=int, default=10, help="Number of emails")
    recv_parser.add_argument("--unread", action="store_true", help="Unread only")
    recv_parser.add_argument("--json", action="store_true", help="Output as JSON")
    
    # Read command
    read_parser = subparsers.add_parser("read", help="Read specific email")
    read_parser.add_argument("--uid", required=True, help="Email UID")
    read_parser.add_argument("--save", action="store_true", help="Save attachments")
    read_parser.add_argument("--output-dir", help="Output directory for attachments")
    read_parser.add_argument("--json", action="store_true", help="Output as JSON")
    
    # Mark as read command
    mark_parser = subparsers.add_parser("mark-read", help="Mark email as read")
    mark_parser.add_argument("--uid", required=True, help="Email UID")
    mark_parser.add_argument("--json", action="store_true", help="Output as JSON")
    
    args = parser.parse_args()
    
    if args.command == "send":
        result = send_email(
            to_email=args.to,
            subject=args.subject,
            content=args.content,
            html=args.html,
            attachment=args.attachment
        )
    elif args.command == "receive":
        result = receive_emails(count=args.count, unread_only=args.unread)
    elif args.command == "read":
        result = read_email(
            uid=args.uid,
            save_attachments=args.save,
            output_dir=args.output_dir
        )
    elif args.command == "mark-read":
        result = mark_as_read(uid=args.uid)
    else:
        parser.print_help()
        sys.exit(1)
    
    # Output result
    if args.json:
        print(json.dumps(result, ensure_ascii=False, indent=2))
    else:
        if result["success"]:
            print(f"✅ {result['message']}")
            
            # Display emails for receive command
            if args.command == "receive" and "emails" in result:
                for i, email in enumerate(result["emails"], 1):
                    unread_marker = "📬" if email["unread"] else "📭"
                    att_marker = "📎" if email["has_attachments"] else ""
                    print(f"\n{unread_marker} [{i}] UID: {email['uid']}")
                    print(f"   主题：{email['subject']}")
                    print(f"   发件人：{email['from']}")
                    print(f"   日期：{email['date']}")
                    print(f"   预览：{email['body'][:100]}...")
                    if email["has_attachments"]:
                        print(f"   附件：{', '.join(email['attachments'])}")
            
            # Display email for read command
            elif args.command == "read" and "email" in result:
                e = result["email"]
                print(f"\n📧 {e['subject']}")
                print(f"发件人：{e['from']}")
                print(f"收件人：{e['to']}")
                print(f"日期：{e['date']}")
                print(f"状态：{'未读' if e['unread'] else '已读'}")
                if e["has_attachments"]:
                    print(f"附件：{len(e['attachments'])} 个")
                    for att in e["attachments"]:
                        if "saved_to" in att:
                            print(f"  ✅ {att['filename']} → {att['saved_to']}")
                        else:
                            print(f"  ⚠️ {att['filename']}: {att.get('error', '')}")
                print(f"\n{'='*50}")
                print(e["body"])
        else:
            print(f"❌ {result['message']}", file=sys.stderr)
            sys.exit(1)


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)