用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/QianJinGuo/wiki --skill smtp-via-proxy命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
精选高价值 RSS feeds 扫描,输出到 raw/rss-inbox/ 暂存区。只保留有独立知识深度的 feed(非 digest 类),不再自动入库。包含 rss-inbox-curl-recovery.py 绕过 blogwatcher read-state 漏抓的兜底。
Meta-orchestrator that wires web-content-reviewer, llm-wiki, and wiki-evolver into a single four-phase knowledge pipeline (Triage → Gate → Store → Evolve). Use as the single entry point for all knowledge base operations. Includes a 6-URL-validated user-pasted WeChat URL fast path, three-axis dedup decision matrix (NEW/MERGE/DEDUP), orphan-raw detection protocol, and sibling-subagent race V7 evidence, and a two-variant V6 mid-write fix for matching vs different-slug duplicates.
从 Gmail 中提取 TLDR AI 等 newsletter 的链接,写入 raw/email-inbox/candidates.md,供后续 inbox-screener 评分。只做链接不做评分,与 inbox-screener 配合使用。
基于 SOC 职业分类
正在显示 SKILL.md
| name | smtp-via-proxy |
| description | Send email via Gmail SMTP and read email via Gmail IMAP through HTTP CONNECT proxy (xray/v2rayN) |
| version | 1.2.0 |
| category | devops |
| author | Hermes Agent |
Access Gmail SMTP and IMAP through an HTTP CONNECT proxy when direct connections time out (e.g., behind the Great Firewall).
smtp.gmail.com:587, imap.gmail.com:993) time outhimalaya CLI has no built-in proxy supportAll traffic routes through a local HTTP CONNECT proxy (127.0.0.1:10808, xray/v2rayN):
send-mail.pysocket.create_connection so imaplib.IMAP4_SSL transparently uses the proxy127.0.0.1:10808 (xray)himalaya-smtp (for SMTP) and himalaya-imap (for IMAP)security find-generic-password -a geekqjg@gmail.com -s himalaya-smtp -wUse port 465 (implicit TLS) instead of 587 (STARTTLS). Port 465 works because the TLS handshake happens immediately without a prior cleartext STARTTLS command.
send-mail.py: Full SMTP conversation — TCP connect to proxy, HTTP CONNECT to smtp.gmail.com:465, SSL wrap, EHLO/AUTH/MAIL/DATA/QUITsend-report.py: Thin cron wrapper — pipes stdin as email body, calls send-mail.pysend-report-587.py (in scripts/): P19 fallback — port 587 + STARTTLS via stdlib smtplib with https_proxy env var. Reuses the same keychain entry. Invoke directly when the 465 path hangs.# Send email with body from file
python3 ~/wiki/scripts/send-mail.py --to recipient@gmail.com --subject 'Subject' < body.txt
# Cron-compatible wrapper
python3 ~/wiki/scripts/send-report.py < report.txt
socket.connect(('127.0.0.1', 10808))b'CONNECT smtp.gmail.com:465 HTTP/1.1\r\nHost: smtp.gmail.com:465\r\n\r\n'HTTP/1.1 200 Connection establishedssl.wrap_socket(sock)\r\n.\r\n), QUIT| Issue | Fix |
|---|---|
| Port 587 STARTTLS fails through proxy | Use port 465 (implicit TLS) instead |
| SMTP end-of-data marker | Must use \r\n.\r\n (CRLF-dot-CRLF) |
| Body newlines need conversion | Convert \n to \r\n before sending |
| himalaya cannot use this approach | Uses its own SMTP library without proxy hooks |
| recv timeout with byte-by-byte reads | Use buffered reads (recv 1024 at a time) |
Monkey-patch socket.create_connection to route through the proxy before creating an imaplib.IMAP4_SSL instance. This transparently makes imaplib.IMAP4_SSL use the proxy without modifying imaplib's code.
~/wiki/scripts/newsletter-tldr-extractor.py contains the IMAP proxy solution.
import socket
def proxy_connect(host, port, proxy_host='127.0.0.1', proxy_port=10808, timeout=30):
"""Connect to host:port via HTTP CONNECT proxy. Returns raw socket (not SSL-wrapped)."""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
sock.connect((proxy_host, proxy_port))
connect_req = f'CONNECT {host}:{port} HTTP/1.1\r\nHost: {host}:{port}\r\n\r\n'
sock.sendall(connect_req.encode())
response = sock.recv(4096)
if b'200' not in response:
raise ConnectionError(f'Proxy CONNECT failed: {response[:200]}')
# Return WITHOUT SSL wrapping — caller (IMAP4_SSL) handles that
return sock
# Monkey-patch: substitute create_connection so IMAP4_SSL transparently proxies
orig_create_connection = socket.create_connection
def patched_create_connection(address, **kwargs):
host, port = address
return proxy_connect(host, port)
socket.create_connection = patched_create_connection
# Now IMAP4_SSL uses the proxy transparently
import imaplib
M = imaplib.IMAP4_SSL('imap.gmail.com', 993)
proxy_connect() returns the raw socket before SSL wrapping — IMAP4_SSL handles the TLS handshake itselfIMAP4_SSL instanceM.login(), M.select(), M.search(), M.fetch() all work normally\\Deleted flagged messages do NOT appear in search results (even if not yet expunged)import socket
s = socket.socket()
s.settimeout(10)
s.connect(('127.0.0.1', 10808))
s.sendall(b'CONNECT smtp.gmail.com:465 HTTP/1.1\r\nHost: smtp.gmail.com:465\r\n\r\n')
print(s.recv(4096))
Expected: b'HTTP/1.1 200 Connection established'
Update wiki-inbox-scan cron job prompt to use send-report.py and IMAP scripts that incorporate the monkey-patch.
When port 465 (implicit TLS) times out during SSL handshake through HTTP CONNECT proxy, try port 587 with environment variables:
import smtplib
import ssl
import subprocess
from email.mime.text import MIMEText
# Set proxy env vars BEFORE creating SMTP connection
import os
os.environ['http_proxy'] = 'http://127.0.0.1:10808'
os.environ['https_proxy'] = 'http://127.0.0.1:10808'
# Get password from keychain
result = subprocess.run(
['security', 'find-generic-password', '-a', 'geekqjg@gmail.com', '-s', 'himalaya-smtp', '-w'],
capture_output=True, text=True
)
password = result.stdout.strip()
body = open('report.md').read()
msg = MIMEText(body, 'plain', 'utf-8')
msg['Subject'] = 'Subject'
msg['From'] = 'geekqjg@gmail.com'
msg['To'] = 'geekqjg@gmail.com'
context = ssl.create_default_context()
with smtplib.SMTP('smtp.gmail.com', 587, timeout=30) as server:
server.ehlo()
server.starttls(context=context)
server.ehlo()
server.login('geekqjg@gmail.com', password)
server.send_message(msg)
http_proxy/https_proxy environment variables as shown above.socket.create_connection is used by many libraries; be specific about the patch lifetime (or restore after done) to avoid side effects.症状: 调 python3 ~/wiki/scripts/send-report.py < report.md 跑 send-mail.py 时:
[DEBUG] Sending CONNECT request...
[DEBUG] Proxy response: HTTP/1.1 200 Connection established
[DEBUG] TCP tunnel to smtp.gmail.com:465 established
ssl.SSLError: The handshake operation timed out
at send-mail.py:112 — context.wrap_socket(sock, server_hostname=SMTP_HOST)
Cron context (wiki-inbox-scan-v2 等): 看到上面这个 stderr 模式,第一步就自动 invoke ~/wiki/scripts/send-report-587.py,不要先 retry 5 次再 fallback —— 587 脚本无副作用(同读 ~/wiki/cron-report.md、同发 geekqjg@gmail.com、同主题),多一次 retry 至少 75s。详见 wiki-auto-pipeline-recipes SKILL.md v1.10 pitfalls 表的 P19 fallback 链说明。
HTTP CONNECT 自身成功(200 Connection established),隧道建立,但在已建立的隧道之上跑 TLS 握手时挂死。这是间歇性代理/出口网络问题——不是代码缺陷。
真实案例 (2026-06-07 10:19 CST):
wiki-inbox-scan-v2 cron 触发,4 次重试 × 5–30s 退避后全部复现同一错误cron-status.log:1203)send-mail.py:91-112 的逻辑和凌晨 02:00 跑的成功版本逐字节相同根因(HTTP CONNECT 代理 + implicit TLS 的特性):
解决(按顺序执行,每步有明确判据):
第一步:retry-with-backoff (3-5 次) — 同一脚本,同一参数:
for i in 1 2 3 4 5; do
python3 ~/wiki/scripts/send-report.py < ~/wiki/cron-report.md && break
echo "[retry $i/5] sleep $((i*15))s then retry..."
sleep $((i*15))
done
退避序列:15s → 30s → 45s → 60s → 75s(总耗时最多 ~3.5 分钟)。5 次都失败再进入 fallback。
实际观察 (2026-06-14 10:10 CST): 3 次重试 × 20-40s 退避(for i in 1 2 3; sleep $((i*20)),总 ~1 分 40 秒)就足以判断是稳态失败而非瞬时抖动。如果 3 次以内都失败,直接进 fallback,不必等满 5 次——同一节点、同一路径的失败模式不会因为第 4、第 5 次而改变,反而浪费 ~2 分钟 cron 时间窗。
第二步:port 587 STARTTLS fallback — 用 stdlib smtplib + 走 http_proxy/https_proxy 环境变量,让代理自己处理协议协商(不再需要应用层 CONNECT)。直接调用 ~/wiki/scripts/send-report-587.py(已签入 skill scripts/ 目录),它会读 ~/wiki/cron-report.md 并发送到 geekqjg@gmail.com。如果需要自定义主题/收件人,下面的内联版本可直接修改:
import os, smtplib, ssl, subprocess
from email.mime.text import MIMEText
os.environ['http_proxy'] = 'http://127.0.0.1:10808'
os.environ['https_proxy'] = 'http://127.0.0.1:10808'
password = subprocess.run(
['security', 'find-generic-password', '-a', 'geekqjg@gmail.com',
'-s', 'himalaya-smtp', '-w'], capture_output=True, text=True
).stdout.strip()
body = open(os.path.expanduser('~/wiki/cron-report.md')).read()
msg = MIMEText(body, , )
msg[] =
msg[] =
msg[] =
ctx = ssl.create_default_context()
smtplib.SMTP(, , timeout=) s:
s.ehlo(); s.starttls(context=ctx); s.ehlo()
s.login(, password)
s.send_message(msg)
经验法则: P19 失败 = "CONNECT 200 + TLS hang" 的精确模式。如果是其他 smtp 错误(Authentication failed、530 Must issue STARTTLS、421 Service not available 等),不是 P19,是别的——查 SMTP RFC 5321 + Gmail SMTP 错误码。
预防性检查清单:
https_proxy env?→ 不是手写 socket关键差异:smtplib + 走 http_proxy env → Python urllib 内部做 CONNECT,而 urllib 的 CONNECT 处理通常比裸 socket 路径更宽容。不要自己手写 socket+CONNECT+SSL 的 fallback,重复发明 P19 失败路径。
第三步:承认失败 + 诚实记录 — 5 次重试 + 587 fallback 都失败后:
~/wiki/cron-status.log(write_file 写 entry 到 /tmp + cat >> log,避免 P6 触发 + P13 误删)[SILENT]:cron 的 deliver 目标是用户邮箱,邮件失败本身就是要被看到的事件部分成功的诚实写法(465 失败 + 587 成功的情况):日志必须同时记录两条链路,让下一次回看时能立刻看出哪条路径退化了。模板:
[YYYY-MM-DD HH:MM:SS TZ] wiki-inbox-scan-v2 run started
- heartbeat: wiki-inbox-scan-v2 → /Users/jinguo/wiki/heartbeat/wiki-inbox-scan-v2.last-run
- file counts: entities=N, rss-inbox=N, wechat-inbox=N, articles=N
- report file: /Users/jinguo/wiki/cron-report.md (N bytes)
- email send: port 465 via proxy → P19 (N retries × Xs backoff all failed: TLS handshake hang after CONNECT 200)
- email send: port 587 + STARTTLS + https_proxy fallback → SUCCESS (sent to geekqjg@gmail.com)
不要省略 465 那行只写 SUCCESS——下次如果 587 也开始挂,从日志能立刻判断是哪条路径先退化、是否需要重启代理或换节点。