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/config.py ADDED
@@ -0,0 +1,248 @@
1
+ import json
2
+ import logging
3
+ import os
4
+ import shutil
5
+ from pathlib import Path
6
+ from typing import Optional, Dict, Any
7
+ from importlib import resources
8
+
9
+ from mailcode.provider_presets import PROVIDER_PRESETS, detect_provider
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ DEFAULT_CONFIG_PATH = resources.files("mailcode") / "resources" / "default.json"
14
+
15
+ # 配置路径优先级:MAILCODE_CONFIG 环境变量 > 默认 ~/.config/mailcode/config.json
16
+ # 可通过 set_config_path() 运行时覆盖
17
+ _env_config = os.environ.get("MAILCODE_CONFIG")
18
+ USER_CONFIG_PATH = Path(_env_config) if _env_config else Path(os.environ.get("HOME", "~")) / ".config" / "mailcode" / "config.json"
19
+
20
+ _config_cache: Optional[Dict[str, Any]] = None
21
+
22
+
23
+ def set_config_path(path: str):
24
+ """运行时设置自定义配置文件路径,覆盖环境变量和默认值。清除缓存强制重载。"""
25
+ global USER_CONFIG_PATH, _config_cache
26
+ USER_CONFIG_PATH = Path(path)
27
+ _config_cache = None
28
+
29
+
30
+ def get_config_path() -> Path:
31
+ """返回当前有效的配置文件路径。"""
32
+ return USER_CONFIG_PATH
33
+
34
+
35
+ def _ensure_user_config():
36
+ if USER_CONFIG_PATH.exists():
37
+ return
38
+
39
+ USER_CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
40
+
41
+ if DEFAULT_CONFIG_PATH.exists():
42
+ shutil.copy(DEFAULT_CONFIG_PATH, USER_CONFIG_PATH)
43
+ print(f"已创建用户配置文件: {USER_CONFIG_PATH}")
44
+ print("请编辑此文件填入您的邮箱和密码配置")
45
+ else:
46
+ default_config = {
47
+ "mailcode_bot": {
48
+ "_notes": {
49
+ "email": "MailCode 机器人管理的邮箱地址。MailCode 会监听此邮箱的收件箱,也用此邮箱发信",
50
+ "password": "邮箱授权码/应用专用密码,不是登录密码。QQ邮箱: 设置→账户→POP3/IMAP→生成授权码",
51
+ "from_name": "发件人显示名称",
52
+ "check_interval": "检查新邮件的间隔(秒),建议 30 秒"
53
+ },
54
+ "email": "",
55
+ "password": "",
56
+ "from_name": "Mailcode Remote",
57
+ "check_interval": 30
58
+ },
59
+ "security": {
60
+ "_notes": {
61
+ "allowed_senders": "哪些邮箱可以给 MailCode 发命令,填你自己的邮箱。多个用逗号分隔。示例: your@qq.com",
62
+ "auth_policy": "邮件认证策略。warn=仅警告, strict=严格拒绝, off=关闭"
63
+ },
64
+ "allowed_senders": [],
65
+ "auth_policy": "warn"
66
+ },
67
+ "session": {
68
+ "_notes": {
69
+ "enabled": "是否启用 session 模式",
70
+ "response_timeout_seconds": "等待 AI 回复的超时时间(秒)",
71
+ "idle_timeout_hours": "空闲超时时间(小时)",
72
+ "session_ttl_days": "session 过期天数, 0 或负数不清理",
73
+ "cleanup_on_startup": "启动时自动清理过期 session"
74
+ },
75
+ "enabled": True,
76
+ "response_timeout_seconds": 180,
77
+ "idle_timeout_hours": 4,
78
+ "session_ttl_days": 90,
79
+ "cleanup_on_startup": True
80
+ }
81
+ }
82
+ with open(USER_CONFIG_PATH, "w", encoding="utf-8") as f:
83
+ json.dump(default_config, f, ensure_ascii=False, indent=2)
84
+ print(f"已创建用户配置文件: {USER_CONFIG_PATH}")
85
+ print("请编辑此文件填入您的邮箱和密码配置")
86
+
87
+
88
+ def load_config(force_reload: bool = False) -> Dict[str, Any]:
89
+ global _config_cache
90
+ if _config_cache is not None and not force_reload:
91
+ return _config_cache
92
+
93
+ _ensure_user_config()
94
+
95
+ with open(USER_CONFIG_PATH, "r", encoding="utf-8") as f:
96
+ try:
97
+ _config_cache = json.load(f)
98
+ except json.JSONDecodeError:
99
+ _config_cache = {}
100
+ logger.warning(f"配置文件 {USER_CONFIG_PATH} 格式错误,已重置为空配置")
101
+
102
+ return _config_cache
103
+
104
+
105
+ def _get_bot_config(config):
106
+ bot = config.get("mailcode_bot") or {}
107
+ if not bot:
108
+ bot = config.get("account") or {}
109
+ if not bot:
110
+ bot = config.get("bot") or {}
111
+ return bot
112
+
113
+
114
+ def _merge_identity(section_cfg: Dict[str, Any], config: Dict[str, Any]) -> Dict[str, Any]:
115
+ """若 section 中缺少 user/pass,从 bot 段补充"""
116
+ if section_cfg.get("user") and section_cfg.get("pass"):
117
+ return section_cfg
118
+ bot = _get_bot_config(config)
119
+ result = dict(section_cfg)
120
+ if not result.get("user") and bot.get("email"):
121
+ result["user"] = bot["email"]
122
+ if not result.get("pass") and bot.get("password"):
123
+ result["pass"] = bot["password"]
124
+ return result
125
+
126
+
127
+ def get_smtp_config():
128
+ config = load_config()
129
+ bot = _get_bot_config(config)
130
+ smtp_manual = config.get("smtp", {})
131
+ provider = bot.get("provider", "") or detect_provider(bot.get("email", ""))
132
+ preset = PROVIDER_PRESETS.get(provider, {}).get("smtp", {})
133
+ merged = dict(preset)
134
+ merged.update(smtp_manual)
135
+ merged = _merge_identity(merged, config)
136
+ return merged
137
+
138
+
139
+ def get_imap_config():
140
+ config = load_config()
141
+ bot = _get_bot_config(config)
142
+ imap_manual = config.get("imap", {})
143
+ provider = bot.get("provider", "") or detect_provider(bot.get("email", ""))
144
+ preset = PROVIDER_PRESETS.get(provider, {}).get("imap", {})
145
+ merged = dict(preset)
146
+ merged.update(imap_manual)
147
+ merged = _merge_identity(merged, config)
148
+ return merged
149
+
150
+
151
+ def get_email_config():
152
+ config = load_config()
153
+ bot = _get_bot_config(config)
154
+ bot_email = bot.get("email", "")
155
+ # 从 mailcode_bot 读新字段,回退到旧 email 段
156
+ legacy_email = config.get("email", {})
157
+ return {
158
+ "from": bot_email,
159
+ "from_name": bot.get("from_name") or legacy_email.get("from_name", "Mailcode Remote"),
160
+ "check_interval": bot.get("check_interval") or legacy_email.get("check_interval", 5),
161
+ }
162
+
163
+
164
+ def get_security_config() -> Dict[str, Any]:
165
+ config = load_config()
166
+ return config.get("security", {})
167
+
168
+
169
+ def get_auth_policy() -> str:
170
+ config = load_config()
171
+ return config.get("security", {}).get("auth_policy", "warn")
172
+
173
+
174
+ def get_notification_config() -> Dict[str, Any]:
175
+ config = load_config()
176
+ return config.get("notification", {})
177
+
178
+
179
+ SESSION_DEFAULTS = {
180
+ "enabled": True,
181
+ "response_timeout_seconds": 180,
182
+ "idle_timeout_hours": 4,
183
+ "session_ttl_days": 90,
184
+ "cleanup_on_startup": True,
185
+ }
186
+
187
+
188
+ def get_session_config() -> Dict[str, Any]:
189
+ config = load_config()
190
+ raw = config.get("session", {})
191
+ result = dict(SESSION_DEFAULTS)
192
+ result.update(raw)
193
+ return result
194
+
195
+
196
+ def is_session_enabled() -> bool:
197
+ return get_session_config().get("enabled", True)
198
+
199
+
200
+ def validate_serve_config() -> list[str]:
201
+ """校验 serve 启动所需配置项, 返回错误消息列表 (空列表 = 通过)。
202
+
203
+ 检查 5 类必填项:
204
+ - mailcode_bot.email / password 非空
205
+ - SMTP host / user / pass 非空 (依赖 _merge_identity 自动补全)
206
+ - IMAP host / user / pass 非空 (依赖 _merge_identity 自动补全)
207
+ - security.allowed_senders 非空列表
208
+
209
+ 配置读取失败时 (文件不存在 / JSON 损坏) 返回包含 "无法读取配置" 的单元素列表。
210
+ """
211
+ errors: list[str] = []
212
+
213
+ try:
214
+ config = load_config()
215
+ except Exception as e:
216
+ return [f"无法读取配置: {e}"]
217
+
218
+ try:
219
+ bot = _get_bot_config(config)
220
+ security = config.get("security", {})
221
+ smtp = get_smtp_config()
222
+ imap = get_imap_config()
223
+
224
+ if not bot.get("email"):
225
+ errors.append("mailcode_bot.email 未设置")
226
+ if not bot.get("password"):
227
+ errors.append("mailcode_bot.password 未设置")
228
+ if not smtp.get("host"):
229
+ errors.append("SMTP host 未设置(自动识别失败)")
230
+ if not smtp.get("user"):
231
+ errors.append("SMTP 用户或 mailcode_bot.email 未设置")
232
+ if not smtp.get("pass"):
233
+ errors.append("SMTP 密码或 mailcode_bot.password 未设置")
234
+ if not imap.get("host"):
235
+ errors.append("IMAP host 未设置(自动识别失败)")
236
+ if not imap.get("user"):
237
+ errors.append("IMAP 用户或 mailcode_bot.email 未设置")
238
+ if not imap.get("pass"):
239
+ errors.append("IMAP 密码或 mailcode_bot.password 未设置")
240
+
241
+ allowed = security.get("allowed_senders", [])
242
+ if not allowed:
243
+ errors.append("security.allowed_senders 为空(至少应包含自己的邮箱)")
244
+ except Exception as e:
245
+ logger.warning(f"配置校验过程中出错: {e}")
246
+ errors.append(f"配置校验失败: {e}")
247
+
248
+ return errors
mailcode/health.py ADDED
@@ -0,0 +1,128 @@
1
+ import smtplib
2
+ import imaplib
3
+ import logging
4
+
5
+ from mailcode.config import get_smtp_config, get_imap_config, get_email_config
6
+
7
+ logger = logging.getLogger("mailcode")
8
+
9
+
10
+ def _check(label: str, ok: bool, detail: str = ""):
11
+ icon = "✓" if ok else "✗"
12
+ print(f" {icon} {label}")
13
+ if detail:
14
+ print(f" {detail}")
15
+ return ok
16
+
17
+
18
+ def run_health():
19
+ """运行邮件连通性检查"""
20
+ print("MailCode Health Check\n")
21
+
22
+ all_ok = True
23
+
24
+ # ── 配置检查 ──
25
+ print("配置检查:")
26
+ smtp_cfg = get_smtp_config()
27
+ imap_cfg = get_imap_config()
28
+ email_cfg = get_email_config()
29
+
30
+ all_ok &= _check("SMTP 用户", bool(smtp_cfg.get("user")),
31
+ f"host={smtp_cfg.get('host')} port={smtp_cfg.get('port')} user={smtp_cfg.get('user')}")
32
+ all_ok &= _check("SMTP 密码", bool(smtp_cfg.get("pass")))
33
+ all_ok &= _check("IMAP 用户", bool(imap_cfg.get("user")),
34
+ f"host={imap_cfg.get('host')} port={imap_cfg.get('port')} user={imap_cfg.get('user')}")
35
+ all_ok &= _check("IMAP 密码", bool(imap_cfg.get("pass")))
36
+ all_ok &= _check("发件地址", bool(email_cfg.get("from")),
37
+ f"from={email_cfg.get('from')}")
38
+
39
+ if not smtp_cfg.get("user") or not smtp_cfg.get("pass") or not imap_cfg.get("user") or not imap_cfg.get("pass"):
40
+ print("\n 配置不完整,跳过网络检查")
41
+ return all_ok
42
+
43
+ # ── SMTP 检查 ──
44
+ print("\nSMTP 检查:")
45
+ host = smtp_cfg.get("host", "smtp.qq.com")
46
+ port = smtp_cfg.get("port", 465)
47
+ secure = smtp_cfg.get("secure", False)
48
+
49
+ server = None
50
+ try:
51
+ server = smtplib.SMTP_SSL(host, port, timeout=10) if secure else smtplib.SMTP(host, port, timeout=10)
52
+ if not secure:
53
+ server.starttls()
54
+ all_ok &= _check("连接", True, f"{host}:{port}")
55
+ except Exception as e:
56
+ all_ok &= _check("连接", False, str(e))
57
+ if server is not None:
58
+ try:
59
+ server.quit()
60
+ except Exception:
61
+ pass
62
+ print("\n 跳过后续 SMTP 检查")
63
+ return all_ok
64
+
65
+ try:
66
+ server.login(smtp_cfg["user"], smtp_cfg["pass"])
67
+ all_ok &= _check("登录", True)
68
+ except Exception as e:
69
+ all_ok &= _check("登录", False, str(e))
70
+ server.quit()
71
+ return all_ok
72
+
73
+ to_email = email_cfg.get("from", "")
74
+ if to_email:
75
+ try:
76
+ from_email = email_cfg.get("from", smtp_cfg["user"])
77
+ server.sendmail(smtp_cfg["user"], [to_email],
78
+ f"From: {from_email}\nSubject: MailCode Health Check\n\nThis is a test email from MailCode health check.")
79
+ all_ok &= _check("发信", True, f"to={to_email}")
80
+ except Exception as e:
81
+ all_ok &= _check("发信", False, str(e))
82
+ else:
83
+ if not to_email:
84
+ _check("发信", False, "未配置 mailcode_bot.email")
85
+ return all_ok
86
+
87
+ server.quit()
88
+
89
+ # ── IMAP 检查 ──
90
+ print("\nIMAP 检查:")
91
+ host = imap_cfg.get("host", "imap.qq.com")
92
+ port = imap_cfg.get("port", 993)
93
+
94
+ try:
95
+ mail = imaplib.IMAP4_SSL(host, port, timeout=10)
96
+ all_ok &= _check("连接", True, f"{host}:{port}")
97
+ except Exception as e:
98
+ all_ok &= _check("连接", False, str(e))
99
+ print("\n 跳过后续 IMAP 检查")
100
+ return all_ok
101
+
102
+ try:
103
+ mail.login(imap_cfg["user"], imap_cfg["pass"])
104
+ all_ok &= _check("登录", True)
105
+ except Exception as e:
106
+ all_ok &= _check("登录", False, str(e))
107
+ mail.logout()
108
+ return all_ok
109
+
110
+ try:
111
+ imaplib.Commands["ID"] = ("NONAUTH", "AUTH", "SELECTED")
112
+ mail._simple_command("ID", '("name" "mailcode" "version" "1.0")')
113
+ except Exception:
114
+ pass
115
+
116
+ try:
117
+ typ, dat = mail.select("INBOX")
118
+ ok = typ == "OK"
119
+ count = len(dat[0].split()) if dat and dat[0] else 0
120
+ all_ok &= _check("收件箱", ok, f"select={typ} 邮件数={count}")
121
+ except Exception as e:
122
+ all_ok &= _check("收件箱", False, str(e))
123
+
124
+ mail.logout()
125
+
126
+ print(f"\n{'='*30}")
127
+ print(f"结果: {'全部正常' if all_ok else '存在问题'}")
128
+ return all_ok
@@ -0,0 +1,51 @@
1
+ """邮件服务商预设 — SMTP/IMAP 默认值及域名检测"""
2
+
3
+ # 域名 → provider 映射
4
+ DOMAIN_PROVIDER_MAP = {
5
+ "qq.com": "qq",
6
+ "126.com": "126",
7
+ "163.com": "163",
8
+ "gmail.com": "gmail",
9
+ "outlook.com": "outlook",
10
+ "hotmail.com": "outlook",
11
+ "live.com": "outlook",
12
+ }
13
+
14
+ # provider → SMTP/IMAP 默认值
15
+ PROVIDER_PRESETS = {
16
+ "qq": {
17
+ "smtp": {"host": "smtp.qq.com", "port": 465, "secure": True},
18
+ "imap": {"host": "imap.qq.com", "port": 993, "secure": True},
19
+ },
20
+ "126": {
21
+ "smtp": {"host": "smtp.126.com", "port": 465, "secure": True},
22
+ "imap": {"host": "imap.126.com", "port": 993, "secure": True},
23
+ },
24
+ "163": {
25
+ "smtp": {"host": "smtp.163.com", "port": 465, "secure": True},
26
+ "imap": {"host": "imap.163.com", "port": 993, "secure": True},
27
+ },
28
+ "gmail": {
29
+ "smtp": {"host": "smtp.gmail.com", "port": 587, "secure": True},
30
+ "imap": {"host": "imap.gmail.com", "port": 993, "secure": True},
31
+ },
32
+ "outlook": {
33
+ "smtp": {"host": "smtp-mail.outlook.com", "port": 587, "secure": True},
34
+ "imap": {"host": "outlook.office365.com", "port": 993, "secure": True},
35
+ },
36
+ }
37
+
38
+
39
+ def detect_provider(email: str) -> str:
40
+ """根据邮箱域名识别邮件服务商。
41
+
42
+ Args:
43
+ email: 邮箱地址。
44
+
45
+ Returns:
46
+ provider 名称: "qq", "126", "163", "gmail", "outlook", 或 "custom"。
47
+ """
48
+ if not email or "@" not in email:
49
+ return "custom"
50
+ domain = email.split("@", 1)[1].lower().strip()
51
+ return DOMAIN_PROVIDER_MAP.get(domain, "custom")
@@ -0,0 +1,3 @@
1
+ from mailcode.relay.security import SecurityChecker
2
+
3
+ __all__ = ["SecurityChecker"]