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
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""单次回复模式核心 — 每封邮件独立调一次 claude -p, 不维护 session。"""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from mailcode.relay.conversation_handler import (
|
|
7
|
+
call_claude,
|
|
8
|
+
extract_cwd,
|
|
9
|
+
send_error_email,
|
|
10
|
+
strip_cwd,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class StatelessHandler:
|
|
17
|
+
"""单次回复处理器: 一封邮件 → 一次 ``claude -p`` → 一封回信。
|
|
18
|
+
|
|
19
|
+
与 ``ConversationHandler`` 的区别:
|
|
20
|
+
- 不写 session 文件, 不读 index
|
|
21
|
+
- 无状态, 可安全 lazy init 后复用同一实例
|
|
22
|
+
- cwd 解析复用 ``extract_cwd`` / ``strip_cwd`` (行为对齐)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, email_channel):
|
|
26
|
+
self.email_channel = email_channel
|
|
27
|
+
|
|
28
|
+
def handle_email(self, from_email: str, subject: str, body: str,
|
|
29
|
+
references: str = "", in_reply_to: str = "") -> bool:
|
|
30
|
+
"""主入口: 处理一封单次回复邮件。
|
|
31
|
+
|
|
32
|
+
流程:
|
|
33
|
+
1. 提取 cwd + 剥离
|
|
34
|
+
2. 构建 prompt
|
|
35
|
+
3. 调 ``claude -p``
|
|
36
|
+
4. 错误处理 (发通知邮件)
|
|
37
|
+
5. SMTP 发回复
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
True 表示成功发送回复
|
|
41
|
+
"""
|
|
42
|
+
logger.info("处理单次回复邮件: from=%s subject=%s", from_email, subject)
|
|
43
|
+
|
|
44
|
+
# 1. 提取 cwd + 剥离
|
|
45
|
+
extracted_cwd = extract_cwd(body)
|
|
46
|
+
clean_body = strip_cwd(body) if extracted_cwd is not None else body
|
|
47
|
+
|
|
48
|
+
# 2. 构建 prompt (单次回复: 无 session, 直接给邮件正文)
|
|
49
|
+
prompt = (
|
|
50
|
+
f"用户最新邮件:\n\n"
|
|
51
|
+
f"主题: {subject}\n\n"
|
|
52
|
+
f"{clean_body}\n\n"
|
|
53
|
+
f"请直接回复这封邮件, 内容将作为邮件正文发送, 用纯文本格式。"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# 3. 调 claude
|
|
57
|
+
cwd = extracted_cwd or str(Path.home())
|
|
58
|
+
response = call_claude(prompt, cwd=cwd)
|
|
59
|
+
|
|
60
|
+
# 4. claude 失败 → 写日志 + 发邮件通知用户
|
|
61
|
+
if response is None:
|
|
62
|
+
logger.error("claude -p 调用失败, 通知用户: from=%s", from_email)
|
|
63
|
+
send_error_email(
|
|
64
|
+
self.email_channel, from_email, subject,
|
|
65
|
+
"抱歉, 处理你的邮件时遇到技术问题。请稍后再试。详细错误已记录到日志。",
|
|
66
|
+
references, in_reply_to,
|
|
67
|
+
)
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
# 5. 空 response → 写日志 + 发邮件通知用户
|
|
71
|
+
if not response:
|
|
72
|
+
logger.error("claude -p 返回空 response, 通知用户: from=%s", from_email)
|
|
73
|
+
send_error_email(
|
|
74
|
+
self.email_channel, from_email, subject,
|
|
75
|
+
"抱歉, AI 助手这次没有回复内容。请稍后再试, 或换个方式描述你的问题。",
|
|
76
|
+
references, in_reply_to,
|
|
77
|
+
)
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
# 6. 准备 outgoing 邮件
|
|
81
|
+
reply_subject = subject if subject.startswith("Re:") else f"Re: {subject}"
|
|
82
|
+
|
|
83
|
+
# 7. SMTP 发回复
|
|
84
|
+
try:
|
|
85
|
+
send_ok, _ = self.email_channel.send_reply(
|
|
86
|
+
to_email=from_email,
|
|
87
|
+
subject=reply_subject,
|
|
88
|
+
body=response,
|
|
89
|
+
in_reply_to_msg_id=in_reply_to,
|
|
90
|
+
references=references,
|
|
91
|
+
)
|
|
92
|
+
except Exception as e:
|
|
93
|
+
logger.error("单次回复发送异常: %s", e)
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
if not send_ok:
|
|
97
|
+
logger.error("单次回复发送失败: from=%s", from_email)
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
logger.info("单次回复已发送: from=%s", from_email)
|
|
101
|
+
return True
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mailcode_bot": {
|
|
3
|
+
"_notes": {
|
|
4
|
+
"email": "MailCode 机器人管理的邮箱地址。MailCode 会监听此邮箱的收件箱,也用此邮箱发信",
|
|
5
|
+
"password": "邮箱授权码/应用专用密码,不是登录密码。QQ邮箱: 设置→账户→POP3/IMAP→生成授权码",
|
|
6
|
+
"from_name": "发件人显示名称",
|
|
7
|
+
"check_interval": "检查新邮件的间隔(秒),默认 5 秒"
|
|
8
|
+
},
|
|
9
|
+
"from_name": "Mailcode Remote",
|
|
10
|
+
"check_interval": 5,
|
|
11
|
+
"email": "",
|
|
12
|
+
"password": ""
|
|
13
|
+
},
|
|
14
|
+
"security": {
|
|
15
|
+
"_notes": {
|
|
16
|
+
"allowed_senders": "哪些邮箱可以给 MailCode 发命令,填你自己的邮箱。多个用逗号分隔。示例: your@qq.com",
|
|
17
|
+
"auth_policy": "邮件认证策略。warn=仅警告, strict=严格拒绝, off=关闭"
|
|
18
|
+
},
|
|
19
|
+
"allowed_senders": [],
|
|
20
|
+
"auth_policy": "warn"
|
|
21
|
+
},
|
|
22
|
+
"session": {
|
|
23
|
+
"_notes": {
|
|
24
|
+
"enabled": "是否启用 session 模式",
|
|
25
|
+
"response_timeout_seconds": "等待 AI 回复的超时时间(秒)",
|
|
26
|
+
"idle_timeout_hours": "空闲超时时间(小时)",
|
|
27
|
+
"session_ttl_days": "session 过期天数, 0 或负数不清理",
|
|
28
|
+
"cleanup_on_startup": "启动时自动清理过期 session"
|
|
29
|
+
},
|
|
30
|
+
"enabled": true,
|
|
31
|
+
"response_timeout_seconds": 180,
|
|
32
|
+
"idle_timeout_hours": 4,
|
|
33
|
+
"session_ttl_days": 90,
|
|
34
|
+
"cleanup_on_startup": true
|
|
35
|
+
}
|
|
36
|
+
}
|
mailcode/server.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""MailCode IMAP 监听服务 — 由 cli.py:cmd_serve 调用"""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import signal
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from mailcode.relay.email_listener import IMAPListener
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger("mailcode")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def run_serve(args):
|
|
13
|
+
"""启动 IMAP 监听器,根据 args 运行(单次轮询 / IDLE / 普通监听)。
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
args: 具有 dry_run、once、idle 属性的 Namespace 对象。
|
|
17
|
+
"""
|
|
18
|
+
listener = IMAPListener()
|
|
19
|
+
|
|
20
|
+
def signal_handler(signum, frame):
|
|
21
|
+
print("\n🛑 收到关闭信号,正在停止监听器...", flush=True)
|
|
22
|
+
listener.stop()
|
|
23
|
+
|
|
24
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
25
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
if args.once:
|
|
29
|
+
emails = listener.fetch_unread_emails(dry_run=args.dry_run)
|
|
30
|
+
logger.info(f"发现 {len(emails)} 封新邮件")
|
|
31
|
+
for entry in emails:
|
|
32
|
+
success, message = listener.process_email(
|
|
33
|
+
entry, dry_run=args.dry_run, force_session=args.session or None,
|
|
34
|
+
)
|
|
35
|
+
logger.info(f"{'✅' if success else '❌'} [{entry.get('token')}] {message}")
|
|
36
|
+
else:
|
|
37
|
+
listener.listen(dry_run=args.dry_run, use_idle=args.idle)
|
|
38
|
+
except Exception:
|
|
39
|
+
logger.exception("监听器主循环异常退出")
|
|
40
|
+
sys.exit(1)
|
mailcode/session_cli.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""mailcode session 子命令的 CLI 格式化与呈现"""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def fmt_ts(ts) -> str:
|
|
7
|
+
"""time.time() 浮点 → 'YYYY-MM-DD HH:MM' 本地时间。"""
|
|
8
|
+
import datetime
|
|
9
|
+
try:
|
|
10
|
+
ts = float(ts or 0)
|
|
11
|
+
except (TypeError, ValueError):
|
|
12
|
+
return "-"
|
|
13
|
+
if ts <= 0:
|
|
14
|
+
return "-"
|
|
15
|
+
return datetime.datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def shorten(text: str, width: int) -> str:
|
|
19
|
+
"""按显示宽度截断(CJK 字符按 2 算,简化处理)。"""
|
|
20
|
+
if text is None:
|
|
21
|
+
return ""
|
|
22
|
+
text = str(text).replace("\n", " ").replace("\r", " ").strip()
|
|
23
|
+
if width <= 0:
|
|
24
|
+
return text
|
|
25
|
+
if len(text) <= width:
|
|
26
|
+
return text
|
|
27
|
+
return text[: max(width - 1, 1)] + "…"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def first_incoming(emails):
|
|
31
|
+
"""从 emails 列表中找出首封 incoming 邮件(作为 session 的代表)。"""
|
|
32
|
+
if not emails:
|
|
33
|
+
return None
|
|
34
|
+
for e in emails:
|
|
35
|
+
if e.get("direction") == "incoming":
|
|
36
|
+
return e
|
|
37
|
+
return emails[0]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def cmd_session_list(handler):
|
|
41
|
+
"""列出所有 session。"""
|
|
42
|
+
sessions = handler.list_sessions()
|
|
43
|
+
if not sessions:
|
|
44
|
+
print("暂无 session")
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
rows = []
|
|
48
|
+
for s in sessions:
|
|
49
|
+
sid = s.get("session_id", "")
|
|
50
|
+
detail = handler.get_session_status(sid)
|
|
51
|
+
first = first_incoming(detail.get("emails", [])) if detail else None
|
|
52
|
+
from_email = first.get("from", "-") if first else "-"
|
|
53
|
+
subject = first.get("subject", "-") if first else "-"
|
|
54
|
+
rows.append({
|
|
55
|
+
"id": sid,
|
|
56
|
+
"from": from_email,
|
|
57
|
+
"subject": subject,
|
|
58
|
+
"last": fmt_ts(s.get("last_interaction")),
|
|
59
|
+
"count": s.get("email_count", 0),
|
|
60
|
+
"cwd": s.get("cwd", "") or "-",
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
header = f"{'SESSION ID':<14} {'FROM':<28} {'SUBJECT':<24} {'LAST INTERACTION':<17} {'MSGS':>4} CWD"
|
|
64
|
+
print(header)
|
|
65
|
+
print("-" * len(header))
|
|
66
|
+
for r in rows:
|
|
67
|
+
print(
|
|
68
|
+
f"{shorten(r['id'], 14):<14} "
|
|
69
|
+
f"{shorten(r['from'], 28):<28} "
|
|
70
|
+
f"{shorten(r['subject'], 24):<24} "
|
|
71
|
+
f"{r['last']:<17} "
|
|
72
|
+
f"{r['count']:>4} "
|
|
73
|
+
f"{shorten(r['cwd'], 40)}"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def cmd_session_show(handler, session_id: str):
|
|
78
|
+
"""查看单个 session 详情。"""
|
|
79
|
+
detail = handler.get_session_status(session_id)
|
|
80
|
+
if detail is None:
|
|
81
|
+
print(f"未找到 session: {session_id}", file=sys.stderr)
|
|
82
|
+
sys.exit(1)
|
|
83
|
+
|
|
84
|
+
emails = detail.get("emails", [])
|
|
85
|
+
first = first_incoming(emails)
|
|
86
|
+
from_email = first.get("from", "-") if first else "-"
|
|
87
|
+
subject = first.get("subject", "-") if first else "-"
|
|
88
|
+
|
|
89
|
+
print(f"Session: {detail.get('session_id', session_id)}")
|
|
90
|
+
print(f"From: {from_email}")
|
|
91
|
+
print(f"Subject: {subject}")
|
|
92
|
+
print(f"Created: {fmt_ts(detail.get('created_at'))}")
|
|
93
|
+
print(f"Last seen: {fmt_ts(detail.get('last_interaction'))}")
|
|
94
|
+
print(f"Cwd: {detail.get('cwd', '') or '-'}")
|
|
95
|
+
print(f"Messages: {detail.get('email_count', len(emails))}")
|
|
96
|
+
print()
|
|
97
|
+
print(f"Emails ({len(emails)}):")
|
|
98
|
+
if not emails:
|
|
99
|
+
print(" (空)")
|
|
100
|
+
return
|
|
101
|
+
for e in emails:
|
|
102
|
+
direction = e.get("direction", "?")
|
|
103
|
+
tag = "[in]" if direction == "incoming" else "[out]" if direction == "outgoing" else f"[{direction}]"
|
|
104
|
+
ts = fmt_ts(e.get("ts") or e.get("date"))
|
|
105
|
+
addr = e.get("from", "-")
|
|
106
|
+
body = shorten(e.get("body", ""), 60)
|
|
107
|
+
print(f" {tag:<5} {ts:<17} {shorten(addr, 28):<28} {body}")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def cmd_session_delete(handler, session_id: str, assume_yes: bool = False):
|
|
111
|
+
"""删除 session,含确认提示。"""
|
|
112
|
+
detail = handler.get_session_status(session_id)
|
|
113
|
+
if detail is None:
|
|
114
|
+
print(f"未找到 session: {session_id}", file=sys.stderr)
|
|
115
|
+
sys.exit(1)
|
|
116
|
+
|
|
117
|
+
if not assume_yes:
|
|
118
|
+
emails = detail.get("emails", [])
|
|
119
|
+
first = first_incoming(emails)
|
|
120
|
+
from_email = first.get("from", "-") if first else "-"
|
|
121
|
+
subject = first.get("subject", "-") if first else "-"
|
|
122
|
+
print(f"即将删除 session: {session_id}")
|
|
123
|
+
print(f" From: {from_email}")
|
|
124
|
+
print(f" Subject: {subject}")
|
|
125
|
+
print(f" Emails: {len(emails)}")
|
|
126
|
+
try:
|
|
127
|
+
confirm = input("确认删除? [y/N]: ").strip().lower()
|
|
128
|
+
except EOFError:
|
|
129
|
+
confirm = ""
|
|
130
|
+
if confirm not in ("y", "yes"):
|
|
131
|
+
print("已取消")
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
ok = handler.terminate_session(session_id)
|
|
135
|
+
if ok:
|
|
136
|
+
print(f"已删除 session: {session_id}")
|
|
137
|
+
else:
|
|
138
|
+
print(f"删除失败: {session_id}", file=sys.stderr)
|
|
139
|
+
sys.exit(1)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def cmd_session_cleanup(handler, dry_run: bool = False):
|
|
143
|
+
"""按 TTL 清理过期 session。"""
|
|
144
|
+
if dry_run:
|
|
145
|
+
count = handler._cleanup_expired_sessions(dry_run=True)
|
|
146
|
+
print(f"[dry-run] 将清理 {count} 个过期 session (实际未删除)")
|
|
147
|
+
else:
|
|
148
|
+
count = handler._cleanup_expired_sessions(dry_run=False)
|
|
149
|
+
print(f"已清理 {count} 个过期 session")
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from logging.handlers import RotatingFileHandler
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def setup_logging(log_file: Path = None, level: int = None):
|
|
9
|
+
if logging.root.handlers:
|
|
10
|
+
return logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
if level is None:
|
|
13
|
+
level = getattr(
|
|
14
|
+
logging,
|
|
15
|
+
os.environ.get("MAILCODE_LOG_LEVEL", "INFO").upper(),
|
|
16
|
+
logging.INFO,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
handlers = []
|
|
20
|
+
|
|
21
|
+
if log_file:
|
|
22
|
+
log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
handlers.append(
|
|
24
|
+
RotatingFileHandler(
|
|
25
|
+
log_file,
|
|
26
|
+
encoding="utf-8",
|
|
27
|
+
maxBytes=5 * 1024 * 1024,
|
|
28
|
+
backupCount=3,
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# 错误级别输出到 stderr,让用户看到出问题了
|
|
33
|
+
err_handler = logging.StreamHandler(sys.stderr)
|
|
34
|
+
err_handler.setLevel(logging.ERROR)
|
|
35
|
+
handlers.append(err_handler)
|
|
36
|
+
|
|
37
|
+
logging.basicConfig(
|
|
38
|
+
level=level,
|
|
39
|
+
format="%(asctime)s [%(levelname)s] %(message)s",
|
|
40
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
41
|
+
handlers=handlers,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
return logging.getLogger(__name__)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_logger(name: str = "mailcode"):
|
|
48
|
+
return logging.getLogger(name)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
mailcode/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
|
|
2
|
+
mailcode/cli.py,sha256=CAoTj0-0NOjmAM9oY06eHE3ME2gcTcFcFkYDuD6sJ7I,14566
|
|
3
|
+
mailcode/config.py,sha256=DOG_gsKospuNACVE7rnLWmuage97Ql9HWsH_IjQ0CP4,8810
|
|
4
|
+
mailcode/health.py,sha256=FuHN7pti2MVqWIkmc-jvEwtcxhOKINnaQU_GakBpIXI,4237
|
|
5
|
+
mailcode/provider_presets.py,sha256=Vp3Z4f9dbwwohiUAlquHfPneNPHS8s0kWfUCn5wAgAA,1598
|
|
6
|
+
mailcode/server.py,sha256=2tyjiVLWfDSbuqyM8QBi7rG4gGWtPjp515qPrgIzyNk,1327
|
|
7
|
+
mailcode/session_cli.py,sha256=wISpuV8DVQ6QNaaYbFV667vHo2VqWB33HKddqVWngu8,5050
|
|
8
|
+
mailcode/channels/__init__.py,sha256=6xYU1pODC7TKf1Eo63d2r966cAQLBy9BGEmYuJda6Vo,68
|
|
9
|
+
mailcode/channels/email_channel.py,sha256=vFZ1mjbrqHjny-MZ_QLIE4IHg50A9MI1CwX9CEY3KtE,4954
|
|
10
|
+
mailcode/relay/__init__.py,sha256=WoCELWxxBDDyOPUrTgWJG2Nzgeu_3oXIcS7a5_RTh4Q,83
|
|
11
|
+
mailcode/relay/conversation_handler.py,sha256=wxz45vTqlAIGugOsHnBA0Qr3ARSTt7eMxgiO8I3E-kc,22224
|
|
12
|
+
mailcode/relay/email_listener.py,sha256=FnxIms__zNLA2QSxVhQTtWJKW9T41wB691HzGE7bO70,21366
|
|
13
|
+
mailcode/relay/security.py,sha256=OTYYUpDMgLKevSnCwRGz1fstY0LM6OeL1UiP0uhekl8,4577
|
|
14
|
+
mailcode/relay/stateless_handler.py,sha256=iuUKl6b74zosUwVuaQa6oIEUB5Q1PAX4o0MDFjkJSFI,3560
|
|
15
|
+
mailcode/resources/default.json,sha256=SZCqNEoO5YoccnhmpwBr-dVPGr3r2qL85cLd9jwayQ0,1369
|
|
16
|
+
mailcode/utils/__init__.py,sha256=5P-WOO4zIxA73NqxZ0Nsj7VQ4dqmRDFtbUdjK8qdyUg,104
|
|
17
|
+
mailcode/utils/logging.py,sha256=ysYR3unsE22KRiWAAmhOsdOpPAXocmw4b9VMs2HQ1ac,1210
|
|
18
|
+
mailcode-0.1.0.dist-info/licenses/LICENSE,sha256=98Ovxs3O_GFWhTZsIB655ChirkNFaIyJSxrRKqWGKno,1063
|
|
19
|
+
mailcode-0.1.0.dist-info/METADATA,sha256=HE-FRnH2CPVT-9M_fyM-Sm4HO5VLbH8yfe7tZBkZx-8,196
|
|
20
|
+
mailcode-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
21
|
+
mailcode-0.1.0.dist-info/entry_points.txt,sha256=UKjtmVVTsKhv7M1Y2SY2_24n8FNEfVcC-5c96vuDOiY,47
|
|
22
|
+
mailcode-0.1.0.dist-info/top_level.txt,sha256=VCjkNSdOf7Zqo-FEVYMl96z5EtaTA2Qc1zjhGRgmZYc,9
|
|
23
|
+
mailcode-0.1.0.dist-info/RECORD,,
|
|
@@ -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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mailcode
|