mailcode 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mailcode-0.1.0/LICENSE +21 -0
- mailcode-0.1.0/PKG-INFO +8 -0
- mailcode-0.1.0/README.md +151 -0
- mailcode-0.1.0/mailcode/__init__.py +1 -0
- mailcode-0.1.0/mailcode/channels/__init__.py +3 -0
- mailcode-0.1.0/mailcode/channels/email_channel.py +144 -0
- mailcode-0.1.0/mailcode/cli.py +360 -0
- mailcode-0.1.0/mailcode/config.py +248 -0
- mailcode-0.1.0/mailcode/health.py +128 -0
- mailcode-0.1.0/mailcode/provider_presets.py +51 -0
- mailcode-0.1.0/mailcode/relay/__init__.py +3 -0
- mailcode-0.1.0/mailcode/relay/conversation_handler.py +576 -0
- mailcode-0.1.0/mailcode/relay/email_listener.py +556 -0
- mailcode-0.1.0/mailcode/relay/security.py +132 -0
- mailcode-0.1.0/mailcode/relay/stateless_handler.py +101 -0
- mailcode-0.1.0/mailcode/resources/default.json +36 -0
- mailcode-0.1.0/mailcode/server.py +40 -0
- mailcode-0.1.0/mailcode/session_cli.py +149 -0
- mailcode-0.1.0/mailcode/utils/__init__.py +3 -0
- mailcode-0.1.0/mailcode/utils/logging.py +48 -0
- mailcode-0.1.0/mailcode.egg-info/PKG-INFO +8 -0
- mailcode-0.1.0/mailcode.egg-info/SOURCES.txt +25 -0
- mailcode-0.1.0/mailcode.egg-info/dependency_links.txt +1 -0
- mailcode-0.1.0/mailcode.egg-info/entry_points.txt +2 -0
- mailcode-0.1.0/mailcode.egg-info/top_level.txt +1 -0
- mailcode-0.1.0/pyproject.toml +22 -0
- mailcode-0.1.0/setup.cfg +4 -0
mailcode-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 zsdfbb
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
mailcode-0.1.0/PKG-INFO
ADDED
mailcode-0.1.0/README.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# MailCode
|
|
2
|
+
|
|
3
|
+
Python 邮件连接器,通过邮件远程操控 Claude Code。
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
收件箱 ──> IMAP 监听器 ──> claude -p 子进程 ──> SMTP 邮件通知
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## 设计理念
|
|
10
|
+
|
|
11
|
+
MailCode 的核心理念是**轻量化的人与 Coding Agent 直连**。
|
|
12
|
+
|
|
13
|
+
市面上的 AI 工具链往往依赖飞书、钉钉等重型协作平台——建机器人、配 Webhook、在对话框里和 Agent 来回聊天。MailCode 反其道而行:直接用邮件,因为你本来就有一个邮箱。
|
|
14
|
+
|
|
15
|
+
**人与 Agent 直连,而不是和机器人对话。** 回复邮件就是下达指令,收件箱就是控制台,不需要打开任何第三方应用。
|
|
16
|
+
|
|
17
|
+
**轻量异步。** 不需要常驻复杂服务,不需要数据库,不需要消息队列。一个 Python 脚本 + 邮件协议,跑在任何能联网的机器上。Agent 在后台慢慢跑,你做别的事,完事了邮件通知你。
|
|
18
|
+
|
|
19
|
+
MailCode 不做大而全的平台,只做一件事:**让你用最习惯的方式(邮件)和 Agent 对话。**
|
|
20
|
+
|
|
21
|
+
## 安装
|
|
22
|
+
|
|
23
|
+
### 系统依赖
|
|
24
|
+
|
|
25
|
+
- **python3**(≥3.9)
|
|
26
|
+
- **Claude Code**(`claude` 命令需在 `PATH` 中)
|
|
27
|
+
|
|
28
|
+
Python 零第三方依赖,全部使用标准库(`imaplib`、`smtplib`、`email`、`subprocess`、`json`、`secrets` 等)。
|
|
29
|
+
|
|
30
|
+
### pip 安装
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install mailcode
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 源码安装
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
git clone <repo-url> && cd MailCode
|
|
40
|
+
bash install.sh
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`install.sh` 自动完成:安装 mailcode 包、初始化配置、创建 `~/.mailcode` 软链接、自动添加 PATH。
|
|
44
|
+
|
|
45
|
+
从本地 wheel 安装:`bash install.sh --local dist/mailcode-*.whl`
|
|
46
|
+
|
|
47
|
+
## 配置
|
|
48
|
+
|
|
49
|
+
编辑 `~/.config/mailcode/config.json`,必填字段:
|
|
50
|
+
|
|
51
|
+
```jsonc
|
|
52
|
+
{
|
|
53
|
+
"mailcode_bot": {
|
|
54
|
+
"email": "your@qq.com", // ← 你的邮箱,MailCode 监听此邮箱收件箱
|
|
55
|
+
"password": "邮箱授权码" // ← 不是登录密码!
|
|
56
|
+
},
|
|
57
|
+
"security": {
|
|
58
|
+
"allowed_senders": ["your@qq.com"] // 哪些邮箱能发命令?填你自己的
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
SMTP 和 IMAP 配置由系统根据邮箱域名自动识别。支持:QQ 邮箱、163/126 邮箱、Gmail、Outlook/Hotmail。
|
|
64
|
+
|
|
65
|
+
如需手动覆盖 SMTP/IMAP(如自建邮箱),可添加 `smtp` / `imap` 段,手动设置的值会覆盖自动识别结果。
|
|
66
|
+
|
|
67
|
+
> 授权码获取:QQ 邮箱 → 设置 → 账户 → POP3/IMAP → 生成授权码;Gmail → Google 账户 → 安全性 → 应用密码。
|
|
68
|
+
|
|
69
|
+
## 使用
|
|
70
|
+
|
|
71
|
+
### CLI 概览
|
|
72
|
+
|
|
73
|
+
| 子命令 | 用途 |
|
|
74
|
+
|--------|------|
|
|
75
|
+
| `mailcode serve` | 启动 IMAP 监听中继 |
|
|
76
|
+
| `mailcode config <动作>` | 配置管理(`show` / `init` / `init-test` / `path` / `validate`)|
|
|
77
|
+
| `mailcode health` | 邮件连通性检查(SMTP/IMAP)|
|
|
78
|
+
| `mailcode session <动作>` | 会话管理(`list` / `show` / `delete` / `cleanup`)|
|
|
79
|
+
| `mailcode --version` | 显示版本号 |
|
|
80
|
+
|
|
81
|
+
### 启动中继
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# 前台运行(IDLE 长连接模式)
|
|
85
|
+
mailcode serve --idle
|
|
86
|
+
|
|
87
|
+
# 干跑模式(仅打印邮件,不调用 claude)
|
|
88
|
+
mailcode serve --idle --dry-run
|
|
89
|
+
|
|
90
|
+
# 单次轮询后退出
|
|
91
|
+
mailcode serve --once
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
查看日志:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
tail -f ~/.config/mailcode/relay.log
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 配置管理
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
mailcode config show # 查看当前配置(密码脱敏)
|
|
104
|
+
mailcode config path # 显示配置文件路径
|
|
105
|
+
mailcode config init # 初始化配置(已存在则跳过)
|
|
106
|
+
mailcode config init --force # 强制重新生成
|
|
107
|
+
mailcode config validate # 校验配置完整性
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### 会话管理
|
|
111
|
+
|
|
112
|
+
MailCode 默认按邮件主题维护多轮对话; 如需单次回复模式请设 `session.enabled = false`。
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
mailcode session list # 列出所有 session
|
|
116
|
+
mailcode session show <session_id> # 查看单个 session 详情
|
|
117
|
+
mailcode session delete <session_id> # 删除 session
|
|
118
|
+
mailcode session cleanup # 按 TTL 清理过期 session
|
|
119
|
+
mailcode session cleanup --dry-run # 仅预览,不实际删除
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### 工作目录 (cwd 指令)
|
|
123
|
+
|
|
124
|
+
在邮件正文**第一行**写 `cwd: <path>`,Claude 子进程会在该目录启动——适合「让 Claude 操作指定项目」。Session 模式下 cwd **粘性**:同一 session 内的后续邮件会沿用该目录,直到新邮件重新指定。
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
cwd: ~/Projects/my-app
|
|
128
|
+
帮我看看 src/auth.py 里那段 JWT 校验逻辑
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**路径解析规则**:
|
|
132
|
+
|
|
133
|
+
- `~` / `~/foo` 走用户目录展开
|
|
134
|
+
- 相对路径(`./foo`、`foo`)以 `Path.cwd()` 为基准
|
|
135
|
+
- 路径必须存在且是目录(`is_dir()` 校验),否则忽略并回退默认(`$HOME`)
|
|
136
|
+
- 写法不区分大小写,`Cwd:` / `CWD:` 等价
|
|
137
|
+
|
|
138
|
+
**两种模式差异**:
|
|
139
|
+
|
|
140
|
+
- **Session 模式**(`session.enabled = true` 默认):cwd 粘性,整个 session 沿用;`mailcode session show <id>` 可查当前 cwd
|
|
141
|
+
- **单次回复模式**(`session.enabled = false`):cwd 不粘性,每封邮件独立解析
|
|
142
|
+
|
|
143
|
+
cwd 行会在调用 Claude 前从 body 中剥离,不会污染 prompt。
|
|
144
|
+
|
|
145
|
+
### 健康检查
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
mailcode health # 检查 SMTP/IMAP 配置与连通性
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
检查项:SMTP 连接 / 登录 / 发信、IMAP 连接 / 登录 / 收件箱。
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import smtplib
|
|
2
|
+
import logging
|
|
3
|
+
from email.mime.text import MIMEText
|
|
4
|
+
from email.mime.multipart import MIMEMultipart
|
|
5
|
+
from email.header import Header
|
|
6
|
+
from typing import Optional
|
|
7
|
+
import email.utils
|
|
8
|
+
|
|
9
|
+
from mailcode.config import get_smtp_config, get_email_config
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EmailChannel:
|
|
15
|
+
def __init__(self, smtp_config=None, email_config=None):
|
|
16
|
+
self.smtp_config = smtp_config or get_smtp_config()
|
|
17
|
+
self.email_config = email_config or get_email_config()
|
|
18
|
+
self.smtp_user = self.smtp_config.get("user", "")
|
|
19
|
+
self.smtp_pass = self.smtp_config.get("pass", "")
|
|
20
|
+
|
|
21
|
+
def _create_connection(self) -> bool:
|
|
22
|
+
host = self.smtp_config.get("host", "smtp.gmail.com")
|
|
23
|
+
port = self.smtp_config.get("port", 587)
|
|
24
|
+
secure = self.smtp_config.get("secure", False)
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
if secure:
|
|
28
|
+
self._server = smtplib.SMTP_SSL(host, port, timeout=15)
|
|
29
|
+
else:
|
|
30
|
+
self._server = smtplib.SMTP(host, port, timeout=15)
|
|
31
|
+
self._server.starttls()
|
|
32
|
+
return True
|
|
33
|
+
except Exception as e:
|
|
34
|
+
logger.error("SMTP 连接失败: %s", e)
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
def send(
|
|
38
|
+
self,
|
|
39
|
+
to_email: Optional[str] = None,
|
|
40
|
+
subject: str = "",
|
|
41
|
+
body: str = "",
|
|
42
|
+
token: Optional[str] = None
|
|
43
|
+
) -> bool:
|
|
44
|
+
from_email = self.email_config.get("from", self.smtp_user)
|
|
45
|
+
from_name = self.email_config.get("from_name", "Mailcode Remote")
|
|
46
|
+
to_email = to_email or ""
|
|
47
|
+
|
|
48
|
+
if not from_email or not self.smtp_user:
|
|
49
|
+
raise ValueError("SMTP 配置不完整,请检查配置文件中的 mailcode_bot.email")
|
|
50
|
+
|
|
51
|
+
msg = MIMEMultipart()
|
|
52
|
+
msg["From"] = f"{from_name} <{from_email}>"
|
|
53
|
+
msg["To"] = to_email
|
|
54
|
+
msg["Subject"] = Header(subject, "utf-8")
|
|
55
|
+
|
|
56
|
+
if token:
|
|
57
|
+
msg["X-MailCode-Remote-Token"] = token
|
|
58
|
+
|
|
59
|
+
msg.attach(MIMEText(body, "plain", "utf-8"))
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
if not self._create_connection():
|
|
63
|
+
return False
|
|
64
|
+
self._server.login(self.smtp_user, self.smtp_pass)
|
|
65
|
+
self._server.sendmail(from_email, [to_email], msg.as_string())
|
|
66
|
+
return True
|
|
67
|
+
except Exception:
|
|
68
|
+
logger.exception("邮件发送失败")
|
|
69
|
+
return False
|
|
70
|
+
finally:
|
|
71
|
+
try:
|
|
72
|
+
if getattr(self, "_server", None):
|
|
73
|
+
self._server.quit()
|
|
74
|
+
except Exception:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
def send_reply(
|
|
78
|
+
self,
|
|
79
|
+
to_email: str,
|
|
80
|
+
subject: str,
|
|
81
|
+
body: str,
|
|
82
|
+
in_reply_to_msg_id: Optional[str] = None,
|
|
83
|
+
references: Optional[str] = None,
|
|
84
|
+
) -> tuple[bool, Optional[str]]:
|
|
85
|
+
"""发送带线程追踪的回复邮件。
|
|
86
|
+
|
|
87
|
+
在 send() 基础上添加 In-Reply-To 和 References 邮件头,
|
|
88
|
+
实现邮件线程的层级追踪。
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
to_email: 收件人地址
|
|
92
|
+
subject: 邮件主题
|
|
93
|
+
body: 邮件正文
|
|
94
|
+
in_reply_to_msg_id: 被回复邮件的 Message-ID
|
|
95
|
+
references: 被回复邮件的 References 头内容(可选)
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
(success: bool, message_id: Optional[str])
|
|
99
|
+
"""
|
|
100
|
+
from_email = self.email_config.get("from", self.smtp_user)
|
|
101
|
+
from_name = self.email_config.get("from_name", "Mailcode Remote")
|
|
102
|
+
to_email = to_email or ""
|
|
103
|
+
|
|
104
|
+
if not from_email or not self.smtp_user:
|
|
105
|
+
raise ValueError("SMTP 配置不完整,请检查配置文件中的 mailcode_bot.email")
|
|
106
|
+
|
|
107
|
+
msg = MIMEMultipart()
|
|
108
|
+
msg["From"] = f"{from_name} <{from_email}>"
|
|
109
|
+
msg["To"] = to_email
|
|
110
|
+
msg["Subject"] = Header(subject, "utf-8")
|
|
111
|
+
|
|
112
|
+
# 生成当前邮件的 Message-ID
|
|
113
|
+
domain = from_email.split("@")[-1] if "@" in from_email else "mailcode"
|
|
114
|
+
message_id = email.utils.make_msgid(domain=domain)
|
|
115
|
+
msg["Message-ID"] = message_id
|
|
116
|
+
msg["Date"] = email.utils.formatdate(localtime=True)
|
|
117
|
+
|
|
118
|
+
# 设置线程追踪头
|
|
119
|
+
if in_reply_to_msg_id:
|
|
120
|
+
msg["In-Reply-To"] = in_reply_to_msg_id
|
|
121
|
+
ref_parts: list[str] = []
|
|
122
|
+
if references:
|
|
123
|
+
ref_parts.append(references)
|
|
124
|
+
ref_parts.append(in_reply_to_msg_id)
|
|
125
|
+
msg["References"] = " ".join(ref_parts)
|
|
126
|
+
|
|
127
|
+
msg.attach(MIMEText(body, "plain", "utf-8"))
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
if not self._create_connection():
|
|
131
|
+
return False, None
|
|
132
|
+
self._server.login(self.smtp_user, self.smtp_pass)
|
|
133
|
+
self._server.sendmail(from_email, [to_email], msg.as_string())
|
|
134
|
+
return True, message_id
|
|
135
|
+
except Exception:
|
|
136
|
+
logger.exception("邮件发送失败")
|
|
137
|
+
return False, None
|
|
138
|
+
finally:
|
|
139
|
+
try:
|
|
140
|
+
if getattr(self, "_server", None):
|
|
141
|
+
self._server.quit()
|
|
142
|
+
except Exception:
|
|
143
|
+
pass
|
|
144
|
+
|