
Email Manager
- 88 installs
- 82 repo stars
- Updated August 2, 2026
- aaaaqwq/claude-code-skills
email-manager is a Claude Code skill that manages multiple IMAP mailboxes, generates AI email summaries, and drafts confirmation-gated replies.
About
email-manager is a Claude Code skill that unifies multiple IMAP mailboxes such as Gmail and QQ Mail. It checks mail on a schedule, generates AI summaries of unread messages, and drafts replies that require user confirmation before sending. A developer uses it to triage inbox volume without manually opening each account. It stores credentials with pass and gates sending and deletion behind explicit permission tiers.
- Manages multiple IMAP mailboxes (Gmail, QQ, Outlook, 163, 126) from one skill
- Scheduled email checks at 8:00 and 17:00 with AI-generated summaries
- Drafts replies but requires user confirmation before sending
Email Manager by the numbers
- 88 all-time installs (skills.sh)
- Ranked #859 of 2,719 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
email-manager capabilities & compatibility
Free skill; needs your own mailbox credentials (IMAP app passwords) stored in pass.
- Capabilities
- email triage · email summary · reply draft · imap sync · scheduled check
- Works with
- gmail · outlook
- Use cases
- Pricing
- Bring your own API key
What email-manager says it does
多邮箱统一管理与智能助手。支持 Gmail、QQ邮箱等 IMAP 邮箱,定时查看邮件,AI 生成摘要和回复草稿,发送前需用户确认。
每日 8:00 和 17:00 自动检查
npx skills add https://github.com/aaaaqwq/claude-code-skills --skill email-managerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 88 |
|---|---|
| repo stars | ★ 82 |
| Last updated | August 2, 2026 |
| Repository | aaaaqwq/claude-code-skills ↗ |
What it does
A developer triages multiple email accounts by pulling scheduled AI summaries and confirming reply drafts before they send.
Who is it for?
Developers who want scheduled summaries across several mailboxes and AI reply drafts that never auto-send.
Skip if: Users needing automatic sending or bulk deletion, since deletion is blocked and sending requires confirmation.
When should I use this skill?
When you want to check mail, get an email summary, or draft a reply across configured IMAP accounts.
What you get
Scheduled, prioritized summaries of unread mail plus ready-to-confirm reply drafts.
- Prioritized email summaries
- AI reply drafts pending confirmation
By the numbers
- Scheduled checks at 08:00 and 17:00 daily
- 5 preset IMAP mailbox configurations listed
- 6-row permission tier table
Files
Email Manager - 邮箱智能管家
当使用此技能
- 管理多个邮箱账号(Gmail、QQ、Outlook 等)
- 定时查看邮件并生成摘要
- 智能生成回复草稿(需确认后发送)
- 重要邮件即时提醒
核心功能
1. 多账号管理
- 支持任意 IMAP 邮箱
- 使用
pass安全存储凭据 - 配置文件:
config/accounts.json
2. 定时任务
- 每日 8:00 和 17:00 自动检查
- 通过 OpenClaw cron 调度
- 发现重要邮件即时通知
3. 智能摘要
- 使用 AI 提取邮件要点
- 按优先级分类
- 推送到 WhatsApp/Telegram
4. 安全发送
- AI 生成回复草稿
- 发送前展示预览
- 用户确认后才发送
触发词
- "查看邮件"
- "邮件摘要"
- "回复邮件"
- "添加邮箱账号"
- "邮件设置"
配置步骤
1. 添加邮箱账号
# Gmail(需先开启 IMAP 并生成应用专用密码)
pass insert email/gmail/your-email@gmail.com
pass insert email/gmail/your-email@gmail.com-app-pass
# QQ 邮箱(需先开启 IMAP 服务并获取授权码)
pass insert email/qq/your-qq@qq.com
pass insert email/qq/your-qq@qq.com-auth-code2. 更新账号配置
编辑 config/accounts.json:
{
"accounts": [
{
"name": "Gmail 主邮箱",
"email": "your-email@gmail.com",
"provider": "gmail",
"imap_server": "imap.gmail.com",
"imap_port": 993,
"smtp_server": "smtp.gmail.com",
"smtp_port": 587,
"enabled": true
},
{
"name": "QQ 邮箱",
"email": "your-qq@qq.com",
"provider": "qq",
"imap_server": "imap.qq.com",
"imap_port": 993,
"smtp_server": "smtp.qq.com",
"smtp_port": 587,
"enabled": true
}
]
}3. 设置定时任务
使用 OpenClaw cron:
# 8:00 检查邮件
openclaw cron add --name "早间邮件检查" --schedule "0 8 * * *" --script "~/clawd/skills/email-manager/scripts/check_email.py"
# 17:00 检查邮件
openclaw cron add --name "晚间邮件检查" --schedule "0 17 * * *" --script "~/clawd/skills/email-manager/scripts/check_email.py"安全策略
| 操作 | 权限级别 |
|---|---|
| 读取邮件列表 | 🟢 自动 |
| 读取邮件内容 | 🟢 自动 |
| 生成摘要 | 🟢 自动 |
| 创建回复草稿 | 🟢 自动 |
| 发送邮件 | 🟡 需确认 |
| 删除邮件 | 🔴 禁止 |
相关文件
scripts/check_email.py- 邮件检查与摘要scripts/send_email.py- 发送邮件(需确认)scripts/reply_draft.py- 生成回复草稿config/accounts.json- 账号配置config/providers.json- 邮件服务商配置
常见邮箱 IMAP 配置
| 邮箱 | IMAP 服务器 | 端口 |
|---|---|---|
| Gmail | imap.gmail.com | 993 |
| QQ 邮箱 | imap.qq.com | 993 |
| Outlook | outlook.office365.com | 993 |
| 163 邮箱 | imap.163.com | 993 |
| 126 邮箱 | imap.126.com | 993 |
使用示例
查看邮件摘要
用户: 查看邮件
AI: [读取所有配置邮箱,生成摘要]
📧 今日邮件摘要 (2026-02-19 08:00)
Gmail 主邮箱 (3 封未读):
1. [重要] 项目进度更新 - 张三
2. 会议邀请:周五例会 - 李四
3. 订单发货通知 - 淘宝
QQ 邮箱 (1 封未读):
1. 验证码通知 - 某服务回复邮件
用户: 回复第1封邮件,告诉他项目进度正常
AI: [生成回复草稿]
📝 回复草稿:
收件人: 张三 <zhangsan@example.com>
主题: Re: 项目进度更新
内容:
---
张三,你好!
感谢你的关注。项目进度一切正常,目前按计划推进中。
如有其他问题,随时联系。
祝好,
Daniel
---
确认发送吗?(回复"确认"发送,"修改"调整,"取消"放弃)---
故障排查
SSL/连接问题
- 症状:
Connection reset by peer,SSL: UNEXPECTED_EOF_WHILE_READING - 根因: Mihomo/Clash TUN 模式劫持 IMAP 长连接不稳定
- 解决:
- Gmail 等海外邮箱:在 Mihomo
Merge.yaml添加DOMAIN-SUFFIX,imap.gmail.com,DIRECT - QQ/163 等国内邮箱:需走国内代理出口,不可直连(海外 VPS)
- 代码已内置 ssl_context + 3次重试 + 2s退避
编码问题
- 症状:
LookupError: unknown encoding: unknown-8bit - 解决:
_parse_email已添加 try/except 兜底用 utf-8 解码
超时问题
- 症状:
check_email.py被 SIGKILL - 解决: 跳过
test_connection、limit 降到 10、使用headers_only=True
创建日期: 2026-02-19 更新日期: 2026-02-24 版本: 1.1.0
{
"timestamp": "2026-02-28T08:33:55.228784",
"emails": {
"QQ邮箱": [
{
"id": "839",
"subject": "【阿里云】云安全中心周报",
"from": "阿里云 <monitor@monitor.aliyun.com>",
"to": "REDACTED_EMAIL",
"date": "Thu, 19 Feb 2026 08:03:21 +0800 (CST)",
"body": "",
"has_attachment": false
},
{
"id": "840",
"subject": "[GitHub] Your GitHub Enterprise trial has ended",
"from": "GitHub <support@github.com>",
"to": "ArMatrix_admin <REDACTED_EMAIL>",
"date": "Wed, 18 Feb 2026 18:12:34 -0800",
"body": "",
"has_attachment": false
},
{
"id": "841",
"subject": "云安全中心云平台配置安全检查告警通知",
"from": "阿里云 <system@notice.aliyun.com>",
"to": "REDACTED_EMAIL",
"date": "Thu, 19 Feb 2026 11:41:54 +0800 (CST)",
"body": "",
"has_attachment": false
},
{
"id": "842",
"subject": "【到期预警】阿里云ECS即将到期提醒",
"from": "阿里云 <system@notice.aliyun.com>",
"to": "REDACTED_EMAIL",
"date": "Thu, 19 Feb 2026 12:04:08 +0800 (CST)",
"body": "",
"has_attachment": false
},
{
"id": "843",
"subject": "Action Required: Cloudinary Account Upgrade",
"from": "Cloudinary <support@cloudinary.com>",
"to": "REDACTED_EMAIL",
"date": "Thu, 19 Feb 2026 06:48:26 +0000",
"body": "",
"has_attachment": false
},
{
"id": "863",
"subject": "QQ邮箱祝你生日快乐",
"from": "\"QQ邮箱管理员\" <admin@qq.com>",
"to": "\"=?utf-8?B?MjA2NzA4OTQ1MQ==?=\" <REDACTED_EMAIL>",
"date": "Wed, 13 Oct 2021 09:45:34 +0800",
"body": "",
"has_attachment": false
},
{
"id": "865",
"subject": "յĺ룬ҵͨ",
"from": "\"腾讯企业微信\" <10000@qq.com>",
"to": "",
"date": "",
"body": "",
"has_attachment": false
},
{
"id": "883",
"subject": "Pruna-V is live: the fastest video model in its class",
"from": "Replicate <team@replicate.email>",
"to": "REDACTED_EMAIL",
"date": "Fri, 27 Feb 2026 05:04:03 +0000",
"body": "",
"has_attachment": false
},
{
"id": "884",
"subject": "AtCoder Beginner Contest 447 Announcement",
"from": "AtCoder System Mail <noreply@atcoder.jp>",
"to": "REDACTED_EMAIL",
"date": "Fri, 27 Feb 2026 13:32:40 +0000",
"body": "",
"has_attachment": false
},
{
"id": "885",
"subject": "Firecrawl Updates: Browser Sandbox, Claude Code Plugin & More",
"from": "Eric at Firecrawl <eric@firecrawl.dev>",
"to": "REDACTED_EMAIL",
"date": "Fri, 27 Feb 2026 17:06:08 +0000",
"body": "",
"has_attachment": false
}
],
"Gmail": []
}
}📬 **今日邮件摘要** (2026-02-28 08:33)
共 10 封未读邮件
📧 **QQ邮箱** (10 封未读):
1. 【阿里云】云安全中心周报 - 阿里云 <monitor@monitor.aliyun.com>
2. [GitHub] Your GitHub Enterprise trial has ended - GitHub <support@github.com>
3. 云安全中心云平台配置安全检查告警通知 - 阿里云 <system@notice.aliyun.com>
4. 【到期预警】阿里云ECS即将到期提醒 - 阿里云 <system@notice.aliyun.com>
5. Action Required: Cloudinary Account Upgrade - Cloudinary <support@cloudinary.com>
6. QQ邮箱祝你生日快乐 - "QQ邮箱管理员" <admin@qq.com>
7. յĺ룬ҵͨ - "腾讯企业微信" <10000@qq.com>
8. Pruna-V is live: the fastest video model in its cl - Replicate <team@replicate.email>
9. AtCoder Beginner Contest 447 Announcement - AtCoder System Mail <noreply@atcoder.jp>
10. Firecrawl Updates: Browser Sandbox, Claude Code Pl - Eric at Firecrawl <eric@firecrawl.dev>
{
"accounts": [
{
"email": "REDACTED_EMAIL",
"provider": "qq",
"name": "QQ邮箱",
"enabled": true
},
{
"email": "aaqwqaa68@gmail.com",
"provider": "gmail",
"name": "Gmail",
"enabled": true
}
],
"settings": {
"check_interval_hours": [8, 17],
"max_emails_per_check": 50,
"summary_model": "glm5",
"notification_channel": "telegram",
"notification_target": "-1003824568687",
"auto_archive_read": false,
"important_keywords": ["urgent", "重要", "紧急", "deadline", "截止"],
"exclude_labels": ["SPAM", "TRASH"]
}
}
{
"gmail": {
"name": "Gmail",
"imap_server": "imap.gmail.com",
"imap_port": 993,
"smtp_server": "smtp.gmail.com",
"smtp_port": 465,
"use_ssl": true,
"auth_type": "app_password",
"instructions": "需在 Google 账户设置中开启 IMAP,并生成应用专用密码"
},
"qq": {
"name": "QQ邮箱",
"imap_server": "imap.qq.com",
"imap_port": 993,
"smtp_server": "smtp.qq.com",
"smtp_port": 465,
"use_ssl": true,
"auth_type": "auth_code",
"instructions": "需在 QQ 邮箱设置中开启 IMAP 服务,获取授权码"
},
"outlook": {
"name": "Outlook",
"imap_server": "outlook.office365.com",
"imap_port": 993,
"smtp_server": "smtp.office365.com",
"smtp_port": 587,
"use_ssl": true,
"auth_type": "app_password",
"instructions": "需在 Microsoft 账户中生成应用密码"
},
"163": {
"name": "163邮箱",
"imap_server": "imap.163.com",
"imap_port": 993,
"smtp_server": "smtp.163.com",
"smtp_port": 465,
"use_ssl": true,
"auth_type": "auth_code",
"instructions": "需在 163 邮箱设置中开启 IMAP,获取授权码"
},
"126": {
"name": "126邮箱",
"imap_server": "imap.126.com",
"imap_port": 993,
"smtp_server": "smtp.126.com",
"smtp_port": 465,
"use_ssl": true,
"auth_type": "auth_code",
"instructions": "需在 126 邮箱设置中开启 IMAP,获取授权码"
}
}
#!/usr/bin/env python3
"""
Check Email - 检查邮件并生成智能摘要
定时任务调用此脚本
"""
import sys
import json
from pathlib import Path
from datetime import datetime
from typing import List, Dict
# 添加脚本目录到路径
sys.path.insert(0, str(Path(__file__).parent))
from email_client import get_all_clients, EmailClient
def format_email_summary(emails: List[Dict], account_name: str) -> str:
"""格式化单个账号的邮件摘要"""
if not emails:
return ""
summary = f"\n📧 **{account_name}** ({len(emails)} 封未读):\n"
for i, email in enumerate(emails[:10], 1): # 最多显示 10 封
subject = email['subject'][:50] # 截断主题
from_ = email['from']
# 检查是否包含重要关键词
important_keywords = ['urgent', '重要', '紧急', 'deadline', '截止', 'asap', 'immediately']
is_important = any(kw in subject.lower() or kw in email['body'].lower()
for kw in important_keywords)
prefix = "🔴" if is_important else " "
summary += f"{prefix} {i}. {subject} - {from_}\n"
return summary
def generate_ai_summary(all_emails: Dict[str, List[Dict]]) -> str:
"""使用 AI 生成智能摘要(简化版,可后续接入 LLM)"""
total_count = sum(len(emails) for emails in all_emails.values())
if total_count == 0:
return "📭 暂无新邮件"
summary = f"📬 **今日邮件摘要** ({datetime.now().strftime('%Y-%m-%d %H:%M')})\n"
summary += f"共 {total_count} 封未读邮件\n"
# 按账号分组
for account_name, emails in all_emails.items():
account_summary = format_email_summary(emails, account_name)
summary += account_summary
# 检查重要邮件
important_emails = []
for account_name, emails in all_emails.items():
for email in emails:
important_keywords = ['urgent', '重要', '紧急', 'deadline', '截止']
if any(kw in email['subject'].lower() or kw in email['body'].lower()
for kw in important_keywords):
important_emails.append((account_name, email))
if important_emails:
summary += "\n⚠️ **重要邮件提醒**:\n"
for account_name, email in important_emails:
summary += f" • [{account_name}] {email['subject']}\n"
return summary
def save_email_cache(all_emails: Dict[str, List[Dict]]):
"""保存邮件缓存(用于后续回复等操作)"""
cache_path = Path(__file__).parent.parent / 'cache' / 'emails.json'
cache_path.parent.mkdir(exist_ok=True)
cache_data = {
'timestamp': datetime.now().isoformat(),
'emails': all_emails
}
with open(cache_path, 'w', encoding='utf-8') as f:
json.dump(cache_data, f, ensure_ascii=False, indent=2)
def check_all_emails() -> str:
"""检查所有邮箱并返回摘要"""
clients = get_all_clients()
if not clients:
return "⚠️ 未配置任何邮箱账号,请先添加账号"
all_emails = {}
for client in clients:
try:
# 直接获取未读邮件(跳过 test_connection 节省连接时间)
emails = client.fetch_unread(limit=10)
account_name = client.config.get('name', client.email_address)
all_emails[account_name] = emails
except Exception as e:
print(f"检查邮箱 {client.email_address} 失败: {e}")
all_emails[client.config.get('name', client.email_address)] = []
# 保存缓存
save_email_cache(all_emails)
# 生成摘要
summary = generate_ai_summary(all_emails)
return summary
def main():
"""主函数"""
print("开始检查邮件...")
try:
summary = check_all_emails()
print(summary)
# 输出到文件(供 cron 任务读取)
output_path = Path(__file__).parent.parent / 'cache' / 'last_summary.txt'
output_path.parent.mkdir(exist_ok=True)
with open(output_path, 'w', encoding='utf-8') as f:
f.write(summary)
print(f"\n摘要已保存到: {output_path}")
except Exception as e:
print(f"错误: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == '__main__':
main()
#!/usr/bin/env python3
"""
Email Client - IMAP/SMTP 通用邮件客户端
支持 Gmail、QQ、Outlook 等邮箱
"""
import imaplib
import smtplib
import email
import socket
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.header import decode_header
import json
import subprocess
from pathlib import Path
from datetime import datetime
from typing import List, Dict, Optional, Tuple
# 默认超时设置(秒)
DEFAULT_SOCKET_TIMEOUT = 30
DEFAULT_IMAP_TIMEOUT = 60
MAX_RETRIES = 5
class EmailClient:
"""通用邮件客户端"""
def __init__(self, account_config: Dict, provider_config: Dict):
self.config = account_config
self.provider = provider_config
self.email_address = account_config['email']
self.provider_name = account_config.get('provider', 'unknown')
def _get_password(self) -> str:
"""从 pass 获取密码/授权码"""
# 尝试不同的 pass 路径格式
pass_paths = [
f"email/{self.provider_name}/{self.email_address}",
f"email/{self.provider_name}/{self.email_address}-app-pass",
f"email/{self.provider_name}/{self.email_address}-auth-code",
]
for path in pass_paths:
try:
result = subprocess.run(
['pass', 'show', path],
capture_output=True,
text=True,
timeout=10
)
if result.returncode == 0:
return result.stdout.strip().split('\n')[0]
except Exception:
continue
raise ValueError(f"未找到邮箱凭据: {self.email_address}")
def connect_imap(self, retries: int = MAX_RETRIES) -> imaplib.IMAP4_SSL:
"""连接 IMAP 服务器(带重试 + 超时)
QQ 邮箱从香港直连有时不稳定,增加重试和超时设置
"""
import ssl
import time
password = self._get_password()
# QQ 邮箱需要更长超时(海外连接)
timeout = DEFAULT_IMAP_TIMEOUT if self.provider_name == 'qq' else DEFAULT_SOCKET_TIMEOUT
# 设置全局 socket 超时
socket.setdefaulttimeout(timeout)
for attempt in range(retries):
try:
ctx = ssl.create_default_context()
# 创建带超时的 IMAP 连接
mail = imaplib.IMAP4_SSL(
self.provider['imap_server'],
self.provider['imap_port'],
ssl_context=ctx,
timeout=timeout
)
# 登录
mail.login(self.email_address, password)
print(f" ✅ {self.email_address} IMAP 连接成功 (attempt {attempt + 1})")
return mail
except (ssl.SSLEOFError, ConnectionResetError, OSError, socket.timeout, TimeoutError) as e:
wait_time = 2 ** (attempt + 1) # 指数退避: 2s, 4s, 8s, 16s, 32s
print(f" ⚠️ {self.email_address} IMAP 连接失败 (attempt {attempt + 1}/{retries}): {e}")
if attempt < retries - 1:
print(f" 等待 {wait_time}s 后重试...")
time.sleep(wait_time)
continue
raise
def connect_smtp(self, retries: int = MAX_RETRIES):
"""连接 SMTP 服务器(带重试 + 超时)"""
import ssl
import time
password = self._get_password()
port = self.provider['smtp_port']
# QQ 邮箱需要更长超时
timeout = DEFAULT_IMAP_TIMEOUT if self.provider_name == 'qq' else DEFAULT_SOCKET_TIMEOUT
socket.setdefaulttimeout(timeout)
for attempt in range(retries):
try:
ctx = ssl.create_default_context()
if port == 465:
smtp = smtplib.SMTP_SSL(self.provider['smtp_server'], port, context=ctx, timeout=timeout)
else:
smtp = smtplib.SMTP(self.provider['smtp_server'], port, timeout=timeout)
smtp.starttls(context=ctx)
smtp.login(self.email_address, password)
print(f" ✅ {self.email_address} SMTP 连接成功")
return smtp
except (ssl.SSLEOFError, ConnectionResetError, OSError, socket.timeout, TimeoutError) as e:
wait_time = 2 ** (attempt + 1)
print(f" ⚠️ {self.email_address} SMTP 连接失败 (attempt {attempt + 1}/{retries}): {e}")
if attempt < retries - 1:
print(f" 等待 {wait_time}s 后重试...")
time.sleep(wait_time)
continue
raise
def fetch_unread(self, limit: int = 20, headers_only: bool = True) -> List[Dict]:
"""获取未读邮件
Args:
limit: 最多获取多少封
headers_only: True=只取标题/发件人(快), False=取完整内容(慢)
"""
mail = self.connect_imap()
mail.select('INBOX')
# 搜索未读邮件
status, messages = mail.search(None, 'UNSEEN')
if status != 'OK':
mail.close()
mail.logout()
return []
email_ids = messages[0].split()
email_ids = email_ids[-limit:] # 只取最新的
emails = []
for email_id in email_ids:
try:
if headers_only:
# 只获取头部信息,速度快 10x+
status, msg_data = mail.fetch(email_id, '(BODY[HEADER.FIELDS (FROM TO SUBJECT DATE)])')
if status == 'OK' and msg_data[0] is not None:
header_bytes = msg_data[0][1] if isinstance(msg_data[0], tuple) else b''
msg = email.message_from_bytes(header_bytes)
email_data = self._parse_email(msg, email_id.decode())
email_data['body'] = '' # 头部模式无正文
emails.append(email_data)
else:
status, msg_data = mail.fetch(email_id, '(RFC822)')
if status == 'OK':
msg = email.message_from_bytes(msg_data[0][1])
email_data = self._parse_email(msg, email_id.decode())
emails.append(email_data)
except Exception as e:
print(f" 跳过邮件 {email_id}: {e}")
continue
mail.close()
mail.logout()
return emails
def fetch_recent(self, hours: int = 24, limit: int = 50) -> List[Dict]:
"""获取最近 N 小时的邮件"""
mail = self.connect_imap()
mail.select('INBOX')
# 搜索最近的邮件
since_date = datetime.now().strftime('%d-%b-%Y')
status, messages = mail.search(None, f'(SINCE {since_date})')
if status != 'OK':
mail.close()
mail.logout()
return []
email_ids = messages[0].split()
email_ids = email_ids[-limit:] # 限制数量
emails = []
for email_id in reversed(email_ids): # 最新的在前
status, msg_data = mail.fetch(email_id, '(RFC822)')
if status == 'OK':
msg = email.message_from_bytes(msg_data[0][1])
email_data = self._parse_email(msg, email_id.decode())
emails.append(email_data)
mail.close()
mail.logout()
return emails
def _parse_email(self, msg: email.message.Message, email_id: str) -> Dict:
"""解析邮件内容"""
# 解码主题
subject = ''
if msg['Subject']:
decoded_parts = decode_header(msg['Subject'])
subject_parts = []
for part, charset in decoded_parts:
if isinstance(part, bytes):
try:
subject_parts.append(part.decode(charset or 'utf-8', errors='ignore'))
except (LookupError, UnicodeDecodeError):
subject_parts.append(part.decode('utf-8', errors='ignore'))
else:
subject_parts.append(part)
subject = ''.join(subject_parts)
# 解码发件人
from_ = msg['From'] or ''
if msg['From']:
decoded_parts = decode_header(msg['From'])
from_parts = []
for part, charset in decoded_parts:
if isinstance(part, bytes):
try:
from_parts.append(part.decode(charset or 'utf-8', errors='ignore'))
except (LookupError, UnicodeDecodeError):
from_parts.append(part.decode('utf-8', errors='ignore'))
else:
from_parts.append(part)
from_ = ''.join(from_parts)
# 提取正文
body = self._extract_body(msg)
return {
'id': email_id,
'subject': subject,
'from': from_,
'to': msg['To'] or '',
'date': msg['Date'] or '',
'body': body[:1000], # 限制正文长度
'has_attachment': self._has_attachment(msg)
}
def _extract_body(self, msg: email.message.Message) -> str:
"""提取邮件正文"""
body = ''
if msg.is_multipart():
for part in msg.walk():
content_type = part.get_content_type()
if content_type == 'text/plain':
try:
payload = part.get_payload(decode=True)
charset = part.get_content_charset() or 'utf-8'
body = payload.decode(charset, errors='ignore')
break
except Exception:
continue
else:
try:
payload = msg.get_payload(decode=True)
charset = msg.get_content_charset() or 'utf-8'
body = payload.decode(charset, errors='ignore')
except Exception:
pass
return body
def _has_attachment(self, msg: email.message.Message) -> bool:
"""检查是否有附件"""
if msg.is_multipart():
for part in msg.walk():
if part.get_content_disposition() == 'attachment':
return True
return False
def send_email(
self,
to: str,
subject: str,
body: str,
is_html: bool = False,
cc: List[str] = None,
reply_to: str = None
) -> bool:
"""发送邮件"""
try:
smtp = self.connect_smtp()
if is_html:
msg = MIMEMultipart('alternative')
msg.attach(MIMEText(body, 'html', 'utf-8'))
else:
msg = MIMEText(body, 'plain', 'utf-8')
msg['From'] = self.email_address
msg['To'] = to
msg['Subject'] = subject
if cc:
msg['Cc'] = ', '.join(cc)
if reply_to:
msg['Reply-To'] = reply_to
recipients = [to]
if cc:
recipients.extend(cc)
smtp.sendmail(self.email_address, recipients, msg.as_string())
smtp.quit()
return True
except Exception as e:
print(f"发送邮件失败: {e}")
return False
def test_connection(self) -> Tuple[bool, str]:
"""测试邮箱连接"""
try:
# 测试 IMAP
mail = self.connect_imap()
mail.select('INBOX')
mail.close()
mail.logout()
# 测试 SMTP
smtp = self.connect_smtp()
smtp.quit()
return True, "连接成功"
except Exception as e:
return False, str(e)
def load_accounts() -> Tuple[List[Dict], Dict]:
"""加载账号和供应商配置"""
base_path = Path(__file__).parent.parent / 'config'
with open(base_path / 'accounts.json', 'r', encoding='utf-8') as f:
accounts_config = json.load(f)
with open(base_path / 'providers.json', 'r', encoding='utf-8') as f:
providers = json.load(f)
return accounts_config.get('accounts', []), providers
def get_all_clients() -> List[EmailClient]:
"""获取所有已配置的邮件客户端"""
accounts, providers = load_accounts()
clients = []
for account in accounts:
if not account.get('enabled', True):
continue
provider_name = account.get('provider')
if provider_name not in providers:
print(f"未知的邮件供应商: {provider_name}")
continue
client = EmailClient(account, providers[provider_name])
clients.append(client)
return clients
if __name__ == '__main__':
# 测试代码
clients = get_all_clients()
for client in clients:
print(f"\n测试账号: {client.email_address}")
success, message = client.test_connection()
print(f"结果: {message}")
if success:
emails = client.fetch_unread(limit=5)
print(f"未读邮件: {len(emails)} 封")
for e in emails:
print(f" - {e['subject']} (from: {e['from']})")
#!/usr/bin/env python3
"""
Email Manager CLI - 邮箱管理命令行工具
"""
import sys
import json
from pathlib import Path
from datetime import datetime
sys.path.insert(0, str(Path(__file__).parent))
from email_client import get_all_clients, EmailClient, load_accounts
def cmd_list_accounts():
"""列出所有已配置的邮箱账号"""
accounts, providers = load_accounts()
if not accounts:
print("📭 暂无配置的邮箱账号")
print("\n添加账号步骤:")
print("1. 使用 pass 存储邮箱凭据:")
print(" pass insert email/gmail/your-email@gmail.com")
print(" pass insert email/gmail/your-email@gmail.com-app-pass")
print("2. 编辑 config/accounts.json 添加账号配置")
return
print("📧 已配置的邮箱账号:")
for i, account in enumerate(accounts, 1):
status = "✅" if account.get('enabled', True) else "❌"
print(f"{i}. {status} {account.get('name', account['email'])}")
print(f" 类型: {account.get('provider', 'unknown')}")
print(f" 邮箱: {account['email']}")
def cmd_test_connection():
"""测试所有邮箱连接"""
clients = get_all_clients()
if not clients:
print("⚠️ 未配置任何邮箱账号")
return
print("🔌 测试邮箱连接...\n")
all_success = True
for client in clients:
print(f"测试: {client.config.get('name', client.email_address)}")
success, message = client.test_connection()
if success:
print(f" ✅ {message}")
else:
print(f" ❌ {message}")
all_success = False
if all_success:
print("\n🎉 所有邮箱连接正常")
else:
print("\n⚠️ 部分邮箱连接失败,请检查配置")
def cmd_check_emails():
"""检查邮件"""
from check_email import check_all_emails
print("📬 开始检查邮件...\n")
summary = check_all_emails()
print(summary)
def cmd_add_account():
"""添加新邮箱账号(交互式)"""
print("📧 添加新邮箱账号\n")
# 选择邮箱类型
print("支持的邮箱类型:")
print("1. Gmail")
print("2. QQ邮箱")
print("3. Outlook")
print("4. 163邮箱")
print("5. 126邮箱")
choice = input("\n选择邮箱类型 (1-5): ").strip()
providers_map = {
'1': 'gmail',
'2': 'qq',
'3': 'outlook',
'4': '163',
'5': '126'
}
provider = providers_map.get(choice)
if not provider:
print("❌ 无效的选择")
return
# 获取邮箱地址
email_address = input("邮箱地址: ").strip()
if not email_address:
print("❌ 邮箱地址不能为空")
return
# 获取账号名称
account_name = input("账号名称 (可选,直接回车使用邮箱地址): ").strip()
if not account_name:
account_name = email_address
print(f"\n📝 配置信息:")
print(f" 类型: {provider}")
print(f" 邮箱: {email_address}")
print(f" 名称: {account_name}")
# 提示存储凭据
print(f"\n🔐 请先存储邮箱凭据:")
print(f" pass insert email/{provider}/{email_address}")
if provider in ['gmail', 'outlook']:
print(f" pass insert email/{provider}/{email_address}-app-pass")
else:
print(f" pass insert email/{provider}/{email_address}-auth-code")
confirm = input("\n已完成凭据存储? (y/n): ").strip().lower()
if confirm != 'y':
print("❌ 取消添加")
return
# 添加到配置文件
accounts_path = Path(__file__).parent.parent / 'config' / 'accounts.json'
with open(accounts_path, 'r', encoding='utf-8') as f:
config = json.load(f)
# 加载供应商配置
providers_path = Path(__file__).parent.parent / 'config' / 'providers.json'
with open(providers_path, 'r', encoding='utf-8') as f:
providers = json.load(f)
provider_config = providers[provider]
new_account = {
"name": account_name,
"email": email_address,
"provider": provider,
"imap_server": provider_config['imap_server'],
"imap_port": provider_config['imap_port'],
"smtp_server": provider_config['smtp_server'],
"smtp_port": provider_config['smtp_port'],
"enabled": True
}
config['accounts'].append(new_account)
with open(accounts_path, 'w', encoding='utf-8') as f:
json.dump(config, f, ensure_ascii=False, indent=2)
print(f"\n✅ 账号已添加: {account_name}")
print(" 运行 'python3 manage.py test' 测试连接")
def cmd_show_help():
"""显示帮助"""
print("Email Manager - 邮箱管理工具\n")
print("用法: python3 manage.py <command>\n")
print("命令:")
print(" list - 列出所有已配置的邮箱账号")
print(" test - 测试所有邮箱连接")
print(" check - 检查邮件并生成摘要")
print(" add - 添加新邮箱账号(交互式)")
print(" help - 显示此帮助")
def main():
if len(sys.argv) < 2:
cmd_show_help()
return
command = sys.argv[1].lower()
commands = {
'list': cmd_list_accounts,
'test': cmd_test_connection,
'check': cmd_check_emails,
'add': cmd_add_account,
'help': cmd_show_help,
}
if command in commands:
commands[command]()
else:
print(f"❌ 未知命令: {command}")
cmd_show_help()
if __name__ == '__main__':
main()
#!/usr/bin/env python3
"""
Reply Draft - 生成回复草稿
基于收到的邮件和用户需求,生成礼貌的回复草稿
"""
import sys
import json
from pathlib import Path
from datetime import datetime
from typing import Dict, Optional
# 添加脚本目录到路径
sys.path.insert(0, str(Path(__file__).parent))
from email_client import get_all_clients, EmailClient
def load_email_cache() -> Optional[Dict]:
"""加载邮件缓存"""
cache_path = Path(__file__).parent.parent / 'cache' / 'emails.json'
if not cache_path.exists():
return None
with open(cache_path, 'r', encoding='utf-8') as f:
return json.load(f)
def get_email_by_index(account_name: str, index: int) -> Optional[Dict]:
"""根据索引获取邮件"""
cache = load_email_cache()
if not cache:
return None
emails = cache.get('emails', {}).get(account_name, [])
if 0 < index <= len(emails):
return emails[index - 1]
return None
def generate_reply_draft(
original_email: Dict,
user_request: str,
sender_name: str = "Daniel"
) -> Dict:
"""生成回复草稿(简化版,可后续接入 LLM)"""
# 提取收件人信息
to_email = original_email.get('from', '')
to_name = to_email.split('<')[0].strip() if '<' in to_email else to_email
# 生成主题
original_subject = original_email.get('subject', '')
reply_subject = f"Re: {original_subject}" if not original_subject.startswith('Re:') else original_subject
# 基于用户请求生成回复内容
# 这里是简化版,实际应该调用 LLM
body = f"""{to_name},你好!
{user_request}
如有其他问题,随时联系。
祝好,
{sender_name}
"""
return {
'to': to_email,
'subject': reply_subject,
'body': body.strip(),
'original_email': original_email,
'created_at': datetime.now().isoformat()
}
def format_draft_for_review(draft: Dict) -> str:
"""格式化草稿供用户审核"""
output = "📝 **回复草稿**\n\n"
output += f"收件人: {draft['to']}\n"
output += f"主题: {draft['subject']}\n\n"
output += "内容:\n"
output += "---\n"
output += draft['body']
output += "\n---\n\n"
output += "确认发送吗?(回复\"确认\"发送,\"修改\"调整,\"取消\"放弃)"
return output
def save_draft(draft: Dict) -> str:
"""保存草稿到文件"""
drafts_path = Path(__file__).parent.parent / 'cache' / 'drafts.json'
drafts_path.parent.mkdir(exist_ok=True)
# 加载现有草稿
if drafts_path.exists():
with open(drafts_path, 'r', encoding='utf-8') as f:
drafts = json.load(f)
else:
drafts = {'drafts': []}
# 添加新草稿
draft_id = f"draft_{datetime.now().strftime('%Y%m%d%H%M%S')}"
draft['id'] = draft_id
drafts['drafts'].append(draft)
# 保存
with open(drafts_path, 'w', encoding='utf-8') as f:
json.dump(drafts, f, ensure_ascii=False, indent=2)
return draft_id
def main():
"""测试代码"""
# 模拟生成草稿
test_email = {
'from': '张三 <zhangsan@example.com>',
'subject': '项目进度更新',
'body': '你好,请问项目进度如何?'
}
draft = generate_reply_draft(
test_email,
'项目进度一切正常,按计划推进中。'
)
print(format_draft_for_review(draft))
# 保存草稿
draft_id = save_draft(draft)
print(f"\n草稿已保存,ID: {draft_id}")
if __name__ == '__main__':
main()
#!/usr/bin/env python3
"""
Send Email - 发送邮件(需用户确认)
读取已保存的草稿,经用户确认后发送
"""
import sys
import json
from pathlib import Path
from datetime import datetime
from typing import Optional, Dict
# 添加脚本目录到路径
sys.path.insert(0, str(Path(__file__).parent))
from email_client import get_all_clients, EmailClient
def load_draft(draft_id: str) -> Optional[Dict]:
"""加载草稿"""
drafts_path = Path(__file__).parent.parent / 'cache' / 'drafts.json'
if not drafts_path.exists():
return None
with open(drafts_path, 'r', encoding='utf-8') as f:
drafts = json.load(f)
for draft in drafts.get('drafts', []):
if draft.get('id') == draft_id:
return draft
return None
def send_draft(draft: Dict, account_email: str = None) -> tuple[bool, str]:
"""发送草稿"""
clients = get_all_clients()
if not clients:
return False, "未配置任何邮箱账号"
# 选择发送账号
client = None
if account_email:
for c in clients:
if c.email_address == account_email:
client = c
break
if not client:
client = clients[0] # 默认使用第一个账号
# 发送邮件
success = client.send_email(
to=draft['to'],
subject=draft['subject'],
body=draft['body'],
is_html=False
)
if success:
# 标记草稿为已发送
mark_draft_sent(draft['id'])
return True, f"邮件已发送到 {draft['to']}"
else:
return False, "发送失败"
def mark_draft_sent(draft_id: str):
"""标记草稿为已发送"""
drafts_path = Path(__file__).parent.parent / 'cache' / 'drafts.json'
if not drafts_path.exists():
return
with open(drafts_path, 'r', encoding='utf-8') as f:
drafts = json.load(f)
for draft in drafts.get('drafts', []):
if draft.get('id') == draft_id:
draft['sent'] = True
draft['sent_at'] = datetime.now().isoformat()
break
with open(drafts_path, 'w', encoding='utf-8') as f:
json.dump(drafts, f, ensure_ascii=False, indent=2)
def list_pending_drafts() -> list:
"""列出待发送的草稿"""
drafts_path = Path(__file__).parent.parent / 'cache' / 'drafts.json'
if not drafts_path.exists():
return []
with open(drafts_path, 'r', encoding='utf-8') as f:
drafts = json.load(f)
pending = [
draft for draft in drafts.get('drafts', [])
if not draft.get('sent', False)
]
return pending
def main():
"""测试代码"""
print("待发送的草稿:")
drafts = list_pending_drafts()
if not drafts:
print(" 无待发送草稿")
return
for draft in drafts:
print(f"\nID: {draft['id']}")
print(f"收件人: {draft['to']}")
print(f"主题: {draft['subject']}")
print(f"创建时间: {draft['created_at']}")
if __name__ == '__main__':
main()
Related skills
FAQ
Which mailboxes does it support?
Any IMAP mailbox; the docs list Gmail, QQ, Outlook, 163, and 126 with their IMAP servers and port 993.
Can it send email on its own?
No. Sending is a confirmation-required action and deleting mail is blocked outright.