Back to skills
SkillHub ClubShip Full StackFull Stack

send-email

邮件发送工具。配置 SMTP 发件人后,通过脚本发送纯文本或 HTML 邮件,支持附件、抄送、密送。在需要发送邮件通知、报告、自动化邮件时触发。

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
3,030
Hot score
99
Updated
March 20, 2026
Overall rating
C4.8
Composite score
4.8
Best-practice grade
C61.1

Install command

npx @skill-hub/cli install openclaw-skills-send-email-tool

Repository

openclaw/skills

Skill path: skills/flyingtimes/send-email-tool

邮件发送工具。配置 SMTP 发件人后,通过脚本发送纯文本或 HTML 邮件,支持附件、抄送、密送。在需要发送邮件通知、报告、自动化邮件时触发。

Open repository

Best for

Primary workflow: Ship Full Stack.

Technical facets: Full Stack.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: openclaw.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install send-email into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/openclaw/skills before adding send-email to shared team environments
  • Use send-email for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: send-email
description: 邮件发送工具。配置 SMTP 发件人后,通过脚本发送纯文本或 HTML 邮件,支持附件、抄送、密送。在需要发送邮件通知、报告、自动化邮件时触发。
---

# Send Email

通过 SMTP 发送邮件的工具,支持 keyring 密钥管理。

## 功能

- ✅ 支持 SMTP 邮件发送(Gmail、QQ 邮箱、163 邮箱等)
- ✅ 支持纯文本和 HTML 格式邮件
- ✅ **支持模板渲染**(使用 `--template` 参数,支持自定义模板)
- ✅ **支持内嵌图片**(图片直接显示在邮件正文中,不是链接)
- ✅ **自动检测 Markdown 格式**(含图片自动嵌入)
- ✅ 支持附件(文档、图片等)
- ✅ 支持抄送(CC)和密送(BCC)
- ✅ 配置持久化,避免重复输入
- ✅ **密钥管理**:支持 keyring 安全存储密码(推荐)

## 密钥管理

### ⚠️ 重要:密码安全

本技能**强制使用 keyring** 管理发件人邮箱和密码,避免敏感信息暴露在命令行或上下文中。

### 安装 keyring

```bash
pip install keyring
```

如果 keyring 未安装,脚本会自动使用备用存储方案(base64 编码的本地文件)。

### 首次使用:保存发件人邮箱

在发送邮件前,必须先保存发件人邮箱到 keyring:

```bash
# 保存发件人邮箱(会提示输入)
python3 send_email.py username --save --email [email protected]

# 或只运行 --save,然后交互输入
python3 send_email.py username --save
```

### 保存密码

```bash
# 保存密码(会提示输入)
python3 send_email.py password --save
```

### 删除密钥

```bash
# 删除发件人邮箱
python3 send_email.py username --delete

# 删除密码
python3 send_email.py password --delete
```

### 查看密钥状态

```bash
# 查看发件人邮箱
python3 send_email.py username

# 查看密码状态
python3 send_email.py password
```

### ⚠️ 安全提醒

- **不要**在命令行参数中传递邮箱或密码
- **不要**使用 `--email` 参数直接指定发件人
- 始终通过 `username --save` 和 `password --save` 命令管理密钥
- 邮箱和密码会自动从 keyring 读取,无需每次输入
- 默认邮箱:[email protected]

---

## 快速开始

### 0. 安装依赖(可选)

**推荐安装 `markdown` 库以支持 Markdown 自动转换:**

```bash
pip install markdown keyring
```

如果不安装 `markdown` 库,脚本仍可正常发送纯文本和 HTML 邮件,但无法自动转换 Markdown。

### 1. 首次配置

```bash
cd $CLAWD/skills/send-email/scripts

# 配置 SMTP 服务器(中国移动邮箱默认配置)
python3 send_email.py smtp --host smtp.gd.chinamobile.com --port 465 --no-tls

# 配置发件人名称
python3 send_email.py sender --name "Your Name"

# 保存发件人邮箱到 keyring
python3 send_email.py username --save --email [email protected]

# 查看当前配置
python3 send_email.py config
```

**中国移动邮箱默认配置:**

| 配置项 | 值 |
|-------|-----|
| SMTP 服务器 | smtp.gd.chinamobile.com |
| 端口 | 465 (SSL) |
| TLS | ❌ (使用 SSL) |
| 默认邮箱 | [email protected] |

**重要提示:** 如果使用 Gmail,需要生成「应用专用密码」(App Password),而不是使用账户密码。

---

### 2. 发送邮件

#### 首次使用:保存密码

```bash
python3 send_email.py password --save
# 按提示输入密码
```

#### 基础发送(纯文本)

```bash
python3 send_email.py send \
  --to [email protected] \
  --subject "邮件主题" \
  --body "邮件正文内容"
```

#### HTML 邮件

```bash
python3 send_email.py send \
  --to [email protected] \
  --subject "HTML 邮件" \
  --body "<h1>标题</h1><p>正文内容</p>" \
  --html
```

#### 带附件的邮件

```bash
python3 send_email.py send \
  --to [email protected] \
  --subject "带附件的邮件" \
  --body "请查看附件" \
  --attachments "/path/to/file1.pdf" "/path/to/file2.png"
```

#### 抄送和密送

```bash
python3 send_email.py send \
  --to [email protected] \
  --cc [email protected] [email protected] \
  --bcc [email protected] \
  --subject "多人邮件" \
  --body "邮件正文"
```

#### 自动检测 Markdown + 内嵌图片(最强推荐)⭐⭐⭐

**功能:** 自动检测 Markdown 格式,自动提取并内嵌图片,无需手动指定!

```bash
python3 send_email.py send \
  --to [email protected] \
  --subject "Markdown 邮件(自动检测图片)" \
  --body "# 标题\n\n![图片说明](/path/to/image.png)\n\n这是正文内容"
```

**工作流程:**
1. 脚本自动检测 Markdown 格式
2. 自动提取 Markdown 中的图片路径(支持 `![alt](path)` 语法)
3. 自动将 Markdown 转换为 HTML
4. 自动将图片转换为 CID 引用(`src="cid:image"`)
5. 图片直接显示在邮件正文中

**示例:发送包含多张图片的 Markdown**

```bash
python3 send_email.py send \
  --to [email protected] \
  --subject "产品报告" \
  --body "$(cat report.md)"
```

其中 `report.md` 内容:

```markdown
# 产品更新报告

## 新功能展示

这是第一个功能截图:

![功能1](/path/to/screenshot1.png)

这是第二个功能截图:

![功能2](/path/to/screenshot2.png)

## 总结

如有问题,请联系我们。
```

**结果:** 收件人会收到一封格式美观的 HTML 邮件,图片直接显示在正文中!

---

#### 手动指定内嵌图片

**功能:** 图片直接显示在邮件正文中,不是链接。使用 CID(Content-ID)技术,兼容所有主流邮件客户端。

```bash
python3 send_email.py send \
  --to [email protected] \
  --subject "带内嵌图片的邮件" \
  --html \
  --body "<h1>标题</h1><p>正文内容</p><img src='/path/to/image.png'>" \
  --inline-images "/path/to/image.png"
```

**工作原理:**
1. 脚本会自动将 HTML 中的图片路径替换为 CID 引用(`src="cid:filename"`)
2. 图片作为内嵌资源添加到邮件中
3. 邮件客户端会直接显示图片,不需要点击链接

**多张图片示例:**

```bash
python3 send_email.py send \
  --to [email protected] \
  --subject "产品截图" \
  --html \
  --body "<h1>产品展示</h1><img src='/path/to/screenshot1.png'><br><img src='/path/to/screenshot2.png'>" \
  --inline-images "/path/to/screenshot1.png" "/path/to/screenshot2.png"
```

**重要提示:**
- `--inline-images` 必须配合 `--html` 参数使用(如未指定,会自动启用)
- 图片路径必须与 HTML 中的 `src` 属性完全一致
- 支持 PNG、JPG、JPEG、GIF、WebP 格式
- CID 会自动使用文件名(去掉扩展名),例如 `image.png` → `cid:image`

---

## 参数说明

### 发送命令 (`send`)

| 参数 | 说明 | 必填 |
|------|------|------|
| `--to` | 收件人邮箱 | ✅ |
| `--to-name` | 收件人名称 | ❌ |
| `--subject` | 邮件主题 | ✅ |
| `--body` | 邮件正文 | ✅ |
| `--html` | 使用 HTML 格式 | ❌ |
| `--template` | 使用指定模板渲染邮件(模板文件名) | ❌ |
| `--title` | 邮件标题(模板中使用,默认:"邮件摘要") | ❌ |
| `--attachments` | 附件路径(可多个) | ❌ |
| `--inline-images` | 内嵌图片路径(可多个,仅 HTML 模式) | ❌ |
| `--cc` | 抄送邮箱(可多个) | ❌ |
| `--bcc` | 密送邮箱(可多个) | ❌ |

### 发件人邮箱管理命令 (`username`)

| 参数 | 说明 |
|------|------|
| `--save` | 保存发件人邮箱到 keyring(会提示输入) |
| `--delete` | 删除保存的发件人邮箱 |

### 密码管理命令 (`password`)

| 参数 | 说明 |
|------|------|
| `--save` | 保存密码到 keyring(会提示输入) |
| `--delete` | 删除保存的密码 |

---

## 配置文件

配置文件保存在:`~/.send_email_config.json`

示例配置:
```json
{
  "smtp": {
    "host": "smtp.gd.chinamobile.com",
    "port": 465,
    "use_tls": false
  },
  "sender": {
    "name": "Your Name"
  }
}
```

**注意:** 发件人邮箱通过 `username --save` 命令存储在 keyring 中,不在配置文件中。

---

## 完整示例

### 示例 1: 发送 Markdown 邮件(自动检测并内嵌图片)⭐

创建一个 Markdown 文件 `newsletter.md`:

```markdown
# 今日新闻摘要

## 科技头条

AI 技术持续突破,以下是最新进展:

![AI 演示](/path/to/ai-demo.png)

## 产品更新

新功能界面展示:

![新功能](/path/to/new-feature.png)

---

如需了解更多,请访问我们的官网。
```

发送邮件(超级简单,无需指定任何图片参数!):

```bash
python3 send_email.py send \
  --to [email protected] \
  --subject "今日新闻摘要" \
  --body "$(cat newsletter.md)"
```

**结果:** 脚本会自动:
1. 检测到 Markdown 格式
2. 提取 2 张图片路径
3. 转换为 HTML 并替换图片为 CID 引用
4. 内嵌图片到邮件中
5. 发送格式美观的 HTML 邮件

---

### 示例 2: 发送带内嵌图片的 HTML 邮件(手动指定)

假设你有两张产品截图:

```bash
cd ~/clawd/skills/send-email/scripts

# 准备 HTML 内容
cat > email_content.html << 'EOF'
<h1>产品更新通知</h1>
<p>大家好!</p>
<p>这是我们最新的产品截图:</p>
<img src="/Users/clark/Pictures/screenshot1.png" style="max-width: 600px; border-radius: 8px;">
<p>第二个功能展示:</p>
<img src="/Users/clark/Pictures/screenshot2.png" style="max-width: 600px; border-radius: 8px;">
<p>如有问题,请随时联系我们!</p>
<p>Best regards,<br>产品团队</p>
EOF

# 发送邮件
python3 send_email.py send \
  --to [email protected] \
  --subject "产品更新 - 新功能截图" \
  --html \
  --body "$(cat email_content.html)" \
  --inline-images "/Users/clark/Pictures/screenshot1.png" "/Users/clark/Pictures/screenshot2.png"
```

**结果:** 收件人打开邮件后,图片会直接显示在邮件正文中,不需要点击链接。

---

### 示例 3: 手动指定内嵌图片(HTML 模式)

假设你有现成的 HTML 内容:

```bash
python3 send_email.py send \
  --to [email protected] \
  --subject "产品截图" \
  --html \
  --body "<h1>产品展示</h1><img src='/path/to/image.png'>" \
  --inline-images "/path/to/image.png"
```

**何时使用手动指定:**
- 已有现成的 HTML 内容
- 不想使用 Markdown 转换
- 需要精确控制 CID 引用

---

### 示例 4: 结合 X 推文技能(自动工作流)⚡

假设你已经使用 `crawl-from-x` 和 `translate` 技能抓取并翻译了 X 推文,结果保存在 `posts_zh.md`。

现在发送邮件(一键搞定!):

```bash
python3 send_email.py send \
  --to [email protected] \
  --subject "今日 X 推文摘要" \
  --body "$(cat /path/to/posts_zh.md)"
```

**完整工作流:**

```bash
# 步骤 1: 抓取 X 推文
crawl-from-x 抓取

# 步骤 2: 翻译为中文
使用 translate 技能翻译最新的文件

# 步骤 3: 发送邮件(自动检测 Markdown + 图片)
python3 send_email.py send \
  --to [email protected] \
  --subject "今日 X 推文摘要" \
  --body "$(cat $CLAWD/skills/crawl-from-x/results/*_zh.md)"
```

**自动化脚本:**

创建 `send-x-news.sh`:

```bash
#!/bin/bash
cd ~/clawd/skills/crawl-from-x/scripts
python3 craw_hot.py crawl

LATEST_MD=$(ls -t ~/clawd/skills/crawl-from-x/results/*.md | head -1)
python3 ~/clawd/skills/send-email/scripts/send_email.py send \
  --to [email protected] \
  --subject "$(date +'%Y-%m-%d') X 推文摘要" \
  --body "$(cat $LATEST_MD)"
```

添加到 cron:

```bash
crontab -e
# 添加:每天早上 8:00 执行
0 8 * * * ~/clawd/skills/send-email/scripts/send-x-news.sh
```

---

## 模板功能

### 使用模板渲染邮件

使用 `--template` 参数可以让邮件内容使用指定的模板进行渲染,提供更美观的视觉效果。

```bash
python3 send_email.py send \
  --to [email protected] \
  --subject "今日 X 推文摘要" \
  --body "$(cat posts_zh.md)" \
  --template default \
  --title "X 帖子摘要"
```

**参数说明:**
- `--template`:模板文件名(不含 .html 扩展名)
  - 默认模板:`default`(仿照 x.com 样式)
  - 模板位置:`templates/` 目录
- `--title`:邮件标题(模板中使用,默认:"邮件摘要")

### 默认模板(default)

send-email 技能内置了一个名为 `default` 的默认模板,具有以下特点:

- **设计风格**:现代、简约、商务、大气
- **视觉参考**:仿照 x.com 网页样式
- **布局**:卡片式布局,圆角边框
- **配色**:
  - 背景:白色(#ffffff)
  - 主要文字:深色(#0f1419)
  - 次要文字:灰色(#536471)
  - 链接:蓝色(#1d9bf0)
  - 边框:浅灰色(#eff3f4)
- **响应式**:适配移动端和桌面端
- **支持内容**:文字、图片、链接

**模板变量:**
- `{{title}}`:邮件标题(通过 `--title` 参数设置)
- `{{subtitle}}`:副标题(自动生成时间戳)
- `{{content}}`:邮件正文内容

### 自定义模板

如果默认模板不满足需求,可以创建自定义模板:

1. 在 `templates/` 目录下创建新的 HTML 文件
2. 使用 `{{title}}`、`{{subtitle}}`、`{{content}}` 作为变量占位符
3. 使用时指定模板名称:

```bash
python3 send_email.py send \
  --to [email protected] \
  --subject "每周报告" \
  --body "$(cat report.md)" \
  --template my-template
```

**模板示例:** `templates/default.html`

```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>{{title}}</title>
    <style>
        /* 自定义样式 */
        body { font-family: Arial, sans-serif; }
        .container { max-width: 600px; margin: 0 auto; }
        /* ... */
    </style>
</head>
<body>
    <div class="container">
        <h1>{{title}}</h1>
        <p class="subtitle">{{subtitle}}</p>
        <div class="content">
            {{content}}
        </div>
    </div>
</body>
</html>
```

### 模板 + Markdown 自动检测

模板功能与 Markdown 自动检测完全兼容:

```bash
# 检测 Markdown → 转换为 HTML → 应用模板 → 发送
python3 send_email.py send \
  --to [email protected] \
  --subject "X 推文摘要" \
  --body "$(cat posts_zh.md)" \
  --template default
```

**处理流程:**
1. 检测 Markdown 格式
2. 提取并内嵌图片
3. 转换为 HTML
4. 应用模板
5. 发送邮件

### 模板目录结构

```
send-email/
├── scripts/
│   └── send_email.py
└── templates/
    ├── default.html       # 默认模板(仿照 x.com)
    ├── my-template.html   # 自定义模板 1
    └── weekly.html       # 自定义模板 2
```

---

## 使用建议

1. **密钥管理(强制):** 首次使用前必须运行 `python send_email.py username --save` 和 `python send_email.py password --save` 分别保存发件人邮箱和密码。这些信息会安全存储在 keyring 中,不会暴露在命令行或上下文中。

2. **不要传递密钥:** 发送邮件时**不要**使用 `--email` 或 `--password` 参数,这些信息会自动从 keyring 读取。这是为了保护密钥安全。

3. **中国移动邮箱:** 默认配置为 `smtp.gd.chinamobile.com:465`(SSL),默认发件人邮箱为 `[email protected]`。

4. **Markdown 自动检测(推荐):**
   - 脚本会自动检测 Markdown 格式(标题、粗体、列表、图片等)
   - 自动提取 Markdown 中的图片并内嵌到邮件中
   - 无需手动指定 `--inline-images` 或 `--html` 参数
   - 如果不想自动转换,使用 `--html` 参数强制指定为纯 HTML 模式

5. **内嵌图片:**
   - 支持 Markdown 语法:`![alt](path/to/image.png)`
   - 支持 HTML 语法:`<img src="path/to/image.png">`
   - 图片会自动转换为 CID 引用,直接显示在邮件正文中
   - 建议压缩图片大小(每张 < 500KB),避免邮件过大被拒收
   - CID 使用文件名(去掉扩展名),例如 `myphoto.png` → `cid:myphoto`

6. **图片路径:**
   - 使用绝对路径(推荐)
   - 确保图片文件存在且可读
   - 跳过 `http://` 和 `https://` 开头的图片链接(外链)

7. **附件路径:** 使用绝对路径或相对于执行目录的路径

8. **测试:** 首次使用时,建议先发送测试邮件给自己

9. **keyring 备用方案:** 如果 keyring 不可用,密钥会保存在 `~/.send_email_password` 和 `~/.send_email_username`(base64 编码),文件权限为 600。注意这不是加密,仅避免明文存储。

## 安全流程

```
1. 首次配置(中国移动邮箱):
   - python send_email.py smtp --host smtp.gd.chinamobile.com --port 465 --no-tls
   - python send_email.py sender --name "Your Name"
   - python send_email.py username --save --email [email protected]
   - python send_email.py password --save  ← 输入密码

2. 后续发送:
   - python send_email.py send --to [email protected] --subject "..." --body "..."
     邮箱和密码自动从 keyring 读取
```

---

## 技术说明

### Markdown 自动检测

脚本会检测以下 Markdown 语法:
- 标题:`# 标题`
- 粗体:`**粗体**`
- 代码块:` ``` `
- 列表:`- 列表项` 或 `1. 列表项`
- 链接:`[文本](url)`
- **图片:`![alt](path)` ⭐**

检测到以上任一语法,会自动转换为 HTML 格式。

### 图片自动提取

支持两种语法:

**Markdown 语法:**
```
![图片说明](/path/to/image.png)
```

**HTML 语法:**
```html
<img src="/path/to/image.png">
```

脚本会:
1. 自动提取所有图片路径
2. 跳过 `http://` 和 `https://` 开头的链接
3. 将本地图片路径转换为 CID 引用
4. 内嵌到邮件中

### CID 工作原理

1. 图片作为 MIME part 添加到邮件
2. 每张图片分配 Content-ID(例如 `cid:myimage`)
3. HTML 中使用 `<img src="cid:myimage">` 引用
4. 邮件客户端解析并直接显示图片

### 支持的图片格式

- PNG
- JPG / JPEG
- GIF
- WebP

### 依赖项

- **必须:** Python 3.7+
- **推荐:** `keyring`(密钥管理)
- **推荐:** `markdown`(Markdown 自动转换)

安装依赖:

```bash
pip install keyring markdown
```

### 邮件大小限制

- 大多数邮件服务器限制:10-25 MB
- 建议:每张图片 < 500KB
- 建议:总邮件大小 < 5 MB

如果邮件过大,可能被拒收或放入垃圾邮件。


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### templates/default.html

```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>X 帖子摘要</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
            background: #f9f9f9;
            color: #0f0f0f;
            line-height: 1.5;
            padding: 20px;
        }

        .container {
            max-width: 680px;
            margin: 0 auto;
            background: #ffffff;
            border-radius: 12px;
            box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
            overflow: hidden;
        }

        .header {
            background: #ffffff;
            text-align: center;
            padding: 24px 20px;
            border-bottom: 1px solid #e5e5e5;
        }

        .header h1 {
            font-size: 22px;
            font-weight: 500;
            color: #0f0f0f;
            margin-bottom: 6px;
        }

        .header .subtitle {
            font-size: 14px;
            color: #606060;
            font-weight: 400;
        }

        .content-wrapper {
            padding: 16px;
            background: #f9f9f9;
        }

        /* 帖子标题(如 @ChinaNow24 的帖子 (6 条))*/
        .content-wrapper > h2 {
            font-size: 18px;
            font-weight: 500;
            color: #0f0f0f;
            margin: 20px 0 12px 0;
            padding-bottom: 8px;
            border-bottom: 2px solid #065fd4;
        }

        /* 帖子序号(如 帖子 1)*/
        .content-wrapper > h3 {
            font-size: 16px;
            font-weight: 500;
            color: #606060;
            margin: 20px 0 12px 0;
        }

        /* 帖子大标题(如 @ChinaNow24 的推文)*/
        .content-wrapper > h1 {
            font-size: 20px;
            font-weight: 500;
            color: #0f0f0f;
            margin-bottom: 12px;
        }

        /* 引用块(作者信息)*/
        .content-wrapper > blockquote {
            background: #f0f0f0;
            border-left: 3px solid #065fd4;
            padding: 12px 16px;
            margin: 12px 0;
            border-radius: 8px;
        }

        .content-wrapper > blockquote p {
            font-size: 14px;
            color: #606060;
            margin: 0;
        }

        /* 分隔线 */
        .content-wrapper > hr {
            border: none;
            border-top: 1px solid #e5e5e5;
            margin: 12px 0;
        }

        /* 正文内容 - 浅色底色 + 24px 粗体 */
        .content-wrapper > p {
            font-size: 24px;
            line-height: 1.5;
            color: #0f0f0f;
            margin-bottom: 12px;
            font-weight: 700;
            background: linear-gradient(135deg, #f8f9ff 0%, #fff5ff 100%);
            padding: 16px;
            border-radius: 12px;
            border: 2px solid #e8e4ff;
        }

        /* 综述区块 */
        .content-wrapper > .summary {
            background: linear-gradient(135deg, #e8f4ff 0%, #f0fff4 100%);
            padding: 20px;
            border-radius: 16px;
            border: 2px solid #b8e6ff;
            margin-bottom: 24px;
        }

        .content-wrapper > .summary h2 {
            font-size: 18px;
            font-weight: 600;
            color: #0f1419;
            margin: 0 0 12px 0;
            border-bottom: 2px solid #1d9bf0;
            padding-bottom: 8px;
        }

        .content-wrapper > .summary > ul {
            margin: 0;
            padding-left: 20px;
        }

        .content-wrapper > .summary > ul li {
            font-size: 15px;
            color: #0f1419;
            margin-bottom: 10px;
            line-height: 1.6;
        }

        .content-wrapper > .summary > ul li:last-child {
            margin-bottom: 0;
        }

        .content-wrapper > .summary strong {
            color: #1d9bf0;
        }

        .content-wrapper > p:last-child {
            margin-bottom: 0;
        }

        /* 媒体标题 */
        .content-wrapper > p + h2 {
            font-size: 14px;
            font-weight: 500;
            color: #606060;
            margin: 16px 0 8px 0;
        }

        /* 互动数据标题 */
        .content-wrapper > p + h2 + hr + h2 {
            font-size: 14px;
            font-weight: 500;
            color: #606060;
            margin: 16px 0 8px 0;
        }

        /* 图片 */
        .content-wrapper > p > img {
            max-width: 100%;
            height: auto;
            display: block;
            border-radius: 12px;
            border: 1px solid #e5e5e5;
        }

        /* 列表 */
        .content-wrapper > ul {
            background: #ffffff;
            padding: 12px 16px 12px 32px;
            border-radius: 12px;
            margin: 8px 0;
            border: 1px solid #e5e5e5;
        }

        .content-wrapper > ul li {
            font-size: 14px;
            color: #0f0f0f;
            margin-bottom: 6px;
            line-height: 1.5;
        }

        .content-wrapper > ul li:last-child {
            margin-bottom: 0;
        }

        /* footer */
        .footer {
            text-align: center;
            padding: 20px;
            background: #f9f9f9;
            border-top: 1px solid #e5e5e5;
        }

        .footer p {
            font-size: 13px;
            color: #606060;
            margin-bottom: 6px;
            font-weight: 400;
        }

        .footer a {
            color: #065fd4;
            text-decoration: none;
            font-weight: 500;
        }
    </style>
</head>
<body>
    <div class="container">
        <header class="header">
            <h1>{{title}}</h1>
            <p class="subtitle">{{subtitle}}</p>
        </header>

        <div class="content-wrapper">
            {{content}}
        </div>

        <footer class="footer">
            <p>此邮件由 OpenClaw 自动发送</p>
            <p>如有疑问,请联系发件人</p>
        </footer>
    </div>
</body>
</html>

```



---

## Skill Companion Files

> Additional files collected from the skill directory layout.

### _meta.json

```json
{
  "owner": "flyingtimes",
  "slug": "send-email-tool",
  "displayName": "Send Email Tool",
  "latest": {
    "version": "2.5.0",
    "publishedAt": 1772465690174,
    "commit": "https://github.com/openclaw/skills/commit/fcf29e9b99038fcfa0467cb711688cd0630fba81"
  },
  "history": [
    {
      "version": "1.4.0",
      "publishedAt": 1772371817500,
      "commit": "https://github.com/openclaw/skills/commit/3c618020e9e4ed409261c403acda2c17f787c5aa"
    },
    {
      "version": "1.3.0",
      "publishedAt": 1772370949187,
      "commit": "https://github.com/openclaw/skills/commit/b397b768ebaddab5262c790f0a5d018a167531fe"
    },
    {
      "version": "1.0.0",
      "publishedAt": 1772366316363,
      "commit": "https://github.com/openclaw/skills/commit/8e9141529fa277d3d2c158a95a9e6f8014bdb66c"
    }
  ]
}

```

### scripts/send_email.py

```python
#!/usr/bin/env python3
"""
Send Email - 邮件发送工具
支持 SMTP 邮件发送,包括 HTML、附件、自动内嵌图片等功能
支持 keyring 密钥管理
"""

import smtplib
import sys
import json
import os
import re
from pathlib import Path
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.application import MIMEApplication
from email.mime.image import MIMEImage
from email.utils import formataddr, formatdate
from typing import Optional, List, Dict, Any, Tuple
import argparse
from datetime import datetime

# 尝试导入 keyring,失败则使用备用方案
try:
    import keyring
    KEYRING_AVAILABLE = True
except ImportError:
    KEYRING_AVAILABLE = False
    print("⚠️  keyring 未安装,将使用备用存储方案")
    print("   安装 keyring: pip install keyring")

# 尝试导入 markdown 库
try:
    import markdown
    MARKDOWN_AVAILABLE = True
except ImportError:
    MARKDOWN_AVAILABLE = False


# ============================================================================
# 密钥管理
# ============================================================================

class KeyringManager:
    """密钥管理器 - 使用 keyring 或备用方案"""

    def __init__(self):
        self.service_name = "send_email_tool"
        self.password_backup_file = Path.home() / ".send_email_password"
        self.username_backup_file = Path.home() / ".send_email_username"

    def get_password(self, username: str) -> Optional[str]:
        """获取密码"""
        if KEYRING_AVAILABLE:
            try:
                password = keyring.get_password(self.service_name, username)
                if password:
                    return password
            except Exception as e:
                print(f"⚠️  keyring 读取失败: {str(e)}")

        # 备用方案:从文件读取
        return self._read_from_backup(self.password_backup_file)

    def get_username(self) -> Optional[str]:
        """获取发件人邮箱(用户名)"""
        if KEYRING_AVAILABLE:
            try:
                username = keyring.get_password(self.service_name, "username")
                if username:
                    return username
            except Exception as e:
                print(f"⚠️  keyring 读取失败: {str(e)}")

        # 备用方案:从文件读取
        return self._read_from_backup(self.username_backup_file)

    def set_password(self, username: str, password: str) -> None:
        """保存密码"""
        if KEYRING_AVAILABLE:
            try:
                keyring.set_password(self.service_name, username, password)
                print(f"✓ 密码已保存到 keyring (用户名: {username})")
                return
            except Exception as e:
                print(f"⚠️  keyring 保存失败: {str(e)},使用备用方案")

        # 备用方案:保存到文件
        self._save_to_backup(self.password_backup_file, password)
        print(f"✓ 密码已保存到备用文件 (用户名: {username})")

    def set_username(self, username: str) -> None:
        """保存发件人邮箱(用户名)"""
        if KEYRING_AVAILABLE:
            try:
                keyring.set_password(self.service_name, "username", username)
                print(f"✓ 发件人邮箱已保存到 keyring: {username}")
                return
            except Exception as e:
                print(f"⚠️  keyring 保存失败: {str(e)},使用备用方案")

        # 备用方案:保存到文件
        self._save_to_backup(self.username_backup_file, username)
        print(f"✓ 发件人邮箱已保存到备用文件: {username}")

    def delete_password(self, username: str) -> None:
        """删除密码"""
        if KEYRING_AVAILABLE:
            try:
                keyring.delete_password(self.service_name, username)
                print(f"✓ 密码已从 keyring 删除 (用户名: {username})")
                return
            except Exception as e:
                print(f"⚠️  keyring 删除失败: {str(e)}")

        # 备用方案:删除文件
        if self.password_backup_file.exists():
            self.password_backup_file.unlink()
            print("✓ 备用密码文件已删除")

    def delete_username(self) -> None:
        """删除发件人邮箱"""
        if KEYRING_AVAILABLE:
            try:
                keyring.delete_password(self.service_name, "username")
                print("✓ 发件人邮箱已从 keyring 删除")
                return
            except Exception as e:
                print(f"⚠️  keyring 删除失败: {str(e)}")

        # 备用方案:删除文件
        if self.username_backup_file.exists():
            self.username_backup_file.unlink()
            print("✓ 备用用户名文件已删除")

    def _save_to_backup(self, backup_file: Path, content: str) -> None:
        """保存到备用文件(加密或编码)"""
        # 使用 base64 简单编码(注意:这不是加密,仅避免明文存储)
        import base64
        encoded = base64.b64encode(content.encode('utf-8')).decode('utf-8')

        # 设置文件权限为仅用户可读写
        backup_file.write_text(encoded)
        backup_file.chmod(0o600)

    def _read_from_backup(self, backup_file: Path) -> Optional[str]:
        """从备用文件读取"""
        if not backup_file.exists():
            return None

        try:
            import base64
            encoded = backup_file.read_text()
            return base64.b64decode(encoded).decode('utf-8')
        except Exception:
            return None


# ============================================================================
# 配置管理
# ============================================================================

class EmailConfig:
    """邮件配置"""

    def __init__(self, config_path: Optional[Path] = None):
        self.config_path = config_path or Path.home() / ".send_email_config.json"
        self.config = self._load_config()

    def _load_config(self) -> Dict[str, Any]:
        """加载配置文件"""
        if not self.config_path.exists():
            # 创建默认配置模板
            default_config = {
                "smtp": {
                    "host": "smtp.gd.chinamobile.com",
                    "port": 465,
                    "use_tls": False
                },
                "sender": {
                    "name": "中国移动用户"
                }
            }
            self._save_config(default_config)
            return default_config

        with open(self.config_path, 'r', encoding='utf-8') as f:
            return json.load(f)

    def _save_config(self, config: Dict[str, Any]) -> None:
        """保存配置文件"""
        with open(self.config_path, 'w', encoding='utf-8') as f:
            json.dump(config, f, indent=2, ensure_ascii=False)

    def get_smtp_config(self) -> Dict[str, Any]:
        """获取 SMTP 配置"""
        return self.config.get('smtp', {})

    def get_sender_config(self, username: Optional[str] = None) -> Dict[str, Any]:
        """获取发件人配置"""
        sender_config = self.config.get('sender', {})

        # 如果提供了 username,使用它;否则尝试从 keyring 读取
        if username:
            sender_config['email'] = username
        elif 'email' not in sender_config:
            # 从 keyring 读取邮箱
            keyring = KeyringManager()
            stored_username = keyring.get_username()
            if stored_username:
                sender_config['email'] = stored_username
            else:
                # 使用默认邮箱
                sender_config['email'] = "[email protected]"

        return sender_config

    def update_smtp(self, host: str, port: int, use_tls: bool = True) -> None:
        """更新 SMTP 配置"""
        if 'smtp' not in self.config:
            self.config['smtp'] = {}
        self.config['smtp']['host'] = host
        self.config['smtp']['port'] = port
        self.config['smtp']['use_tls'] = use_tls
        self._save_config(self.config)
        print(f"✓ SMTP 配置已更新: {host}:{port}")

    def update_sender(self, name: str) -> None:
        """更新发件人配置"""
        if 'sender' not in self.config:
            self.config['sender'] = {}
        self.config['sender']['name'] = name
        self._save_config(self.config)
        print(f"✓ 发件人名称已更新: {name}")

        # 提示使用 keyring 保存邮箱
        print(f"\n⚠️  邮箱地址需通过 keyring 保存:")
        print(f"   python send_email.py username --save --email [email protected]")


# ============================================================================
# Markdown 处理
# ============================================================================

class MarkdownProcessor:
    """Markdown 处理器 - 自动检测并处理图片"""

    def __init__(self):
        self.has_markdown = MARKDOWN_AVAILABLE

    def detect_markdown(self, text: str) -> bool:
        """检测文本是否为 Markdown 格式"""
        # 简单检测:检查常见的 Markdown 语法
        markdown_patterns = [
            r'!\[.*?\]\([^)]+\)',  # 图片语法
            r'^#{1,6}\s+',         # 标题
            r'\*\*.*?\*\*',         # 粗体
            r'```',                 # 代码块
            r'^\s*[-*+]\s+',        # 无序列表
            r'^\s*\d+\.\s+',        # 有序列表
            r'\[.*?\]\([^)]+\)',    # 链接
        ]

        for pattern in markdown_patterns:
            if re.search(pattern, text, re.MULTILINE):
                return True

        return False

    def extract_images(self, text: str) -> List[str]:
        """从 Markdown 或 HTML 中提取图片路径"""
        images = []

        # Markdown 格式:![alt](path)
        markdown_images = re.findall(r'!\[.*?\]\(([^)]+)\)', text)
        images.extend(markdown_images)

        # HTML 格式:<img src="path">
        html_images = re.findall(r'<img[^>]+src=["\']([^"\']+)["\']', text, re.IGNORECASE)
        images.extend(html_images)

        # 去重并过滤掉非本地路径(http/https)
        local_images = []
        seen = set()
        for img in images:
            # 去除 URL 参数
            img = re.sub(r'\?.*$', '', img)
            # 去除锚点
            img = re.sub(r'#.*$', '', img)

            # 跳过非本地路径
            if img.startswith(('http://', 'https://', 'data:')):
                continue

            # 去重
            if img not in seen:
                seen.add(img)
                local_images.append(img)

        return local_images

    def convert_to_html(self, text: str, images: List[str]) -> str:
        """将 Markdown 转换为 HTML,并替换图片引用为 CID"""
        if not self.has_markdown:
            print("⚠️  markdown 库未安装,使用简单转换(仅处理图片)")

            # 简单处理:转换 Markdown 图片语法为 HTML img 标签
            html = text
            for img_path in images:
                path = Path(img_path)
                cid = path.stem  # 使用文件名(不含扩展名)作为 CID

                # 替换 Markdown 图片语法为 HTML img 标签
                # 格式:![alt](path) -> <img src="cid:path_stem" alt="alt">
                import re
                html = re.sub(
                    r'!\[([^\]]*)\]\(' + re.escape(img_path) + r'\)',
                    f'<img src="cid:{cid}" alt="\\1">',
                    html
                )

            return html

        # 转换 Markdown 为 HTML
        html = markdown.markdown(
            text,
            extensions=['fenced_code', 'tables', 'sane_lists']
        )

        # 替换图片路径为 CID 引用
        for img_path in images:
            path = Path(img_path)
            cid = path.stem  # 使用文件名(不含扩展名)作为 CID

            # 替换 HTML 中的图片路径
            html = html.replace(
                f'src="{img_path}"',
                f'src="cid:{cid}"'
            ).replace(
                f"src='{img_path}'",
                f"src='cid:{cid}'"
            )

        return html


# ============================================================================
# 模板处理器
# ============================================================================

class TemplateProcessor:
    """模板处理器 - 支持简单的变量替换"""

    def __init__(self, template_dir: Optional[Path] = None):
        if template_dir is None:
            # 默认模板目录在脚本所在目录的 templates 文件夹
            script_dir = Path(__file__).parent.parent
            self.template_dir = script_dir / "templates"
        else:
            self.template_dir = Path(template_dir)

    def load_template(self, template_name: str) -> Optional[str]:
        """加载模板文件"""
        if not template_name.endswith('.html'):
            template_name += '.html'

        template_path = self.template_dir / template_name

        if not template_path.exists():
            print(f"⚠️  模板文件不存在: {template_path}")
            return None

        with open(template_path, 'r', encoding='utf-8') as f:
            return f.read()

    def render(
        self,
        template_name: str,
        content: str,
        title: str = "邮件摘要",
        subtitle: Optional[str] = None,
        **kwargs
    ) -> str:
        """渲染模板"""
        template = self.load_template(template_name)
        if template is None:
            # 如果模板不存在,直接返回内容
            return content

        # 获取当前时间
        if subtitle is None:
            subtitle = datetime.now().strftime('%Y年%m月%d日 %H:%M')

        # 替换模板变量
        rendered = template.replace('{{title}}', title)
        rendered = rendered.replace('{{subtitle}}', subtitle)
        rendered = rendered.replace('{{content}}', content)

        # 替换其他变量
        for key, value in kwargs.items():
            placeholder = '{{' + key + '}}'
            rendered = rendered.replace(placeholder, str(value))

        return rendered


# ============================================================================
# 邮件发送器
# ============================================================================

class EmailSender:
    """邮件发送器"""

    def __init__(self, config: EmailConfig, username: Optional[str] = None):
        self.config = config
        self.keyring = KeyringManager()
        self.markdown_processor = MarkdownProcessor()
        self.template_processor = TemplateProcessor()
        self.smtp_config = config.get_smtp_config()
        self.sender_config = config.get_sender_config(username)

        # 从 keyring 读取发件人邮箱
        if not username:
            stored_username = self.keyring.get_username()
            if stored_username:
                self.sender_config['email'] = stored_username

        # 从 keyring 读取密码
        email = self.sender_config.get('email', '')
        self.password = self.keyring.get_password(email)

    def create_message(
        self,
        to_email: str,
        subject: str,
        body: str,
        to_name: str = "",
        is_html: bool = False,
        attachments: Optional[List[str]] = None,
        cc_emails: Optional[List[str]] = None,
        bcc_emails: Optional[List[str]] = None,
        inline_images: Optional[List[str]] = None
    ) -> MIMEMultipart:
        """创建邮件消息"""
        msg = MIMEMultipart('related')
        msg['Subject'] = subject
        msg['From'] = formataddr((
            self.sender_config.get('name', ''),
            self.sender_config.get('email', '')
        ))
        msg['To'] = formataddr((to_name, to_email)) if to_name else to_email

        # 添加 CC 和 BCC
        if cc_emails:
            msg['Cc'] = ', '.join(cc_emails)
        # BCC 不添加到头部,只在发送时使用

        msg['Date'] = formatdate(localtime=True)

        # 如果有内嵌图片,先处理并替换 HTML 中的占位符
        processed_body = body
        cid_map = {}  # 存储文件路径到 CID 的映射

        if inline_images and is_html:
            for image_path in inline_images:
                cid = self._add_inline_image(msg, image_path)
                if cid:
                    cid_map[image_path] = cid
                    # 替换 HTML 中的图片路径为 CID 引用
                    # 支持:src="path/to/image.png" 或 src='path/to/image.png'
                    processed_body = processed_body.replace(
                        f'src="{image_path}"',
                        f'src="cid:{cid}"'
                    ).replace(
                        f"src='{image_path}'",
                        f"src='cid:{cid}'"
                    )

        # 创建正文部分
        if is_html:
            msg_text = MIMEText(processed_body, 'html', 'utf-8')
        else:
            msg_text = MIMEText(processed_body, 'plain', 'utf-8')
        msg.attach(msg_text)

        # 添加附件
        if attachments:
            for filepath in attachments:
                self._add_attachment(msg, filepath)

        return msg

    def _add_attachment(self, msg: MIMEMultipart, filepath: str) -> None:
        """添加附件"""
        path = Path(filepath)
        if not path.exists():
            print(f"⚠️  附件不存在,跳过: {filepath}")
            return

        try:
            with open(path, 'rb') as f:
                part = MIMEApplication(f.read())

            # 判断文件类型
            maintype, subtype = self._get_mime_type(path)

            part.set_type(f'{maintype}/{subtype}')
            part.add_header('Content-Disposition', 'attachment', filename=path.name)
            msg.attach(part)
            print(f"✓ 已添加附件: {path.name}")
        except Exception as e:
            print(f"✗ 添加附件失败 {path.name}: {str(e)}")

    def _add_inline_image(self, msg: MIMEMultipart, image_path: str) -> Optional[str]:
        """添加内嵌图片(使用 CID 方式)"""
        path = Path(image_path)
        if not path.exists():
            print(f"⚠️  图片不存在,跳过: {image_path}")
            return None

        try:
            with open(path, 'rb') as f:
                img_data = f.read()

            # 判断图片类型
            ext = path.suffix.lower()
            if ext == '.jpg' or ext == '.jpeg':
                maintype, subtype = 'image', 'jpeg'
            elif ext == '.png':
                maintype, subtype = 'image', 'png'
            elif ext == '.gif':
                maintype, subtype = 'image', 'gif'
            elif ext == '.webp':
                maintype, subtype = 'image', 'webp'
            else:
                print(f"⚠️  不支持的图片格式: {ext}")
                return None

            # 创建 MIMEImage
            img = MIMEImage(img_data, _subtype=subtype)

            # 生成 CID(使用文件名,去掉扩展名)
            cid = path.stem

            # 设置 Content-ID
            img.add_header('Content-ID', f'<{cid}>')
            img.add_header('Content-Disposition', 'inline', filename=path.name)

            # 添加到邮件
            msg.attach(img)
            print(f"✓ 已添加内嵌图片: {path.name} (cid:{cid})")

            return cid

        except Exception as e:
            print(f"✗ 添加内嵌图片失败 {path.name}: {str(e)}")
            return None

    def _get_mime_type(self, path: Path) -> tuple:
        """获取文件的 MIME 类型"""
        ext = path.suffix.lower()

        # 常见文件类型映射
        mime_types = {
            '.pdf': ('application', 'pdf'),
            '.doc': ('application', 'msword'),
            '.docx': ('application', 'vnd.openxmlformats-officedocument.wordprocessingml.document'),
            '.xls': ('application', 'vnd.ms-excel'),
            '.xlsx': ('application', 'vnd.openxmlformats-officedocument.spreadsheetml.sheet'),
            '.ppt': ('application', 'vnd.ms-powerpoint'),
            '.pptx': ('application', 'vnd.openxmlformats-officedocument.presentationml.presentation'),
            '.jpg': ('image', 'jpeg'),
            '.jpeg': ('image', 'jpeg'),
            '.png': ('image', 'png'),
            '.gif': ('image', 'gif'),
            '.zip': ('application', 'zip'),
            '.txt': ('text', 'plain'),
            '.csv': ('text', 'csv'),
            '.json': ('application', 'json'),
            '.xml': ('application', 'xml'),
        }

        return mime_types.get(ext, ('application', 'octet-stream'))

    def send(
        self,
        to_email: str,
        subject: str,
        body: str,
        to_name: str = "",
        is_html: bool = False,
        attachments: Optional[List[str]] = None,
        cc_emails: Optional[List[str]] = None,
        bcc_emails: Optional[List[str]] = None,
        inline_images: Optional[List[str]] = None,
        template: Optional[str] = None,
        title: str = "邮件摘要"
    ) -> bool:
        """发送邮件"""
        try:
            # 自动检测 Markdown 格式
            final_is_html = is_html
            final_body = body
            final_inline_images = inline_images or []

            # 如果用户没有明确指定 HTML,检测是否是 Markdown
            if not is_html and self.markdown_processor.detect_markdown(body):
                print("📝 检测到 Markdown 格式,自动转换为 HTML...")

                # 提取 Markdown 中的图片
                detected_images = self.markdown_processor.extract_images(body)

                if detected_images:
                    print(f"🖼️  检测到 {len(detected_images)} 张图片:")
                    for img in detected_images:
                        print(f"   • {img}")

                    # 转换 Markdown 为 HTML 并替换图片引用
                    final_body = self.markdown_processor.convert_to_html(body, detected_images)
                    final_is_html = True

                    # 添加到内嵌图片列表
                    final_inline_images.extend(detected_images)
                else:
                    # 没有图片,只转换 Markdown
                    final_body = self.markdown_processor.convert_to_html(body, [])
                    if final_body == body:  # 转换失败(markdown 库未安装)
                        # 简单处理:保持 Markdown 格式,不作为 HTML
                        final_is_html = False
                        print("⚠️  Markdown 库未安装,使用纯文本格式(图片仍可内嵌)")
                    else:
                        final_is_html = True
                        print("✓ Markdown 转换为 HTML(无图片)")

            # 应用模板
            if template:
                print(f"🎨 使用模板: {template}")
                final_body = self.template_processor.render(
                    template_name=template,
                    content=final_body,
                    title=title,
                    subtitle=datetime.now().strftime('%Y年%m月%d日 %H:%M')
                )
                final_is_html = True  # 模板输出一定是 HTML

            # 创建消息
            msg = self.create_message(
                to_email=to_email,
                to_name=to_name,
                subject=subject,
                body=final_body,
                is_html=final_is_html,
                attachments=attachments,
                cc_emails=cc_emails,
                bcc_emails=bcc_emails,
                inline_images=final_inline_images
            )

            # 准备收件人列表
            recipients = [to_email]
            if cc_emails:
                recipients.extend(cc_emails)
            if bcc_emails:
                recipients.extend(bcc_emails)

            # 连接 SMTP 服务器
            host = self.smtp_config.get('host', 'smtp.gd.chinamobile.com')
            port = self.smtp_config.get('port', 587)
            use_tls = self.smtp_config.get('use_tls', True)

            print(f"正在连接 SMTP 服务器: {host}:{port}")

            if use_tls:
                server = smtplib.SMTP(host, port)
                server.starttls()
            else:
                server = smtplib.SMTP_SSL(host, port)

            # 登录
            email = self.sender_config.get('email', '')
            print(f"正在登录 (发件人: {email})...")
            server.login(email, self.password)

            # 发送
            print(f"正在发送邮件到: {to_email}")
            server.send_message(msg, to_addrs=recipients)
            server.quit()

            print("✓ 邮件发送成功!")
            return True

        except Exception as e:
            print(f"✗ 邮件发送失败: {str(e)}")
            return False


# ============================================================================
# 命令行界面
# ============================================================================

def main():
    parser = argparse.ArgumentParser(description='Send Email - 邮件发送工具')
    subparsers = parser.add_subparsers(dest='command', help='可用命令')

    # 配置命令
    config_parser = subparsers.add_parser('config', help='配置邮件设置')

    # SMTP 配置
    smtp_parser = subparsers.add_parser('smtp', help='配置 SMTP 服务器')
    smtp_parser.add_argument('--host', required=True, help='SMTP 服务器地址')
    smtp_parser.add_argument('--port', type=int, required=True, help='SMTP 服务器端口')
    smtp_parser.add_argument('--no-tls', action='store_true', help='不使用 TLS')

    # 发件人配置
    sender_parser = subparsers.add_parser('sender', help='配置发件人名称')
    sender_parser.add_argument('--name', required=True, help='发件人名称')

    # 发件人邮箱管理命令
    username_parser = subparsers.add_parser('username', help='发件人邮箱管理')
    username_parser.add_argument('--save', action='store_true', help='保存发件人邮箱到 keyring')
    username_parser.add_argument('--delete', action='store_true', help='删除保存的发件人邮箱')
    username_parser.add_argument('--email', help='发件人邮箱地址')

    # 密码管理命令
    password_parser = subparsers.add_parser('password', help='密码管理')
    password_parser.add_argument('--save', action='store_true', help='保存密码到 keyring')
    password_parser.add_argument('--delete', action='store_true', help='删除保存的密码')
    password_parser.add_argument('--password', help='密码内容')

    # 发送命令
    send_parser = subparsers.add_parser('send', help='发送邮件')
    send_parser.add_argument('--to', help='收件人邮箱(可选:--config 中会使用)')
    send_parser.add_argument('--to-name', default='', help='收件人名称')
    send_parser.add_argument('--subject', required=True, help='邮件主题')
    send_parser.add_argument('--body', required=True, help='邮件正文')
    send_parser.add_argument('--html', action='store_true', help='使用 HTML 格式')
    send_parser.add_argument('--attachments', nargs='*', help='附件文件路径(多个)')
    send_parser.add_argument('--inline-images', nargs='*', help='内嵌图片文件路径(多个,仅 HTML 模式)')
    send_parser.add_argument('--cc', nargs='*', help='抄送邮箱(多个,可选:--config 中会使用)')
    send_parser.add_argument('--bcc', nargs='*', help='密送邮箱(多个)')
    send_parser.add_argument('--config', help='配置文件路径(包含 to 和 cc,覆盖命令行参数)')
    send_parser.add_argument('--template', help='使用指定模板渲染邮件(模板文件名,不含 .html)')
    send_parser.add_argument('--title', default='邮件摘要', help='邮件标题(模板中使用)')
    # 移除 --password 和 --save-pwd 参数,强制使用 keyring

    args = parser.parse_args()

    if not args.command:
        parser.print_help()
        sys.exit(1)

    # 初始化配置
    config = EmailConfig()

    if args.command == 'config':
        print("配置文件位置:", config.config_path)
        print("\n当前配置:")
        print(json.dumps(config.config, indent=2, ensure_ascii=False))

        # 显示 keyring 中的邮箱
        keyring = KeyringManager()
        stored_username = keyring.get_username()
        if stored_username:
            print(f"\nkeyring 中的发件人邮箱: {stored_username}")
        else:
            print(f"\nkeyring 中的发件人邮箱: 未设置")

        print("\n使用以下命令配置:")
        print("  python send_email.py smtp --host smtp.gd.chinamobile.com --port 587")
        print("  python send_email.py sender --name 'Your Name'")
        print("  python send_email.py username --save --email [email protected]")
        print("  python send_email.py password --save")

    elif args.command == 'smtp':
        config.update_smtp(args.host, args.port, not args.no_tls)

    elif args.command == 'sender':
        config.update_sender(args.name)

    elif args.command == 'username':
        keyring = KeyringManager()

        if args.save:
            email = args.email
            if not email:
                print("请输入发件人邮箱:")
                email = input().strip()
            if email:
                keyring.set_username(email)
            else:
                print("✗ 邮箱不能为空")

        elif args.delete:
            keyring.delete_username()

        else:
            # 显示当前邮箱状态
            stored_username = keyring.get_username()
            if stored_username:
                print(f"✓ 已保存发件人邮箱到 keyring: {stored_username}")
            else:
                print(f"✗ 未保存发件人邮箱到 keyring")
                print(f"默认邮箱: [email protected]")

    elif args.command == 'password':
        keyring = KeyringManager()
        email = keyring.get_username() or "[email protected]"

        if args.save:
            password = args.password
            if not password:
                print(f"请输入密码 (邮箱: {email}):")
                password = input().strip()
            if password:
                keyring.set_password(email, password)
            else:
                print("✗ 密码不能为空")

        elif args.delete:
            keyring.delete_password(email)

        else:
            # 显示当前密码状态
            stored_pwd = keyring.get_password(email)
            if stored_pwd:
                print(f"✓ 已保存密码到 keyring (邮箱: {email})")
            else:
                print(f"✗ 未保存密码 (邮箱: {email})")

    elif args.command == 'send':
        # 创建发送器(自动从 keyring 获取邮箱和密码)
        sender = EmailSender(config)

        # 检查发件人邮箱和密码
        email = sender.sender_config.get('email', '')
        if not email:
            print("\n✗ 未找到发件人邮箱,请先运行以下命令:")
            print("   python send_email.py username --save")
            sys.exit(1)

        if not sender.password:
            print(f"\n✗ 未找到密码,请先运行以下命令:")
            print(f"   python send_email.py password --save")
            print(f"\n发件人邮箱: {email}")
            sys.exit(1)

        # 检查 inline-images 是否需要 HTML 模式
        if args.inline_images and not args.html:
            print("\n⚠️  内嵌图片功能需要 HTML 模式,已自动启用")
            args.html = True

        # 发送邮件
        sender.send(
            to_email=args.to,
            to_name=args.to_name,
            subject=args.subject,
            body=args.body,
            is_html=args.html,
            attachments=args.attachments,
            cc_emails=args.cc,
            bcc_emails=args.bcc,
            inline_images=args.inline_images,
            template=args.template,
            title=args.title
        )


if __name__ == '__main__':
    main()

```

send-email | SkillHub