mailcode 0.1.0__py3-none-any.whl
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/__init__.py +1 -0
- mailcode/channels/__init__.py +3 -0
- mailcode/channels/email_channel.py +144 -0
- mailcode/cli.py +360 -0
- mailcode/config.py +248 -0
- mailcode/health.py +128 -0
- mailcode/provider_presets.py +51 -0
- mailcode/relay/__init__.py +3 -0
- mailcode/relay/conversation_handler.py +576 -0
- mailcode/relay/email_listener.py +556 -0
- mailcode/relay/security.py +132 -0
- mailcode/relay/stateless_handler.py +101 -0
- mailcode/resources/default.json +36 -0
- mailcode/server.py +40 -0
- mailcode/session_cli.py +149 -0
- mailcode/utils/__init__.py +3 -0
- mailcode/utils/logging.py +48 -0
- mailcode-0.1.0.dist-info/METADATA +8 -0
- mailcode-0.1.0.dist-info/RECORD +23 -0
- mailcode-0.1.0.dist-info/WHEEL +5 -0
- mailcode-0.1.0.dist-info/entry_points.txt +2 -0
- mailcode-0.1.0.dist-info/licenses/LICENSE +21 -0
- mailcode-0.1.0.dist-info/top_level.txt +1 -0
mailcode/__init__.py
ADDED
|
@@ -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
|
+
|
mailcode/cli.py
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""mailcode 统一 CLI 入口"""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from mailcode import __version__
|
|
8
|
+
from mailcode.config import get_smtp_config, get_imap_config
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _mask_sensitive(config: dict) -> dict:
|
|
12
|
+
import json
|
|
13
|
+
result = json.loads(json.dumps(config))
|
|
14
|
+
for section in ("smtp", "imap"):
|
|
15
|
+
if section in result and "pass" in result[section]:
|
|
16
|
+
if result[section]["pass"]:
|
|
17
|
+
result[section]["pass"] = "***"
|
|
18
|
+
if "mailcode_bot" in result and "password" in result["mailcode_bot"]:
|
|
19
|
+
if result["mailcode_bot"]["password"]:
|
|
20
|
+
result["mailcode_bot"]["password"] = "***"
|
|
21
|
+
return result
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def cmd_serve(args):
|
|
25
|
+
from mailcode.config import get_config_path, validate_serve_config
|
|
26
|
+
|
|
27
|
+
# 启动预检 —— 必须在 setup_logging 之前, 避免给无效配置写 relay.log
|
|
28
|
+
errors = validate_serve_config()
|
|
29
|
+
if errors:
|
|
30
|
+
print("❌ MailCode 中继启动失败:")
|
|
31
|
+
for e in errors:
|
|
32
|
+
print(f" - {e}")
|
|
33
|
+
sys.exit(1)
|
|
34
|
+
|
|
35
|
+
from mailcode.utils.logging import setup_logging
|
|
36
|
+
_log_file = get_config_path().parent / "relay.log"
|
|
37
|
+
setup_logging(_log_file)
|
|
38
|
+
print("🌐 MailCode 中继已启动")
|
|
39
|
+
print(f"📋 日志文件: {_log_file}")
|
|
40
|
+
|
|
41
|
+
from mailcode.server import run_serve
|
|
42
|
+
run_serve(args)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def cmd_session(args):
|
|
46
|
+
"""session 子命令: list / show / delete / cleanup。"""
|
|
47
|
+
sub = getattr(args, "session_command", None)
|
|
48
|
+
if sub is None:
|
|
49
|
+
print("用法: mailcode session <list|show|delete|cleanup>", file=sys.stderr)
|
|
50
|
+
sys.exit(1)
|
|
51
|
+
|
|
52
|
+
handler = _build_session_handler()
|
|
53
|
+
|
|
54
|
+
from mailcode.session_cli import (
|
|
55
|
+
cmd_session_list, cmd_session_show,
|
|
56
|
+
cmd_session_delete, cmd_session_cleanup,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if sub == "list":
|
|
60
|
+
cmd_session_list(handler)
|
|
61
|
+
elif sub == "show":
|
|
62
|
+
cmd_session_show(handler, args.session_id)
|
|
63
|
+
elif sub == "delete":
|
|
64
|
+
cmd_session_delete(handler, args.session_id, assume_yes=getattr(args, "yes", False))
|
|
65
|
+
elif sub == "cleanup":
|
|
66
|
+
cmd_session_cleanup(handler, dry_run=getattr(args, "dry_run", False))
|
|
67
|
+
else:
|
|
68
|
+
print("用法: mailcode session <list|show|delete|cleanup>", file=sys.stderr)
|
|
69
|
+
sys.exit(1)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _build_session_handler():
|
|
73
|
+
"""构造 ConversationHandler 实例 (用真实 EmailChannel, 即便不发邮件也 OK)。"""
|
|
74
|
+
from mailcode.channels.email_channel import EmailChannel
|
|
75
|
+
from mailcode.relay.conversation_handler import ConversationHandler
|
|
76
|
+
channel = EmailChannel()
|
|
77
|
+
return ConversationHandler(email_channel=channel)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def cmd_config(args):
|
|
81
|
+
import json
|
|
82
|
+
from mailcode.config import load_config, _ensure_user_config, get_config_path
|
|
83
|
+
config_path = get_config_path()
|
|
84
|
+
if args.config_command == "show":
|
|
85
|
+
config = load_config()
|
|
86
|
+
masked = _mask_sensitive(config)
|
|
87
|
+
print(json.dumps(masked, ensure_ascii=False, indent=2))
|
|
88
|
+
elif args.config_command == "init":
|
|
89
|
+
if config_path.exists():
|
|
90
|
+
if args.force:
|
|
91
|
+
config_path.unlink()
|
|
92
|
+
_ensure_user_config()
|
|
93
|
+
load_config(force_reload=True)
|
|
94
|
+
print(f"配置已重新初始化: {config_path}")
|
|
95
|
+
else:
|
|
96
|
+
print(f"配置已存在: {config_path}")
|
|
97
|
+
print("使用 --force 可强制重新创建")
|
|
98
|
+
else:
|
|
99
|
+
_ensure_user_config()
|
|
100
|
+
load_config(force_reload=True)
|
|
101
|
+
print(f"配置已创建: {config_path}")
|
|
102
|
+
elif args.config_command == "init-test":
|
|
103
|
+
_cmd_config_init_test(force=getattr(args, "force", False))
|
|
104
|
+
elif args.config_command == "path":
|
|
105
|
+
print(config_path)
|
|
106
|
+
elif args.config_command == "validate":
|
|
107
|
+
_cmd_config_validate(load_config())
|
|
108
|
+
else:
|
|
109
|
+
print("用法: mailcode config <show|init|init-test|path|validate>", file=sys.stderr)
|
|
110
|
+
sys.exit(1)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _cmd_config_init_test(force: bool = False):
|
|
114
|
+
"""初始化集成测试配置 ~/.config/mailcode/test_config.json"""
|
|
115
|
+
import json
|
|
116
|
+
from pathlib import Path
|
|
117
|
+
|
|
118
|
+
test_config_path = Path.home() / ".config" / "mailcode" / "test_config.json"
|
|
119
|
+
if test_config_path.exists() and not force:
|
|
120
|
+
print(f"测试配置已存在: {test_config_path}")
|
|
121
|
+
print("使用 --force 可强制重新创建")
|
|
122
|
+
return
|
|
123
|
+
elif test_config_path.exists() and force:
|
|
124
|
+
test_config_path.unlink()
|
|
125
|
+
|
|
126
|
+
test_config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
127
|
+
|
|
128
|
+
template = {
|
|
129
|
+
"_comment": "MailCode 集成测试配置。由 mailcode config init-test 生成",
|
|
130
|
+
"_init_command": "mailcode config init-test",
|
|
131
|
+
|
|
132
|
+
"sender": {
|
|
133
|
+
"_notes": "发件人邮箱配置 —— 测试框架使用此账号发送命令邮件给 bot",
|
|
134
|
+
"smtp": {
|
|
135
|
+
"host": "smtp.163.com",
|
|
136
|
+
"port": 465,
|
|
137
|
+
"secure": True,
|
|
138
|
+
"user": "mailcode_test@163.com",
|
|
139
|
+
"pass": "请输入授权码"
|
|
140
|
+
},
|
|
141
|
+
"email": {
|
|
142
|
+
"from": "mailcode_test@163.com",
|
|
143
|
+
"from_name": "MailCode Test"
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
"bot": {
|
|
148
|
+
"_notes": "机器人邮箱配置 —— MailCode 监听的账号,接收命令并回复通知",
|
|
149
|
+
"smtp": {
|
|
150
|
+
"host": "smtp.163.com",
|
|
151
|
+
"port": 465,
|
|
152
|
+
"secure": True,
|
|
153
|
+
"user": "mailcode_bot@163.com",
|
|
154
|
+
"pass": "请输入授权码"
|
|
155
|
+
},
|
|
156
|
+
"imap": {
|
|
157
|
+
"host": "imap.163.com",
|
|
158
|
+
"port": 993,
|
|
159
|
+
"secure": True,
|
|
160
|
+
"user": "mailcode_bot@163.com",
|
|
161
|
+
"pass": "请输入授权码"
|
|
162
|
+
},
|
|
163
|
+
"email": {
|
|
164
|
+
"from": "mailcode_bot@163.com",
|
|
165
|
+
"from_name": "MailCode Bot",
|
|
166
|
+
"check_interval": 5
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
"security": {
|
|
171
|
+
"allowed_senders": [
|
|
172
|
+
"mailcode_test@163.com"
|
|
173
|
+
],
|
|
174
|
+
"auth_policy": "off"
|
|
175
|
+
},
|
|
176
|
+
"notification": {
|
|
177
|
+
"desktop": False,
|
|
178
|
+
"desktop_sound": ""
|
|
179
|
+
},
|
|
180
|
+
"test": {
|
|
181
|
+
"imap_folder": "INBOX",
|
|
182
|
+
"wait_timeout_seconds": 120,
|
|
183
|
+
"cleanup_after_test": True,
|
|
184
|
+
"verbose": False
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
with open(test_config_path, "w", encoding="utf-8") as f:
|
|
189
|
+
json.dump(template, f, ensure_ascii=False, indent=2)
|
|
190
|
+
|
|
191
|
+
print(f"测试配置已创建: {test_config_path}")
|
|
192
|
+
print("请编辑此文件填入 sender 和 bot 的授权码(pass 字段)")
|
|
193
|
+
print("编辑完成后执行: bash tests/run_tests.sh --integration")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _cmd_config_validate(config: dict):
|
|
197
|
+
from mailcode.config import validate_serve_config
|
|
198
|
+
errors = validate_serve_config()
|
|
199
|
+
|
|
200
|
+
if errors:
|
|
201
|
+
print("❌ 配置校验失败:")
|
|
202
|
+
for e in errors:
|
|
203
|
+
print(f" - {e}")
|
|
204
|
+
sys.exit(1)
|
|
205
|
+
else:
|
|
206
|
+
smtp = get_smtp_config() # 含 auto-detected 值
|
|
207
|
+
imap = get_imap_config() # 含 auto-detected 值
|
|
208
|
+
allowed = config.get("security", {}).get("allowed_senders", [])
|
|
209
|
+
print("✅ 配置校验通过")
|
|
210
|
+
print(f" SMTP: {smtp.get('host')}:{smtp.get('port')} (user: {smtp.get('user')})")
|
|
211
|
+
print(f" IMAP: {imap.get('host')}:{imap.get('port')} (user: {imap.get('user')})")
|
|
212
|
+
print(f" 发件人白名单: {len(allowed)} 个")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def cmd_health(args):
|
|
216
|
+
from mailcode.health import run_health
|
|
217
|
+
ok = run_health()
|
|
218
|
+
sys.exit(0 if ok else 1)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def build_parser():
|
|
222
|
+
parser = argparse.ArgumentParser(
|
|
223
|
+
prog="mailcode",
|
|
224
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
225
|
+
description=(
|
|
226
|
+
"MailCode: 通过 IMAP 邮件远程操控本地 AI Agent (OpenCode / Claude Code)。\n"
|
|
227
|
+
"\n"
|
|
228
|
+
"工作流: 发件人给机器人邮箱发邮件 → MailCode 在 IMAP 拉取 → 注入到本地 Agent\n"
|
|
229
|
+
" → Agent 回复内容回写到同一主题 → MailCode 通过 SMTP 把回复转发给发件人。\n"
|
|
230
|
+
"\n"
|
|
231
|
+
"本 CLI 主要用于: 配置管理、连通性检查、启动中继、以及维护 Agent 对话 session。"
|
|
232
|
+
),
|
|
233
|
+
epilog=(
|
|
234
|
+
"典型使用流程:\n"
|
|
235
|
+
" 1) 首次部署 mailcode config init # 生成默认配置\n"
|
|
236
|
+
" 2) 编辑授权码 mailcode config path && $EDITOR .../config.json\n"
|
|
237
|
+
" 3) 校验配置 mailcode config validate\n"
|
|
238
|
+
" 4) 自检连通性 mailcode health\n"
|
|
239
|
+
" 5) 启动中继 mailcode serve --idle # 长期后台运行\n"
|
|
240
|
+
" 6) 维护会话 mailcode session list|show|delete|cleanup\n"
|
|
241
|
+
"\n"
|
|
242
|
+
"调试提示:\n"
|
|
243
|
+
" • 想看收到什么邮件但不想执行: mailcode serve --once --dry-run\n"
|
|
244
|
+
" • 想用临时配置: mailcode --config /tmp/x.json <子命令>\n"
|
|
245
|
+
" • 集成测试配置: mailcode config init-test"
|
|
246
|
+
),
|
|
247
|
+
)
|
|
248
|
+
parser.add_argument("--version", action="version", version=f"mailcode {__version__}")
|
|
249
|
+
parser.add_argument("--config", "-c", metavar="PATH",
|
|
250
|
+
help="指定配置文件路径(默认: ~/.config/mailcode/config.json)")
|
|
251
|
+
|
|
252
|
+
subparsers = parser.add_subparsers(dest="command", title="子命令", metavar="<子命令>")
|
|
253
|
+
|
|
254
|
+
# ── serve ──
|
|
255
|
+
p_serve = subparsers.add_parser(
|
|
256
|
+
"serve",
|
|
257
|
+
help="启动 IMAP 监听中继 (前台常驻)",
|
|
258
|
+
description=(
|
|
259
|
+
"启动 IMAP 监听中继: 拉取 bot 邮箱里的未读邮件, 注入本地 AI Agent, 把回复通过 SMTP 转发回发件人。\n"
|
|
260
|
+
"Ctrl-C 退出, 日志写入 ~/.config/mailcode/relay.log。"
|
|
261
|
+
),
|
|
262
|
+
)
|
|
263
|
+
p_serve.add_argument("--dry-run", action="store_true",
|
|
264
|
+
help="干跑模式: 只打印邮件内容, 不注入 Agent, 不发送回复(用于排查邮件解析)")
|
|
265
|
+
p_serve.add_argument("--once", action="store_true",
|
|
266
|
+
help="处理完一轮未读后退出 (用于脚本/调试, 默认持续轮询)")
|
|
267
|
+
p_serve.add_argument("--idle", action="store_true",
|
|
268
|
+
help="使用 IMAP IDLE 长连接, 实时接收新邮件推送 (推荐生产环境使用)")
|
|
269
|
+
p_serve.add_argument("--session", "-S", action="store_true",
|
|
270
|
+
help="按邮件主题维护多轮对话 session (覆盖 config 中 session.enabled)")
|
|
271
|
+
|
|
272
|
+
# ── config ──
|
|
273
|
+
p_config = subparsers.add_parser(
|
|
274
|
+
"config",
|
|
275
|
+
help="配置管理 (init/show/validate/path/init-test)",
|
|
276
|
+
description="管理 ~/.config/mailcode/ 下的配置文件: 初始化、查看、校验、定位。",
|
|
277
|
+
)
|
|
278
|
+
p_config_sub = p_config.add_subparsers(dest="config_command", title="配置动作", metavar="<动作>")
|
|
279
|
+
|
|
280
|
+
p_config_sub.add_parser("show", help="打印当前配置 (密码字段自动脱敏为 ***)")
|
|
281
|
+
p_config_sub.add_parser("path", help="打印配置文件绝对路径 (便于编辑器/查看)")
|
|
282
|
+
p_config_sub.add_parser("validate", help="校验配置完整性: 检查 SMTP/IMAP/白名单等必填项")
|
|
283
|
+
|
|
284
|
+
p_init = p_config_sub.add_parser("init",
|
|
285
|
+
help="首次部署: 生成默认配置到 ~/.config/mailcode/config.json",
|
|
286
|
+
description="如果配置已存在则跳过, 加上 --force 会先删除再重建 (注意: 会丢失已有授权码)。")
|
|
287
|
+
p_init.add_argument("--force", action="store_true",
|
|
288
|
+
help="强制重新创建 (先删除已有配置, 谨慎使用)")
|
|
289
|
+
|
|
290
|
+
p_init_test = p_config_sub.add_parser("init-test",
|
|
291
|
+
help="生成集成测试配置 ~/.config/mailcode/test_config.json",
|
|
292
|
+
description=(
|
|
293
|
+
"集成测试需要 sender + bot 两个邮箱, 与正式配置完全隔离。\n"
|
|
294
|
+
"生成后请填入两个邮箱的授权码, 然后跑: bash tests/run_tests.sh --integration"
|
|
295
|
+
))
|
|
296
|
+
p_init_test.add_argument("--force", action="store_true",
|
|
297
|
+
help="强制重新创建 (先删除已有测试配置)")
|
|
298
|
+
|
|
299
|
+
# ── health ──
|
|
300
|
+
p_health = subparsers.add_parser(
|
|
301
|
+
"health",
|
|
302
|
+
help="邮件连通性自检 (SMTP + IMAP)",
|
|
303
|
+
description="按顺序检查: 配置完整性 → SMTP 连接/登录/发信 → IMAP 连接/登录/收件箱, 最后汇总。",
|
|
304
|
+
)
|
|
305
|
+
p_health.add_argument("--send", action="store_true",
|
|
306
|
+
help="额外发一封自检邮件到 bot 自身 (默认只检查连接与登录)")
|
|
307
|
+
|
|
308
|
+
# ── session ──
|
|
309
|
+
p_session = subparsers.add_parser(
|
|
310
|
+
"session",
|
|
311
|
+
help="对话 session 管理 (list|show|delete|cleanup)",
|
|
312
|
+
description=(
|
|
313
|
+
"Session = 同一邮件主题下的多轮对话上下文, 以独立文件持久化。\n"
|
|
314
|
+
"用子命令 list/show/delete/cleanup 来维护这些 session。"
|
|
315
|
+
),
|
|
316
|
+
)
|
|
317
|
+
p_session_sub = p_session.add_subparsers(dest="session_command", title="session 动作", metavar="<动作>")
|
|
318
|
+
|
|
319
|
+
p_session_sub.add_parser("list", help="列出全部 session (ID/发件人/主题/最近活动/消息数/工作目录)")
|
|
320
|
+
p_session_show = p_session_sub.add_parser("show",
|
|
321
|
+
help="查看单个 session 的完整邮件流 (in/out 交替)")
|
|
322
|
+
p_session_show.add_argument("session_id", help="12 位 hex session ID (从 list 中获取)")
|
|
323
|
+
|
|
324
|
+
p_session_delete = p_session_sub.add_parser("delete",
|
|
325
|
+
help="删除单个 session (会先打印详情并提示确认, --yes 跳过)")
|
|
326
|
+
p_session_delete.add_argument("session_id", help="12 位 hex session ID")
|
|
327
|
+
p_session_delete.add_argument("-y", "--yes", action="store_true",
|
|
328
|
+
help="跳过交互式确认, 直接删除")
|
|
329
|
+
|
|
330
|
+
p_session_cleanup = p_session_sub.add_parser("cleanup",
|
|
331
|
+
help="按 TTL 清理过期 session (用 --dry-run 先预览)")
|
|
332
|
+
p_session_cleanup.add_argument("--dry-run", action="store_true",
|
|
333
|
+
help="只列出将被清理的 session, 不实际删除")
|
|
334
|
+
|
|
335
|
+
return parser
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def main():
|
|
339
|
+
parser = build_parser()
|
|
340
|
+
args = parser.parse_args()
|
|
341
|
+
|
|
342
|
+
if args.config:
|
|
343
|
+
from mailcode.config import set_config_path
|
|
344
|
+
set_config_path(args.config)
|
|
345
|
+
|
|
346
|
+
if args.command is None:
|
|
347
|
+
parser.print_help()
|
|
348
|
+
sys.exit(1)
|
|
349
|
+
|
|
350
|
+
if args.command == "serve":
|
|
351
|
+
cmd_serve(args)
|
|
352
|
+
elif args.command == "config":
|
|
353
|
+
cmd_config(args)
|
|
354
|
+
elif args.command == "health":
|
|
355
|
+
cmd_health(args)
|
|
356
|
+
elif args.command == "session":
|
|
357
|
+
cmd_session(args)
|
|
358
|
+
|
|
359
|
+
if __name__ == "__main__":
|
|
360
|
+
main()
|