fix(unsub): List-Unsubscribe 头兜底 + mailto 退订按钮放行

两封推广邮件无退订按钮,两个独立根因:
1. Cloudflare 邮件 (msg_id=1364):程序只喂纯文本给 AI,纯文本里
   'Unsubscribe' 无 URL;邮件头 List-Unsubscribe 的权威退订链接被丢弃。
   -> 解析 List-Unsubscribe 头 (RFC 2369) 存入 Email.list_unsubscribe,
      注入 AI 提示词;显示层新增 _ensure_unsub_link 兜底:AI 漏提取时
      直接用头部 http(s)/mailto 链接补退订按钮(不依赖 AI)。
2. InferX 邮件 (msg_id=1363):AI 提取到 mailto:team@inferx.net?subject=Unsubscribe,
   但 _link_buttons 只放行 http 协议把它过滤掉。
   -> URL 校验放行 mailto:(退订场景常见)。

send_summary / tg_send_and_mark 均应用兜底并写回 summary_data.links,
回调恢复视图也能显示退订按钮。新增 6 个测试,共 14 个全部通过。
This commit is contained in:
2026-09-04 08:03:45 +08:00
parent 8a471cb489
commit f65e67c2f2
5 files changed
+120 -14

No files matched your search

+46 -2
View File
@@ -1,6 +1,11 @@
"""测试摘要键盘渲染:推广邮件的退订链接按钮必须显示且排最前。"""
from src.tg_bot import _is_unsub_link, _link_buttons, _summary_keyboard
from src.tg_bot import (
_ensure_unsub_link,
_is_unsub_link,
_link_buttons,
_summary_keyboard,
)
def _rows_of(kb: dict) -> list[list[dict]]:
@@ -102,4 +107,43 @@ def test_oversized_url_filtered():
url = "https://example.com/" + "x" * 2100
links = [{"text": "退订", "url": url}]
rows = _link_buttons(links)
assert rows == []
assert rows == []
def test_mailto_unsub_link_rendered():
"""mailto: 退订链接(如 InferX 的 mailto:team@inferx.net?subject=Unsubscribe)必须渲染"""
links = [{"text": "退订", "url": "mailto:team@inferx.net?subject=Unsubscribe"}]
kb = _summary_keyboard(can_reply=False, is_promotion=True, links=links)
rows = kb["inline_keyboard"]
assert rows[0][0]["url"] == "mailto:team@inferx.net?subject=Unsubscribe"
assert "🔕" in rows[0][0]["text"]
assert _is_unsub_link(links[0])
def test_ensure_unsub_link_http_header_fallback():
"""AI 没提取到退订链接时,用 List-Unsubscribe 头的 http 链接兜底"""
header = "https://links.iterable.com/s/uh/tKjpy4SHCKvbzE2votNyZEJDvlMdeodVyWFExNs310ugJ_RvZtoZq4gGLQ0BXMSL2lf3kEZsmr1XJmrODIFHpEwcypo3LR096pZ7jsfBynZBgGWz171Wc1E6ZIIC5Lu0e-Stq6p5-_Ad3CnNlUWrW1tnrLSeiGsOiQG_zMYKVbtfQZgvfm3QYfD9lQ/Cey29KU8yOzIUltaEJA8EMRFf-FKMZQy/24"
links = _ensure_unsub_link([], header)
assert links == [{"text": "退订", "url": header}]
kb = _summary_keyboard(can_reply=False, is_promotion=True, links=links)
assert kb["inline_keyboard"][0][0]["url"] == header
def test_ensure_unsub_link_mailto_header_fallback():
"""List-Unsubscribe 头只有 mailto 时也兜底"""
header = "mailto:unsubscribe+19693436+25822730@unsubscribe.iterable.com"
links = _ensure_unsub_link([], header)
assert links == [{"text": "退订", "url": header}]
def test_ensure_unsub_link_no_duplicate_when_ai_extracted():
"""AI 已提取退订链接时,不重复添加头部链接"""
ai_links = [{"text": "取消订阅", "url": "https://r.openai.com/asm/unsubscribe/?x=1"}]
links = _ensure_unsub_link(ai_links, "https://links.example.com/unsub")
assert links == ai_links
def test_ensure_unsub_link_ignores_invalid_header():
"""头部无有效链接时不兜底"""
assert _ensure_unsub_link([], "") == []
assert _ensure_unsub_link([], "javascript:alert(1)") == []