ErisPulse-EmailAdapter 4.2.0__tar.gz → 4.3.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.
- {erispulse_emailadapter-4.2.0 → erispulse_emailadapter-4.3.0}/EmailAdapter/Core.py +104 -1
- {erispulse_emailadapter-4.2.0 → erispulse_emailadapter-4.3.0/ErisPulse_EmailAdapter.egg-info}/PKG-INFO +1 -1
- {erispulse_emailadapter-4.2.0/ErisPulse_EmailAdapter.egg-info → erispulse_emailadapter-4.3.0}/PKG-INFO +1 -1
- {erispulse_emailadapter-4.2.0 → erispulse_emailadapter-4.3.0}/pyproject.toml +1 -1
- {erispulse_emailadapter-4.2.0 → erispulse_emailadapter-4.3.0}/EmailAdapter/__init__.py +0 -0
- {erispulse_emailadapter-4.2.0 → erispulse_emailadapter-4.3.0}/ErisPulse_EmailAdapter.egg-info/SOURCES.txt +0 -0
- {erispulse_emailadapter-4.2.0 → erispulse_emailadapter-4.3.0}/ErisPulse_EmailAdapter.egg-info/dependency_links.txt +0 -0
- {erispulse_emailadapter-4.2.0 → erispulse_emailadapter-4.3.0}/ErisPulse_EmailAdapter.egg-info/entry_points.txt +0 -0
- {erispulse_emailadapter-4.2.0 → erispulse_emailadapter-4.3.0}/ErisPulse_EmailAdapter.egg-info/top_level.txt +0 -0
- {erispulse_emailadapter-4.2.0 → erispulse_emailadapter-4.3.0}/LICENSE +0 -0
- {erispulse_emailadapter-4.2.0 → erispulse_emailadapter-4.3.0}/README.md +0 -0
- {erispulse_emailadapter-4.2.0 → erispulse_emailadapter-4.3.0}/setup.cfg +0 -0
- {erispulse_emailadapter-4.2.0 → erispulse_emailadapter-4.3.0}/tests/test.py +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import email
|
|
3
|
+
import html as _html
|
|
3
4
|
import imaplib
|
|
4
5
|
import smtplib
|
|
5
6
|
import ssl
|
|
@@ -25,12 +26,79 @@ try:
|
|
|
25
26
|
except ImportError: # pragma: no cover
|
|
26
27
|
spawn_background = None
|
|
27
28
|
|
|
28
|
-
__version__ = "4.
|
|
29
|
+
__version__ = "4.3.0"
|
|
29
30
|
|
|
30
31
|
# 软依赖的框架最低版本(运行时检测,仅提示不强制)
|
|
31
32
|
MIN_FRAMEWORK_VERSION = (2, 7, 1)
|
|
32
33
|
|
|
33
34
|
|
|
35
|
+
def _forward_node_text(node: Dict) -> str:
|
|
36
|
+
"""提取合并转发节点 content 中全部 text 段文本(段间直接连接)"""
|
|
37
|
+
data = node.get("data") or {}
|
|
38
|
+
return "".join(
|
|
39
|
+
str(seg.get("data", {}).get("text") or "")
|
|
40
|
+
for seg in (data.get("content") or [])
|
|
41
|
+
if isinstance(seg, dict) and seg.get("type") == "text"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _forward_nodes_plain(nodes, separator: str = "\n") -> str:
|
|
46
|
+
"""合并转发节点降级纯文本:每条「昵称: 正文」"""
|
|
47
|
+
lines = []
|
|
48
|
+
for node in nodes if isinstance(nodes, list) else [nodes]:
|
|
49
|
+
if not isinstance(node, dict):
|
|
50
|
+
continue
|
|
51
|
+
data = node.get("data") or {}
|
|
52
|
+
nickname = str(data.get("nickname") or "").strip()
|
|
53
|
+
text = _forward_node_text(node).strip()
|
|
54
|
+
if nickname and text:
|
|
55
|
+
lines.append(f"{nickname}: {text}")
|
|
56
|
+
elif text:
|
|
57
|
+
lines.append(text)
|
|
58
|
+
return separator.join(lines)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _build_forward_html(nodes, title: str = "合并聊天记录") -> str:
|
|
62
|
+
"""渲染合并转发折叠 HTML 卡片(<details> 折叠 + CSS 系统色,自动适配明暗主题)"""
|
|
63
|
+
bubbles = []
|
|
64
|
+
for node in nodes if isinstance(nodes, list) else [nodes]:
|
|
65
|
+
if not isinstance(node, dict):
|
|
66
|
+
continue
|
|
67
|
+
data = node.get("data") or {}
|
|
68
|
+
nickname = str(data.get("nickname") or "")
|
|
69
|
+
text = _forward_node_text(node).strip()
|
|
70
|
+
if not text:
|
|
71
|
+
continue
|
|
72
|
+
body = _html.escape(text).replace("\n", "<br>")
|
|
73
|
+
bubbles.append(
|
|
74
|
+
'<div style="margin:8px 0;padding:10px 12px;border:1px solid GrayText;border-radius:8px;'
|
|
75
|
+
'background:Canvas;color:CanvasText;">'
|
|
76
|
+
'<div style="font-size:12px;line-height:1.4;margin-bottom:6px;color:GrayText;">'
|
|
77
|
+
f"<strong>{_html.escape(nickname)}</strong></div>"
|
|
78
|
+
'<div style="font-size:14px;line-height:1.6;word-break:break-word;color:CanvasText;">' + body + "</div></div>"
|
|
79
|
+
)
|
|
80
|
+
if not bubbles:
|
|
81
|
+
return ""
|
|
82
|
+
return (
|
|
83
|
+
'<div style="color:CanvasText;color-scheme:light dark;">'
|
|
84
|
+
'<details style="width:100%;box-sizing:border-box;border:1px solid GrayText;border-radius:10px;'
|
|
85
|
+
'background:Canvas;color:CanvasText;overflow:hidden;">'
|
|
86
|
+
'<summary style="list-style:none;cursor:pointer;padding:10px 12px;background:Canvas;'
|
|
87
|
+
'border-bottom:1px solid GrayText;font-size:14px;font-weight:600;color:CanvasText;">'
|
|
88
|
+
f"{_html.escape(title)}</summary>"
|
|
89
|
+
'<div style="padding:8px 10px;">' + "".join(bubbles) + "</div></details></div>"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _wrap_email_html(body: str) -> str:
|
|
94
|
+
"""包装为完整 HTML 文档(声明 charset,避免邮件客户端乱码)"""
|
|
95
|
+
return (
|
|
96
|
+
'<!DOCTYPE html><html><head><meta charset="utf-8">'
|
|
97
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1">'
|
|
98
|
+
"</head><body>" + body + "</body></html>"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
34
102
|
@dataclass
|
|
35
103
|
class EmailGlobalConfig(BaseConfig):
|
|
36
104
|
"""Email 适配器全局配置(适用于所有账户的默认值)"""
|
|
@@ -255,6 +323,32 @@ class EmailAdapter(BaseAdapter):
|
|
|
255
323
|
self._html = html
|
|
256
324
|
return asyncio.create_task(self._send())
|
|
257
325
|
|
|
326
|
+
def MergeForward(self, nodes, title: str = "合并聊天记录"):
|
|
327
|
+
"""发送合并转发邮件(正文为折叠 HTML 卡片)
|
|
328
|
+
|
|
329
|
+
:param nodes: go-cqhttp 扩展的 OneBot v11 合并转发节点数组(或单节点 dict)
|
|
330
|
+
:param title: 折叠卡标题(未设置主题时同时作为邮件主题)
|
|
331
|
+
:return: asyncio.Task
|
|
332
|
+
"""
|
|
333
|
+
if isinstance(nodes, dict):
|
|
334
|
+
nodes = [nodes]
|
|
335
|
+
return asyncio.create_task(
|
|
336
|
+
self._do_send_merge_forward(
|
|
337
|
+
[n for n in nodes if isinstance(n, dict)], title
|
|
338
|
+
)
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
async def _do_send_merge_forward(self, nodes, title: str = "合并聊天记录"):
|
|
342
|
+
html = _build_forward_html(nodes, title)
|
|
343
|
+
if not html:
|
|
344
|
+
return self._adapter.make_error(
|
|
345
|
+
retcode=10006, message="MergeForward: 节点无有效文本内容"
|
|
346
|
+
)
|
|
347
|
+
if not self._subject:
|
|
348
|
+
self._subject = title
|
|
349
|
+
self._html = _wrap_email_html(html)
|
|
350
|
+
return await self._send()
|
|
351
|
+
|
|
258
352
|
def Text(self, text: str):
|
|
259
353
|
"""设置纯文本内容并发送邮件"""
|
|
260
354
|
self._text = text
|
|
@@ -297,6 +391,7 @@ class EmailAdapter(BaseAdapter):
|
|
|
297
391
|
"video",
|
|
298
392
|
"file",
|
|
299
393
|
"audio",
|
|
394
|
+
"node",
|
|
300
395
|
]:
|
|
301
396
|
segments = [message]
|
|
302
397
|
elif isinstance(message, list):
|
|
@@ -306,6 +401,12 @@ class EmailAdapter(BaseAdapter):
|
|
|
306
401
|
"Invalid message format, expected OneBot12 message segment or array"
|
|
307
402
|
)
|
|
308
403
|
|
|
404
|
+
# node 段解析:纯 node 列表自动路由合并转发邮件;混合列表降级为纯文本
|
|
405
|
+
if segments and all(
|
|
406
|
+
isinstance(seg, dict) and seg.get("type") == "node" for seg in segments
|
|
407
|
+
):
|
|
408
|
+
return await self._do_send_merge_forward(segments)
|
|
409
|
+
|
|
309
410
|
# 解析消息段
|
|
310
411
|
for segment in segments:
|
|
311
412
|
seg_type = segment.get("type")
|
|
@@ -313,6 +414,8 @@ class EmailAdapter(BaseAdapter):
|
|
|
313
414
|
|
|
314
415
|
if seg_type == "text":
|
|
315
416
|
self._text += data.get("text", "")
|
|
417
|
+
elif seg_type == "node":
|
|
418
|
+
self._text += _forward_nodes_plain(segment)
|
|
316
419
|
elif seg_type == "image":
|
|
317
420
|
self._add_attachment_from_segment(data, "image")
|
|
318
421
|
elif seg_type == "video":
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|