Skill: shiming-git/simple-email (version 1.0.1)

--- name: simple-email description: 简单易用的 IMAP/SMTP 邮件技能。支持收发邮件、查看未读邮件、搜索、标记已读/未读、下载附件。兼容 Gmail、QQ 邮箱、163、新浪等所有标准 IMAP/SMTP 服务。 homepage: https://github.com/openclaw/skills metadata: openclaw: emoji: "📧" requires: env: - IMAP_HOST - IMAP_USER - IMAP_PASS - SMTP_HOST - SMTP_USER - SMTP_PASS bins: - node - npm primaryEnv: SMTP_PASS --- # Simple Email - 简单易用的邮件技能 通过 IMAP 协议接收和管理邮件，通过 SMTP 协议发送邮件。支持所有标准 IMAP/SMTP 邮件服务，包括 Gmail、QQ 邮箱、163、新浪、Outlook 等。 ## 🚀 快速开始 ### 1. 安装依赖 ```bash cd skills/simple-email npm install ``` ### 2. 配置邮箱 复制配置模板并填写你的邮箱信息： ```bash cp .env.example .env ``` 编辑 `.env` 文件，填入你的邮箱配置： ```bash # IMAP 配置（接收邮件） IMAP_HOST=imap.sina.cn IMAP_PORT=993 IMAP_USER=[REDACTED_SECRET] IMAP_PASS=your_password_or_auth_code IMAP_TLS=true # SMTP 配置（发送邮件） SMTP_HOST=smtp.sina.cn SMTP_PORT=465 SMTP_SECURE=true SMTP_USER=[REDACTED_SECRET] SMTP_PASS=your_password_or_auth_code SMTP_FROM=小智助理 <[REDACTED_SECRET]> # 安全配置（文件读写权限） ALLOWED_READ_DIRS=C:\Users\Administrator\.openclaw\workspace ALLOWED_WRITE_DIRS=C:\Users\Administrator\.openclaw\workspace ``` ### 3. 测试连接 ```bash # 测试 SMTP 发送 node scripts/smtp.js test # 检查收件箱 node scripts/imap.js check --limit 5 ``` --- ## 📥 接收邮件（IMAP） ### 检查新邮件 ```bash # 检查最新 10 封邮件 node scripts/imap.js check # 检查最新 5 封邮件 node scripts/imap.js check --limit 5 # 只检查未读邮件 node scripts/imap.js check --unseen # 检查最近 2 小时的邮件 node scripts/imap.js check --recent 2h # 检查指定邮箱 node scripts/imap.js check --mailbox INBOX ``` ### 搜索邮件 ```bash # 搜索未读邮件 node scripts/imap.js search --unseen # 按发件人搜索 node scripts/imap.js search --from "[REDACTED_SECRET]" # 按主题搜索 node scripts/imap.js search --subject "报告" # 搜索最近 7 天的邮件 node scripts/imap.js search --recent 7d # 组合搜索 node scripts/imap.js search --unseen --from "[REDACTED_SECRET]" --limit 10 ``` ### 查看邮件详情 ```bash # 查看指定 UID 的邮件 node scripts/imap.js fetch <uid> ``` ### 标记已读/未读 ```bash # 标记为已读 node scripts/imap.js mark-read <uid1> <uid2> ... # 标记为未读 node scripts/imap.js mark-unread <uid1> <uid2> ... ``` ### 下载附件 ```bash # 下载邮件的所有附件 node scripts/imap.js download <uid> # 下载到指定目录 node scripts/imap.js download <uid> --dir ./attachments # 下载指定附件 node scripts/imap.js download <uid> --file report.pdf ``` ### 列出所有邮箱 ```bash node scripts/imap.js list-mailboxes ``` --- ## 📤 发送邮件（SMTP） ### 发送纯文本邮件 ```bash node scripts/smtp.js send --to [REDACTED_SECRET] --subject "你好" --body "这是一封测试邮件" ``` ### 发送 HTML 邮件 ```bash node scripts/smtp.js send --to [REDACTED_SECRET] --subject "HTML 邮件" --html --body "<h1>你好</h1><p>这是<strong>HTML</strong>格式的邮件</p>" ``` ### 发送带附件的邮件 ```bash node scripts/smtp.js send --to [REDACTED_SECRET] --subject "报告" --body "请查收附件" --attach ./report.pdf ``` ### 发送给多人 ```bash # 多个收件人 node scripts/smtp.js send --to "[REDACTED_SECRET],[REDACTED_SECRET]" --subject "通知" --body "大家好" # 抄送 node scripts/smtp.js send --to [REDACTED_SECRET] --cc "[REDACTED_SECRET],[REDACTED_SECRET]" --subject "抄送" --body "你好" # 密送 node scripts/smtp.js send --to [REDACTED_SECRET] --bcc "[REDACTED_SECRET]" --subject "密送" --body "你好" ``` ### 从文件读取内容 ```bash # 从文件读取邮件正文 node scripts/smtp.js send --to [REDACTED_SECRET] --subject "报告" --body-file ./message.txt # 从文件读取 HTML 内容 node scripts/smtp.js send --to [REDACTED_SECRET] --subject "新闻稿" --html --html-file ./newsletter.html # 从文件读取主题 node scripts/smtp.js send --to [REDACTED_SECRET] --subject-file ./subject.txt --body-file ./body.txt ``` --- ## 📋 常用邮箱服务器配置 | 服务商 | IMAP 主机 | IMAP 端口 | SMTP 主机 | SMTP 端口 | 说明 | |--------|-----------|-----------|-----------|-----------|------| | **新浪邮箱** | imap.sina.cn | 993 | smtp.sina.cn | 465 | 使用授权码 | | **QQ 邮箱** | imap.qq.com | 993 | smtp.qq.com | 587 | 使用授权码 | | **163 邮箱** | imap.163.com | 993 | smtp.163.com | 465 | 使用授权码 | | **126 邮箱** | imap.126.com | 993 | smtp.126.com | 465 | 使用授权码 | | **Gmail** | imap.gmail.com | 993 | smtp.gmail.com | 587 | 使用应用专用密码 | | **Outlook** | outlook.office365.com | 993 | smtp.office365.com | 587 | 使用应用密码 | --- ## ⚠️ 重要提示 ### Gmail 用户 - Gmail **不接受**常规账户密码 - 必须生成 **应用专用密码**：https://myaccount.google.com/apppasswords - 需要启用两步验证 ### 163/QQ/新浪邮箱用户 - 使用 **授权码**，不是账户密码 - 需要在网页版邮箱设置中先启用 IMAP/SMTP 服务 - 授权码在邮箱设置 → 账户安全中获取 --- ## 🔒 安全建议 1. **不要提交 `.env` 文件**到版本控制系统 2. 使用授权码/应用密码，而不是账户密码 3. 启用 SSL/TLS 加密连接 4. 定期更换密码 5. 限制 `ALLOWED_READ_DIRS` 和 `ALLOWED_WRITE_DIRS` 到必要的目录 --- ## 🐛 常见问题 ### 认证失败 - 检查用户名是否为完整邮箱地址 - 确认使用授权码/应用密码，而非账户密码 - 检查是否已启用 IMAP/SMTP 服务 ### 连接超时 - 检查服务器地址和端口是否正确 - 确认网络连接正常 - 检查防火墙设置 ### TLS/SSL 错误 - 确认 `IMAP_TLS` 和 `SMTP_SECURE` 设置正确 - 自签名证书可设置 `IMAP_REJECT_UNAUTHORIZED=false` --- ## 📝 命令速查 ### IMAP 命令 ```bash node scripts/imap.js check [--limit N] [--unseen] [--recent 2h] node scripts/imap.js search [--unseen] [--from xxx] [--subject xxx] node scripts/imap.js fetch <uid> node scripts/imap.js mark-read <uid> [uid2 ...] node scripts/imap.js mark-unread <uid> [uid2 ...] node scripts/imap.js download <uid> [--dir ./path] node scripts/imap.js list-mailboxes ``` ### SMTP 命令 ```bash node scripts/smtp.js send --to <email> --subject <text> --body <text> node scripts/smtp.js send --to <email> --subject <text> --html --body "<html>..." node scripts/smtp.js send --to <email> --subject <text> --attach <file> node scripts/smtp.js test ``` --- ## 📄 许可证 MIT License

path: email-template.html
content: <html>
<head><meta charset="UTF-8"></head>
<body style="margin:0;padding:0;font-family:Arial,'Microsoft YaHei',sans-serif;line-height:1.6;color:#0F172A;background-color:#F1F5F9;">
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:#F1F5F9;padding:20px;">
<tr><td align="center">
<table width="600" cellpadding="0" cellspacing="0" border="0" style="background-color:#ffffff;border-radius:8px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.08);">
<tr><td style="background:linear-gradient(135deg,#6366f1 0%,#8b5cf6 100%);padding:40px 30px;text-align:center;">
<h1 style="color:#ffffff;font-size:26px;font-weight:700;margin:0 0 8px 0;">UI/UX 设计邮件模板</h1>
<p style="color:#e0e7ff;font-size:14px;margin:0;">Professional Email Design</p>
</td></tr>
<tr><td style="padding:35px 30px;">
<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tr><td style="background:#f8fafc;border-radius:8px;padding:20px;margin-bottom:16px;border-left:4px solid #6366f1;">
<h3 style="color:#334155;font-size:15px;font-weight:600;margin:0 0 12px 0;">🎨 设计理念</h3>
<p style="color:#64748b;font-size:14px;margin:0;">基于 UI/UX Pro Max 设计原则，打造专业、清晰、美观的邮件体验。</p>
</td></tr>
<tr><td style="height:30px;"></td></tr>
<tr><td><h2 style="color:#1e293b;font-size:18px;font-weight:600;margin:0 0 16px 0;padding-bottom:8px;border-bottom:2px solid #e2e8f0;">📐 设计系统</h2></td></tr>
<tr><td><table width="100%" cellpadding="0" cellspacing="0" border="0">
<tr><td style="padding:10px 12px;background:#f1f5f9;font-weight:600;color:#475568;width:35%;border:1px solid #e2e8f0;">主色调</td><td style="padding:10px 12px;background:#ffffff;color:#334155;border:1px solid #e2e8f0;">Indigo (#6366f1) + Purple (#8b5cf6)</td></tr>
<tr><td style="padding:10px 12px;background:#f1f5f9;font-weight:600;color:#475568;border:1px solid #e2e8f0;">字体</td><td style="padding:10px 12px;background:#ffffff;color:#334155;border:1px solid #e2e8f0;">System UI Font Stack</td></tr>
<tr><td style="padding:10px 12px;background:#f1f5f9;font-weight:600;color:#475568;border:1px solid #e2e8f0;">间距</td><td style="padding:10px 12px;background:#ffffff;color:#334155;border:1px solid #e2e8f0;">8px 基准网格系统</td></tr>
<tr><td style="padding:10px 12px;background:#f1f5f9;font-weight:600;color:#475568;border:1px solid #e2e8f0;">圆角</td><td style="padding:10px 12px;background:#ffffff;color:#334155;border:1px solid #e2e8f0;">8px (卡片)</td></tr>
</table></td></tr>
<tr><td style="height:30px;"></td></tr>
<tr><td><h2 style="color:#1e293b;font-size:18px;font-weight:600;margin:0 0 16px 0;padding-bottom:8px;border-bottom:2px solid #e2e8f0;">✨ 核心特性</h2></td></tr>
<tr><td><table width="100%" cellpadding="0" cellspacing="0" border="0">
<tr><td style="padding:12px 16px;margin:8px 0;background:#ffffff;border-left:3px solid #8b5cf6;color:#475568;font-size:14px;">✓ 渐变页眉，视觉吸引力强</td></tr>
<tr><td style="padding:12px 16px;margin:8px 0;background:#ffffff;border-left:3px solid #8b5cf6;color:#475568;font-size:14px;">✓ 卡片式布局，信息层次清晰</td></tr>
<tr><td style="padding:12px 16px;margin:8px 0;background:#ffffff;border-left:3px solid #8b5cf6;color:#475568;font-size:14px;">✓ 左侧彩色边框，快速识别模块</td></tr>
<tr><td style="padding:12px 16px;margin:8px 0;background:#ffffff;border-left:3px solid #8b5cf6;color:#475568;font-size:14px;">✓ 统一的颜色系统和间距规范</td></tr>
<tr><td style="padding:12px 16px;margin:8px 0;background:#ffffff;border-left:3px solid #8b5cf6;color:#475568;font-size:14px;">✓ 响应式设计，移动端友好</td></tr>
</table></td></tr>
<tr><td style="height:20px;"></td></tr>
<tr><td style="background:#fef3c7;border-radius:8px;padding:20px;border:1px solid #f59e0b;">
<p style="color:#92400e;font-size:14px;margin:6px 0;font-weight:600;">⚠️ 设计注意事项</p>
<p style="color:#92400e;font-size:14px;margin:6px 0;">• 避免使用 emoji 作为 UI 图标</p>
<p style="color:#92400e;font-size:14px;margin:6px 0;">• 确保颜色对比度符合 WCAG AA 标准</p>
</td></tr>
<tr><td style="height:20px;"></td></tr>
<tr><td style="background:#d1fae5;border-radius:8px;padding:20px;border:1px solid #10b981;">
<p style="color:#065f46;font-size:14px;margin:6px 0;font-weight:600;">✅ 测试状态</p>
<p style="color:#065f46;font-size:14px;margin:6px 0;">邮件已发送，HTML 渲染正常，所有样式正确应用。</p>
</td></tr>
</table></td></tr>
<tr><td style="background:#f8fafc;padding:25px 30px;text-align:center;border-top:1px solid #e2e8f0;">
<p style="font-size:15px;font-weight:600;color:#1e293b;margin:0 0 8px 0;">小智助理</p>
<p style="font-size:13px;color:#64748b;margin:4px 0;">使用 simple-email 技能发送</p>
<p style="font-size:12px;color:#94a3b8;margin-top:12px;">© 2026 All Rights Reserved</p>
</td></tr>
</table>
</td></tr></table>
</body>
</html>


path: package-lock.json
content: {
  "name": "imap-smtp-email-skill",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "imap-smtp-email-skill",
      "version": "1.0.0",
      "license": "MIT",
      "dependencies": {
        "dotenv": "^16.6.1",
        "imap": "^0.8.19",
        "imap-simple": "^5.1.0",
        "mailparser": "^3.9.3",
        "nodemailer": "^7.0.13"
      }
    },
    "node_modules/@selderee/plugin-htmlparser2": {
      "version": "0.11.0",
      "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "domhandler": "^5.0.3",
        "selderee": "^0.11.0"
      },
      "funding": {
        "url": "https://ko-fi.com/killymxi"
      }
    },
    "node_modules/@zone-eu/mailsplit": {
      "version": "5.4.8",
      "resolved": "https://registry.npmjs.org/@zone-eu/mailsplit/-/mailsplit-5.4.8.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "(MIT OR EUPL-1.1+)",
      "dependencies": {
        "libbase64": "1.3.0",
        "libmime": "5.3.7",
        "libqp": "2.1.1"
      }
    },
    "node_modules/core-util-is": {
      "version": "1.0.3",
      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/deepmerge": {
      "version": "4.3.1",
      "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/dom-serializer": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "domelementtype": "^2.3.0",
        "domhandler": "^5.0.2",
        "entities": "^4.2.0"
      },
      "funding": {
        "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
      }
    },
    "node_modules/domelementtype": {
      "version": "2.3.0",
      "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/fb55"
        }
      ],
      "license": "BSD-2-Clause"
    },
    "node_modules/domhandler": {
      "version": "5.0.3",
      "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause",
      "dependencies": {
        "domelementtype": "^2.3.0"
      },
      "engines": {
        "node": ">= 4"
      },
      "funding": {
        "url": "https://github.com/fb55/domhandler?sponsor=1"
      }
    },
    "node_modules/domutils": {
      "version": "3.2.2",
      "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause",
      "dependencies": {
        "dom-serializer": "^2.0.0",
        "domelementtype": "^2.3.0",
        "domhandler": "^5.0.3"
      },
      "funding": {
        "url": "https://github.com/fb55/domutils?sponsor=1"
      }
    },
    "node_modules/dotenv": {
      "version": "16.6.1",
      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause",
      "engines": {
        "node": ">=12"
      },
      "funding": {
        "url": "https://dotenvx.com"
      }
    },
    "node_modules/encoding-japanese": {
      "version": "2.2.0",
      "resolved": "https://registry.npmjs.org/encoding-japanese/-/encoding-japanese-2.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=8.10.0"
      }
    },
    "node_modules/entities": {
      "version": "4.5.0",
      "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause",
      "engines": {
        "node": ">=0.12"
      },
      "funding": {
        "url": "https://github.com/fb55/entities?sponsor=1"
      }
    },
    "node_modules/he": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "bin": {
        "he": "bin/he"
      }
    },
    "node_modules/html-to-text": {
      "version": "9.0.5",
      "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "@selderee/plugin-htmlparser2": "^0.11.0",
        "deepmerge": "^4.3.1",
        "dom-serializer": "^2.0.0",
        "htmlparser2": "^8.0.2",
        "selderee": "^0.11.0"
      },
      "engines": {
        "node": ">=14"
      }
    },
    "node_modules/htmlparser2": {
      "version": "8.0.2",
      "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        "https://github.com/fb55/htmlparser2?sponsor=1",
        {
          "type": "github",
          "url": "https://github.com/sponsors/fb55"
        }
      ],
      "license": "MIT",
      "dependencies": {
        "domelementtype": "^2.3.0",
        "domhandler": "^5.0.3",
        "domutils": "^3.0.1",
        "entities": "^4.4.0"
      }
    },
    "node_modules/iconv-lite": {
      "version": "0.4.24",
      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "safer-buffer": ">= 2.1.2 < 3"
      },
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/imap": {
      "version": "0.8.19",
      "resolved": "https://registry.npmjs.org/imap/-/imap-0.8.19.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "readable-stream": "1.1.x",
        "utf7": ">=1.0.2"
      },
      "engines": {
        "node": ">=0.8.0"
      }
    },
    "node_modules/imap-simple": {
      "version": "5.1.0",
      "resolved": "https://registry.npmjs.org/imap-simple/-/imap-simple-5.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "iconv-lite": "~0.4.13",
        "imap": "^0.8.18",
        "nodeify": "^1.0.0",
        "quoted-printable": "^1.0.0",
        "utf8": "^2.1.1",
        "uuencode": "0.0.4"
      },
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/inherits": {
      "version": "2.0.4",
      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/is-promise": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/isarray": {
      "version": "0.0.1",
      "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/leac": {
      "version": "0.6.0",
      "resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "funding": {
        "url": "https://ko-fi.com/killymxi"
      }
    },
    "node_modules/libbase64": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/libbase64/-/libbase64-1.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/libmime": {
      "version": "5.3.7",
      "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.3.7.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "encoding-japanese": "2.2.0",
        "iconv-lite": "0.6.3",
        "libbase64": "1.3.0",
        "libqp": "2.1.1"
      }
    },
    "node_modules/libmime/node_modules/iconv-lite": {
      "version": "0.6.3",
      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "safer-buffer": ">= 2.1.2 < 3.0.0"
      },
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/libqp": {
      "version": "2.1.1",
      "resolved": "https://registry.npmjs.org/libqp/-/libqp-2.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/linkify-it": {
      "version": "5.0.0",
      "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "uc.micro": "^2.0.0"
      }
    },
    "node_modules/mailparser": {
      "version": "3.9.3",
      "resolved": "https://registry.npmjs.org/mailparser/-/mailparser-3.9.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "@zone-eu/mailsplit": "5.4.8",
        "encoding-japanese": "2.2.0",
        "he": "1.2.0",
        "html-to-text": "9.0.5",
        "iconv-lite": "0.7.2",
        "libmime": "5.3.7",
        "linkify-it": "5.0.0",
        "nodemailer": "7.0.13",
        "punycode.js": "2.3.1",
        "tlds": "1.261.0"
      }
    },
    "node_modules/mailparser/node_modules/iconv-lite": {
      "version": "0.7.2",
      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "safer-buffer": ">= 2.1.2 < 3.0.0"
      },
      "engines": {
        "node": ">=0.10.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/nodeify": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/nodeify/-/nodeify-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "is-promise": "~1.0.0",
        "promise": "~1.3.0"
      }
    },
    "node_modules/nodemailer": {
      "version": "7.0.13",
      "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.13.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT-0",
      "engines": {
        "node": ">=6.0.0"
      }
    },
    "node_modules/parseley": {
      "version": "0.12.1",
      "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "leac": "^0.6.0",
        "peberminta": "^0.9.0"
      },
      "funding": {
        "url": "https://ko-fi.com/killymxi"
      }
    },
    "node_modules/peberminta": {
      "version": "0.9.0",
      "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "funding": {
        "url": "https://ko-fi.com/killymxi"
      }
    },
    "node_modules/promise": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/promise/-/promise-1.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "is-promise": "~1"
      }
    },
    "node_modules/punycode.js": {
      "version": "2.3.1",
      "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/quoted-printable": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/quoted-printable/-/quoted-printable-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "utf8": "^2.1.0"
      },
      "bin": {
        "quoted-printable": "bin/quoted-printable"
      }
    },
    "node_modules/readable-stream": {
      "version": "1.1.14",
      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "core-util-is": "~1.0.0",
        "inherits": "~2.0.1",
        "isarray": "0.0.1",
        "string_decoder": "~0.10.x"
      }
    },
    "node_modules/safer-buffer": {
      "version": "2.1.2",
      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/selderee": {
      "version": "0.11.0",
      "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "parseley": "^0.12.0"
      },
      "funding": {
        "url": "https://ko-fi.com/killymxi"
      }
    },
    "node_modules/semver": {
      "version": "5.3.0",
      "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "bin": {
        "semver": "bin/semver"
      }
    },
    "node_modules/string_decoder": {
      "version": "0.10.31",
      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/tlds": {
      "version": "1.261.0",
      "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.261.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "bin": {
        "tlds": "bin.js"
      }
    },
    "node_modules/uc.micro": {
      "version": "2.1.0",
      "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/utf7": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/utf7/-/utf7-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "semver": "~5.3.0"
      }
    },
    "node_modules/utf8": {
      "version": "2.1.2",
      "resolved": "https://registry.npmjs.org/utf8/-/utf8-2.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/uuencode": {
      "version": "0.0.4",
      "resolved": "https://registry.npmjs.org/uuencode/-/uuencode-0.0.4.tgz",
      "integrity": [REDACTED_SECRET]
    }
  }
}


path: package.json
content: {
  "name": "imap-smtp-email-skill",
  "version": "1.0.0",
  "description": "IMAP/SMTP email tool for Claude. Works with Gmail, Outlook, 163.com, vip.163.com, 126.com, vip.126.com, 188.com, vip.188.com, and any standard IMAP/SMTP server.",
  "main": "scripts/imap.js",
  "scripts": {
    "check": "node scripts/imap.js check",
    "fetch": "node scripts/imap.js fetch",
    "search": "node scripts/imap.js search"
  },
  "dependencies": {
    "dotenv": "^16.6.1",
    "imap": "^0.8.19",
    "imap-simple": "^5.1.0",
    "mailparser": "^3.9.3",
    "nodemailer": "^7.0.13"
  },
  "keywords": [
    "imap",
    "smtp",
    "email",
    "163.com",
    "126.com",
    "188.com",
    "gmail",
    "outlook",
    "skill"
  ],
  "author": "NetEase",
  "license": "MIT"
}


path: scripts/imap.js
content: #!/usr/bin/env node

/**
 * IMAP Email CLI
 * Works with any standard IMAP server (Gmail, ProtonMail Bridge, Fastmail, etc.)
 * Supports IMAP ID extension (RFC 2971) for 163.com and other servers
 */

const Imap = require('imap');
const { simpleParser } = require('mailparser');
const path = require('path');
const fs = require('fs');
const os = require('os');
require('dotenv').config({ path: path.resolve(__dirname, '../.env') });

function validateWritePath(dirPath) {
  const allowedDirsStr = process.env.ALLOWED_WRITE_DIRS;
  if (!allowedDirsStr) {
    throw new Error('ALLOWED_WRITE_DIRS not set in .env. Attachment download is disabled.');
  }

  const resolved = path.resolve(dirPath.replace(/^~/, os.homedir()));

  const allowedDirs = allowedDirsStr.split(',').map(d =>
    path.resolve(d.trim().replace(/^~/, os.homedir()))
  );

  const allowed = allowedDirs.some(dir =>
    resolved === dir || resolved.startsWith(dir + path.sep)
  );

  if (!allowed) {
    throw new Error(`Access denied: '${dirPath}' is outside allowed write directories`);
  }

  return resolved;
}

function sanitizeFilename(filename) {
  return path.basename(filename).replace(/\.\./g, '').replace(/^[./\\]/, '') || 'attachment';
}

// IMAP ID information for 163.com compatibility
const IMAP_ID = {
  name: 'openclaw',
  version: '0.0.1',
  vendor: 'netease',
  'support-email': '[REDACTED_SECRET]'
};

const DEFAULT_MAILBOX = process.env.IMAP_MAILBOX || 'INBOX';

// Parse command-line arguments
function parseArgs() {
  const args = process.argv.slice(2);
  const command = args[0];
  const options = {};
  const positional = [];

  for (let i = 1; i < args.length; i++) {
    const arg = args[i];
    if (arg.startsWith('--')) {
      const key = arg.slice(2);
      const value = args[i + 1];
      // Only treat next arg as value if it doesn't start with --
      if (value && !value.startsWith('--')) {
        options[key] = value;
        i++;
      } else {
        options[key] = true;
      }
    } else {
      positional.push(arg);
    }
  }

  return { command, options, positional };
}

// Create IMAP connection config
function createImapConfig() {
  return {
    user: process.env.IMAP_USER,
    [REDACTED_SECRET],
    host: process.env.IMAP_HOST || '127.0.0.1',
    port: parseInt(process.env.IMAP_PORT) || 1143,
    tls: process.env.IMAP_TLS === 'true',
    tlsOptions: {
      rejectUnauthorized: process.env.IMAP_REJECT_UNAUTHORIZED !== 'false',
    },
    connTimeout: 10000,
    authTimeout: 10000,
  };
}

// Connect to IMAP server with ID support
async function connect() {
  const config = createImapConfig();

  if (!config.user || !config.password) {
    throw new Error('Missing IMAP_USER or IMAP_PASS environment variables');
  }

  return new Promise((resolve, reject) => {
    const imap = new Imap(config);

    imap.once('ready', () => {
      // Send IMAP ID command for 163.com compatibility
      if (typeof imap.id === 'function') {
        imap.id(IMAP_ID, (err) => {
          if (err) {
            console.warn('Warning: IMAP ID command failed:', err.message);
          }
          resolve(imap);
        });
      } else {
        // ID not supported, continue without it
        resolve(imap);
      }
    });

    imap.once('error', (err) => {
      reject(new Error(`IMAP connection failed: ${err.message}`));
    });

    imap.connect();
  });
}

// Open mailbox and return promise
function openBox(imap, mailbox, readOnly = false) {
  return new Promise((resolve, reject) => {
    imap.openBox(mailbox, readOnly, (err, box) => {
      if (err) reject(err);
      else resolve(box);
    });
  });
}

// Search for messages
function searchMessages(imap, criteria, fetchOptions) {
  return new Promise((resolve, reject) => {
    imap.search(criteria, (err, results) => {
      if (err) {
        reject(err);
        return;
      }

      if (!results || results.length === 0) {
        resolve([]);
        return;
      }

      const fetch = imap.fetch(results, fetchOptions);
      const messages = [];

      fetch.on('message', (msg) => {
        const parts = [];

        msg.on('body', (stream, info) => {
          let buffer = '';

          stream.on('data', (chunk) => {
            buffer += chunk.toString('utf8');
          });

          stream.once('end', () => {
            parts.push({ which: info.which, body: buffer });
          });
        });

        msg.once('attributes', (attrs) => {
          parts.forEach((part) => {
            part.attributes = attrs;
          });
        });

        msg.once('end', () => {
          if (parts.length > 0) {
            messages.push(parts[0]);
          }
        });
      });

      fetch.once('error', (err) => {
        reject(err);
      });

      fetch.once('end', () => {
        resolve(messages);
      });
    });
  });
}

// Parse email from raw buffer
async function parseEmail(bodyStr, includeAttachments = false) {
  const parsed = await simpleParser(bodyStr);

  return {
    from: parsed.from?.text || 'Unknown',
    to: parsed.to?.text,
    subject: parsed.subject || '(no subject)',
    date: parsed.date,
    text: parsed.text,
    html: parsed.html,
    snippet: parsed.text
      ? parsed.text.slice(0, 200)
      : (parsed.html ? parsed.html.slice(0, 200).replace(/<[^>]*>/g, '') : ''),
    attachments: parsed.attachments?.map((a) => ({
      filename: a.filename,
      contentType: a.contentType,
      size: a.size,
      content: includeAttachments ? a.content : undefined,
      cid: a.cid,
    })),
  };
}

// Check for new/unread emails
async function checkEmails(mailbox = DEFAULT_MAILBOX, limit = 10, recentTime = null, unreadOnly = false) {
  const imap = await connect();

  try {
    await openBox(imap, mailbox);

    // Build search criteria
    const searchCriteria = unreadOnly ? ['UNSEEN'] : ['ALL'];

    if (recentTime) {
      const sinceDate = parseRelativeTime(recentTime);
      searchCriteria.push(['SINCE', sinceDate]);
    }

    // Fetch messages sorted by date (newest first)
    const fetchOptions = {
      bodies: [''],
      markSeen: false,
    };

    const messages = await searchMessages(imap, searchCriteria, fetchOptions);

    // Sort by date (newest first) - parse from message attributes
    const sortedMessages = messages.sort((a, b) => {
      const dateA = a.attributes.date ? new Date(a.attributes.date) : new Date(0);
      const dateB = b.attributes.date ? new Date(b.attributes.date) : new Date(0);
      return dateB - dateA;
    }).slice(0, limit);

    const results = [];

    for (const item of sortedMessages) {
      const bodyStr = item.body;
      const parsed = await parseEmail(bodyStr);

      results.push({
        uid: item.attributes.uid,
        ...parsed,
        flags: item.attributes.flags,
      });
    }

    return results;
  } finally {
    imap.end();
  }
}

// Fetch full email by UID
async function fetchEmail(uid, mailbox = DEFAULT_MAILBOX) {
  const imap = await connect();

  try {
    await openBox(imap, mailbox);

    const searchCriteria = [['UID', uid]];
    const fetchOptions = {
      bodies: [''],
      markSeen: false,
    };

    const messages = await searchMessages(imap, searchCriteria, fetchOptions);

    if (messages.length === 0) {
      throw new Error(`Message UID ${uid} not found`);
    }

    const item = messages[0];
    const parsed = await parseEmail(item.body);

    return {
      uid: item.attributes.uid,
      ...parsed,
      flags: item.attributes.flags,
    };
  } finally {
    imap.end();
  }
}

// Download attachments from email
async function downloadAttachments(uid, mailbox = DEFAULT_MAILBOX, outputDir = '.', specificFilename = null) {
  const imap = await connect();

  try {
    await openBox(imap, mailbox);

    const searchCriteria = [['UID', uid]];
    const fetchOptions = {
      bodies: [''],
      markSeen: false,
    };

    const messages = await searchMessages(imap, searchCriteria, fetchOptions);

    if (messages.length === 0) {
      throw new Error(`Message UID ${uid} not found`);
    }

    const item = messages[0];
    const parsed = await parseEmail(item.body, true);

    if (!parsed.attachments || parsed.attachments.length === 0) {
      return {
        uid,
        downloaded: [],
        message: 'No attachments found',
      };
    }

    // Create output directory if it doesn't exist
    const resolvedDir = validateWritePath(outputDir);
    if (!fs.existsSync(resolvedDir)) {
      fs.mkdirSync(resolvedDir, { recursive: true });
    }

    const downloaded = [];

    for (const attachment of parsed.attachments) {
      // If specificFilename is provided, only download matching attachment
      if (specificFilename && attachment.filename !== specificFilename) {
        continue;
      }
      if (attachment.content) {
        const filePath = path.join(resolvedDir, sanitizeFilename(attachment.filename));
        fs.writeFileSync(filePath, attachment.content);
        downloaded.push({
          filename: attachment.filename,
          path: filePath,
          size: attachment.size,
        });
      }
    }

    // If specific file was requested but not found
    if (specificFilename && downloaded.length === 0) {
      const availableFiles = parsed.attachments.map(a => a.filename).join(', ');
      return {
        uid,
        downloaded: [],
        message: `File "${specificFilename}" not found. Available attachments: ${availableFiles}`,
      };
    }

    return {
      uid,
      downloaded,
      message: `Downloaded ${downloaded.length} attachment(s)`,
    };
  } finally {
    imap.end();
  }
}

// Parse relative time (e.g., "2h", "30m", "7d") to Date
function parseRelativeTime(timeStr) {
  const match = timeStr.match(/^(\d+)(m|h|d)$/);
  if (!match) {
    throw new Error('Invalid time format. Use: 30m, 2h, 7d');
  }

  const value = parseInt(match[1]);
  const unit = match[2];
  const now = new Date();

  switch (unit) {
    case 'm': // minutes
      return new Date(now.getTime() - value * 60 * 1000);
    case 'h': // hours
      return new Date(now.getTime() - value * 60 * 60 * 1000);
    case 'd': // days
      return new Date(now.getTime() - value * 24 * 60 * 60 * 1000);
    default:
      throw new Error('Unknown time unit');
  }
}

// Search emails with criteria
async function searchEmails(options) {
  const imap = await connect();

  try {
    const mailbox = options.mailbox || DEFAULT_MAILBOX;
    await openBox(imap, mailbox);

    const criteria = [];

    if (options.unseen) criteria.push('UNSEEN');
    if (options.seen) criteria.push('SEEN');
    if (options.from) criteria.push(['FROM', options.from]);
    if (options.subject) criteria.push(['SUBJECT', options.subject]);

    // Handle relative time (--recent 2h)
    if (options.recent) {
      const sinceDate = parseRelativeTime(options.recent);
      criteria.push(['SINCE', sinceDate]);
    } else {
      // Handle absolute dates
      if (options.since) criteria.push(['SINCE', options.since]);
      if (options.before) criteria.push(['BEFORE', options.before]);
    }

    // Default to all if no criteria
    if (criteria.length === 0) criteria.push('ALL');

    const fetchOptions = {
      bodies: [''],
      markSeen: false,
    };

    const messages = await searchMessages(imap, criteria, fetchOptions);
    const limit = parseInt(options.limit) || 20;
    const results = [];

    // Sort by date (newest first)
    const sortedMessages = messages.sort((a, b) => {
      const dateA = a.attributes.date ? new Date(a.attributes.date) : new Date(0);
      const dateB = b.attributes.date ? new Date(b.attributes.date) : new Date(0);
      return dateB - dateA;
    }).slice(0, limit);

    for (const item of sortedMessages) {
      const parsed = await parseEmail(item.body);
      results.push({
        uid: item.attributes.uid,
        ...parsed,
        flags: item.attributes.flags,
      });
    }

    return results;
  } finally {
    imap.end();
  }
}

// Mark message(s) as read
async function markAsRead(uids, mailbox = DEFAULT_MAILBOX) {
  const imap = await connect();

  try {
    await openBox(imap, mailbox);

    return new Promise((resolve, reject) => {
      imap.addFlags(uids, '\\Seen', (err) => {
        if (err) reject(err);
        else resolve({ success: true, uids, action: 'marked as read' });
      });
    });
  } finally {
    imap.end();
  }
}

// Mark message(s) as unread
async function markAsUnread(uids, mailbox = DEFAULT_MAILBOX) {
  const imap = await connect();

  try {
    await openBox(imap, mailbox);

    return new Promise((resolve, reject) => {
      imap.delFlags(uids, '\\Seen', (err) => {
        if (err) reject(err);
        else resolve({ success: true, uids, action: 'marked as unread' });
      });
    });
  } finally {
    imap.end();
  }
}

// List all mailboxes
async function listMailboxes() {
  const imap = await connect();

  try {
    return new Promise((resolve, reject) => {
      imap.getBoxes((err, boxes) => {
        if (err) reject(err);
        else resolve(formatMailboxTree(boxes));
      });
    });
  } finally {
    imap.end();
  }
}

// Format mailbox tree recursively
function formatMailboxTree(boxes, prefix = '') {
  const result = [];
  for (const [name, info] of Object.entries(boxes)) {
    const fullName = prefix ? `${prefix}${info.delimiter}${name}` : name;
    result.push({
      name: fullName,
      delimiter: info.delimiter,
      attributes: info.attribs,
    });

    if (info.children) {
      result.push(...formatMailboxTree(info.children, fullName));
    }
  }
  return result;
}

// Main CLI handler
async function main() {
  const { command, options, positional } = parseArgs();

  try {
    let result;

    switch (command) {
      case 'check':
        result = await checkEmails(
          options.mailbox || DEFAULT_MAILBOX,
          parseInt(options.limit) || 10,
          options.recent || null,
          options.unseen === 'true' // if --unseen is set, only get unread messages
        );
        break;

      case 'fetch':
        if (!positional[0]) {
          throw new Error('UID required: node imap.js fetch <uid>');
        }
        result = await fetchEmail(positional[0], options.mailbox);
        break;

      case 'download':
        if (!positional[0]) {
          throw new Error('UID required: node imap.js download <uid>');
        }
        result = await downloadAttachments(positional[0], options.mailbox, options.dir || '.', options.file || null);
        break;

      case 'search':
        result = await searchEmails(options);
        break;

      case 'mark-read':
        if (positional.length === 0) {
          throw new Error('UID(s) required: node imap.js mark-read <uid> [uid2...]');
        }
        result = await markAsRead(positional, options.mailbox);
        break;

      case 'mark-unread':
        if (positional.length === 0) {
          throw new Error('UID(s) required: node imap.js mark-unread <uid> [uid2...]');
        }
        result = await markAsUnread(positional, options.mailbox);
        break;

      case 'list-mailboxes':
        result = await listMailboxes();
        break;

      default:
        console.error('Unknown command:', command);
        console.error('Available commands: check, fetch, download, search, mark-read, mark-unread, list-mailboxes');
        process.exit(1);
    }

    console.log(JSON.stringify(result, null, 2));
  } catch (err) {
    console.error('Error:', err.message);
    process.exit(1);
  }
}

main();


path: scripts/smtp.js
content: #!/usr/bin/env node

/**
 * SMTP Email CLI
 * Send email via SMTP protocol. Works with Gmail, Outlook, 163.com, and any standard SMTP server.
 * Supports attachments, HTML content, and multiple recipients.
 */

const nodemailer = require('nodemailer');
const path = require('path');
const os = require('os');
const fs = require('fs');
require('dotenv').config({ path: path.resolve(__dirname, '../.env') });

function validateReadPath(inputPath) {
  let realPath;
  try {
    realPath = fs.realpathSync(inputPath);
  } catch {
    realPath = path.resolve(inputPath);
  }

  const allowedDirsStr = process.env.ALLOWED_READ_DIRS;
  if (!allowedDirsStr) {
    throw new Error('ALLOWED_READ_DIRS not set in .env. File read operations are disabled.');
  }

  const allowedDirs = allowedDirsStr.split(',').map(d =>
    path.resolve(d.trim().replace(/^~/, os.homedir()))
  );

  const allowed = allowedDirs.some(dir =>
    realPath === dir || realPath.startsWith(dir + path.sep)
  );

  if (!allowed) {
    throw new Error(`Access denied: '${inputPath}' is outside allowed read directories`);
  }

  return realPath;
}

// Parse command-line arguments
function parseArgs() {
  const args = process.argv.slice(2);
  const command = args[0];
  const options = {};
  const positional = [];

  for (let i = 1; i < args.length; i++) {
    const arg = args[i];
    if (arg.startsWith('--')) {
      const key = arg.slice(2);
      const value = args[i + 1];
      // Only treat next arg as value if it doesn't start with --
      if (value && !value.startsWith('--')) {
        options[key] = value;
        i++;
      } else {
        options[key] = true;
      }
    } else {
      positional.push(arg);
    }
  }

  return { command, options, positional };
}

// Create SMTP transporter
function createTransporter() {
  const config = {
    host: process.env.SMTP_HOST,
    port: parseInt(process.env.SMTP_PORT) || 587,
    secure: process.env.SMTP_SECURE === 'true', // true for 465, false for other ports
    auth: {
      user: process.env.SMTP_USER,
      pass: process.env.SMTP_PASS,
    },
    tls: {
      rejectUnauthorized: process.env.SMTP_REJECT_UNAUTHORIZED !== 'false',
    },
  };

  if (!config.host || !config.auth.user || !config.auth.pass) {
    throw new Error('Missing SMTP configuration. Please set SMTP_HOST, SMTP_USER, and SMTP_PASS in .env');
  }

  return nodemailer.createTransport(config);
}

// Send email
async function sendEmail(options) {
  const transporter = createTransporter();

  // Verify connection
  try {
    await transporter.verify();
    console.error('SMTP server is ready to send');
  } catch (err) {
    throw new Error(`SMTP connection failed: ${err.message}`);
  }

  const mailOptions = {
    from: options.from || process.env.SMTP_FROM || process.env.SMTP_USER,
    to: options.to,
    cc: options.cc || undefined,
    bcc: options.bcc || undefined,
    subject: options.subject || '(no subject)',
    text: options.text || undefined,
    html: options.html || undefined,
    attachments: options.attachments || [],
  };

  // If neither text nor html provided, use default text
  if (!mailOptions.text && !mailOptions.html) {
    mailOptions.text = options.body || '';
  }

  const info = await transporter.sendMail(mailOptions);

  return {
    success: true,
    messageId: info.messageId,
    response: info.response,
    to: mailOptions.to,
  };
}

// Read file content for attachments
function readAttachment(filePath) {
  validateReadPath(filePath);
  if (!fs.existsSync(filePath)) {
    throw new Error(`Attachment file not found: ${filePath}`);
  }
  return {
    filename: path.basename(filePath),
    path: path.resolve(filePath),
  };
}

// Send email with file content
async function sendEmailWithContent(options) {
  // Handle attachments
  if (options.attach) {
    const attachFiles = options.attach.split(',').map(f => f.trim());
    options.attachments = attachFiles.map(f => readAttachment(f));
  }

  return await sendEmail(options);
}

// Test SMTP connection
async function testConnection() {
  const transporter = createTransporter();

  try {
    await transporter.verify();
    const info = await transporter.sendMail({
      from: process.env.SMTP_FROM || process.env.SMTP_USER,
      to: process.env.SMTP_USER, // Send to self
      subject: 'SMTP Connection Test',
      text: 'This is a test email from the IMAP/SMTP email skill.',
      html: '<p>This is a <strong>test email</strong> from the IMAP/SMTP email skill.</p>',
    });

    return {
      success: true,
      message: 'SMTP connection successful',
      messageId: info.messageId,
    };
  } catch (err) {
    throw new Error(`SMTP test failed: ${err.message}`);
  }
}

// Main CLI handler
async function main() {
  const { command, options, positional } = parseArgs();

  try {
    let result;

    switch (command) {
      case 'send':
        if (!options.to) {
          throw new Error('Missing required option: --to <email>');
        }
        if (!options.subject && !options['subject-file']) {
          throw new Error('Missing required option: --subject <text> or --subject-file <file>');
        }

        // Read subject from file if specified
        if (options['subject-file']) {
          validateReadPath(options['subject-file']);
          options.subject = fs.readFileSync(options['subject-file'], 'utf8').trim();
        }

        // Read body from file if specified
        if (options['body-file']) {
          validateReadPath(options['body-file']);
          const content = fs.readFileSync(options['body-file'], 'utf8');
          if (options['body-file'].endsWith('.html') || options.html) {
            options.html = content;
          } else {
            options.text = content;
          }
        } else if (options['html-file']) {
          validateReadPath(options['html-file']);
          options.html = fs.readFileSync(options['html-file'], 'utf8');
        } else if (options.body) {
          if (options.html === true) {
            options.html = options.body;
          } else {
            options.text = options.body;
          }
        }

        result = await sendEmailWithContent(options);
        break;

      case 'test':
        result = await testConnection();
        break;

      default:
        console.error('Unknown command:', command);
        console.error('Available commands: send, test');
        console.error('\nUsage:');
        console.error('  send   --to <email> --subject <text> [--body <text>] [--html] [--cc <email>] [--bcc <email>] [--attach <file>]');
        console.error('  send   --to <email> --subject <text> --body-file <file> [--html-file <file>] [--attach <file>]');
        console.error('  test   Test SMTP connection');
        process.exit(1);
    }

    console.log(JSON.stringify(result, null, 2));
  } catch (err) {
    console.error('Error:', err.message);
    process.exit(1);
  }
}

main();


path: setup.sh
content: #!/bin/bash

# IMAP/SMTP Email Skill Setup Helper

echo "================================"
echo "  IMAP/SMTP Email Skill Setup"
echo "================================"
echo ""
echo "This script will help you create a .env file with your email credentials."
echo ""

# Prompt for email provider
echo "Select your email provider:"
echo "  1) Gmail"
echo "  2) Outlook"
echo "  3) 163.com"
echo "  4) vip.163.com"
echo "  5) 126.com"
echo "  6) vip.126.com"
echo "  7) 188.com"
echo "  8) vip.188.com"
echo "  9) yeah.net"
echo " 10) QQ Mail"
echo " 11) Custom"
echo ""
read -p "Enter choice (1-11): " PROVIDER_CHOICE

case $PROVIDER_CHOICE in
  1)
    IMAP_HOST="imap.gmail.com"
    IMAP_PORT="993"
    SMTP_HOST="smtp.gmail.com"
    SMTP_PORT="587"
    SMTP_SECURE="false"
    IMAP_TLS="true"
    echo ""
    echo "⚠️  Gmail requires an App Password — your regular Google password will NOT work."
    echo "   1. Go to: https://myaccount.google.com/apppasswords"
    echo "   2. Generate an App Password (requires 2-Step Verification enabled)"
    echo "   3. Use the generated 16-character password below"
    echo ""
    ;;
  2)
    IMAP_HOST="outlook.office365.com"
    IMAP_PORT="993"
    SMTP_HOST="smtp.office365.com"
    SMTP_PORT="587"
    SMTP_SECURE="false"
    IMAP_TLS="true"
    ;;
  3)
    IMAP_HOST="imap.163.com"
    IMAP_PORT="993"
    SMTP_HOST="smtp.163.com"
    SMTP_PORT="465"
    SMTP_SECURE="true"
    IMAP_TLS="true"
    ;;
  4)
    IMAP_HOST="imap.vip.163.com"
    IMAP_PORT="993"
    SMTP_HOST="smtp.vip.163.com"
    SMTP_PORT="465"
    SMTP_SECURE="true"
    IMAP_TLS="true"
    ;;
  5)
    IMAP_HOST="imap.126.com"
    IMAP_PORT="993"
    SMTP_HOST="smtp.126.com"
    SMTP_PORT="465"
    SMTP_SECURE="true"
    IMAP_TLS="true"
    ;;
  6)
    IMAP_HOST="imap.vip.126.com"
    IMAP_PORT="993"
    SMTP_HOST="smtp.vip.126.com"
    SMTP_PORT="465"
    SMTP_SECURE="true"
    IMAP_TLS="true"
    ;;
  7)
    IMAP_HOST="imap.188.com"
    IMAP_PORT="993"
    SMTP_HOST="smtp.188.com"
    SMTP_PORT="465"
    SMTP_SECURE="true"
    IMAP_TLS="true"
    ;;
  8)
    IMAP_HOST="imap.vip.188.com"
    IMAP_PORT="993"
    SMTP_HOST="smtp.vip.188.com"
    SMTP_PORT="465"
    SMTP_SECURE="true"
    IMAP_TLS="true"
    ;;
  9)
    IMAP_HOST="imap.yeah.net"
    IMAP_PORT="993"
    SMTP_HOST="smtp.yeah.net"
    SMTP_PORT="465"
    SMTP_SECURE="true"
    IMAP_TLS="true"
    ;;
  10)
    IMAP_HOST="imap.qq.com"
    IMAP_PORT="993"
    SMTP_HOST="smtp.qq.com"
    SMTP_PORT="587"
    SMTP_SECURE="false"
    IMAP_TLS="true"
    ;;
  11)
    read -p "IMAP Host: " IMAP_HOST
    read -p "IMAP Port: " IMAP_PORT
    read -p "SMTP Host: " SMTP_HOST
    read -p "SMTP Port: " SMTP_PORT
    read -p "Use TLS for IMAP? (true/false): " IMAP_TLS
    read -p "Use SSL for SMTP? (true/false): " SMTP_SECURE
    ;;
  *)
    echo "Invalid choice"
    exit 1
    ;;
esac

echo ""
read -p "Email address: " EMAIL
read -s -p "Password / App Password / Authorization Code: " PASSWORD
echo ""
read -p "Accept self-signed certificates? (y/n): " ACCEPT_CERT
if [ "$ACCEPT_CERT" = "y" ]; then
  REJECT_UNAUTHORIZED="false"
else
  REJECT_UNAUTHORIZED="true"
fi

read -p "Allowed directories for reading files (comma-separated, e.g. ~/Downloads,~/Documents): " ALLOWED_READ_DIRS
read -p "Allowed directories for saving attachments (comma-separated, e.g. ~/Downloads): " ALLOWED_WRITE_DIRS

# Create .env file
cat > .env << EOF
# IMAP Configuration
IMAP_HOST=$IMAP_HOST
IMAP_PORT=$IMAP_PORT
IMAP_USER=$EMAIL
IMAP_PASS=$PASSWORD
IMAP_TLS=$IMAP_TLS
IMAP_REJECT_UNAUTHORIZED=$REJECT_UNAUTHORIZED
IMAP_MAILBOX=INBOX

# SMTP Configuration
SMTP_HOST=$SMTP_HOST
SMTP_PORT=$SMTP_PORT
SMTP_SECURE=$SMTP_SECURE
SMTP_USER=$EMAIL
SMTP_PASS=$PASSWORD
SMTP_FROM=$EMAIL
SMTP_REJECT_UNAUTHORIZED=$REJECT_UNAUTHORIZED

# File access whitelist (security)
ALLOWED_READ_DIRS=${ALLOWED_READ_DIRS:-$HOME/Downloads,$HOME/Documents}
ALLOWED_WRITE_DIRS=${ALLOWED_WRITE_DIRS:-$HOME/Downloads}
EOF

echo ""
echo "✅ Created .env file"
chmod 600 .env
echo "✅ Set .env file permissions to 600 (owner read/write only)"
echo ""
echo "Testing connections..."
echo ""

# Test IMAP connection
echo "Testing IMAP..."
if node scripts/imap.js list-mailboxes >/dev/null 2>&1; then
    echo "✅ IMAP connection successful!"
else
    echo "❌ IMAP connection test failed"
    echo "   Please check your credentials and settings"
fi

# Test SMTP connection
echo ""
echo "Testing SMTP..."
echo "  (This will send a test email to your own address: $EMAIL)"
if node scripts/smtp.js test >/dev/null 2>&1; then
    echo "✅ SMTP connection successful!"
else
    echo "❌ SMTP connection test failed"
    echo "   Please check your credentials and settings"
fi

echo ""
echo "Setup complete! Try:"
echo "  node scripts/imap.js check"
echo "  node scripts/smtp.js send --to [REDACTED_SECRET] --subject Test --body 'Hello World'"


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