用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/QianJinGuo/wiki --skill smtp-via-proxy命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
正在显示 SKILL.md
基于 SOC 职业分类
| name | smtp-via-proxy |
| description | Send email via Gmail SMTP and read email via Gmail IMAP through HTTP CONNECT proxy (xray/v2rayN) |
| version | 1.4.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 config, but works via http_proxy/https_proxy env vars (confirmed 2026-06-18 — see P20)All 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/QUIT. Auto-detects HTML vs plain text (Content-Type based on body prefix). Fixed 2026-07-04: was hardcoded to text/plain — now auto-detects <!DOCTYPE/<html → text/html.send-html-mail.py (2026-07-04): HTML-only sender for Outlook-compatible HTML. Base64-encoded body, proper MIME construction, EHLO multi-line response handling. Location: ~/wiki/scripts/send-html-mail.py. Usage: python3 send-html-mail.py --to x@y.com --subject "Subject" file.htmlsend-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-html-mail.py (added 2026-07-04): Full MIME-compliant HTML email sender. Uses base64 Content-Transfer-Encoding for safe Chinese character transmission. Includes proper multi-line EHLO response handling (send-mail.py's recv_line_TIMEOUT silent read silently drops continuation lines). Preferred for HTML email delivery.# CORRECT: pass file as positional arg (avoids tirith:pipe_to_interpreter block)
python3 ~/wiki/scripts/send-report.py ~/wiki/cron-report.md
python3 ~/wiki/scripts/send-mail.py --to recipient@gmail.com --subject 'Subject' body.txt
# WRONG: pipe gets blocked by tirith in cron mode (HIGH severity)
cat ~/wiki/cron-report.md | python3 ~/wiki/scripts/send-report.py # ✗ BLOCKED
# WRONG: stdin redirect works but may hit timeout if proxy is down
python3 ~/wiki/scripts/send-report.py < report.txt # ✓ works, but no fallback
Key rule: Always pass the file path as a positional argument to send-report.py. The script supports it (line 14-16). Never use cat | python3 in cron mode.
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 |
Outdated (2026-06-18): himalaya template send works as a third fallback — see P20 | |
| 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)Run the diagnostic script to test all paths at once:
bash ~/.hermes/skills/devops/smtp-via-proxy/scripts/smtp-diagnostic.sh
Or test manually:
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.
Symptom: CONNECT 200 success, then SSL handshake timeout. Cause: Intermittent proxy/network issue — not code defect. Fix: Try direct connection first (P21), then retry 3× with backoff, then fallback to port 587.
Confirmed (2026-06-30): SOCKS5 proxy also fails at the same SSL handshake step — the tunnel establishes (0500 auth, 050000017f0000012a38 connect), but ssl.wrap_socket() times out. This confirms the issue is TLS-level (not proxy-protocol-level). Increasing the socket timeout from 15s to 30s does NOT help — the handshake hangs indefinitely regardless of timeout.
Confirmed (2026-07-01): When proxy SSL handshake hangs on port 465, direct connection to smtp.gmail.com:465 works (TLSv1.3, full SMTP conversation succeeds). The proxy tunnel establishes but the TLS negotiation through it stalls — this is a proxy-side TLS forwarding issue, not a network-level SMTP block.
Diagnosis shortcut: When port 465 implicit TLS hangs through the proxy, skip retrying and go directly to direct smtplib (P21). The 3× retry with backoff wastes time on a confirmed-dead path.
Symptom: You pipe HTML content to send-mail.py, the email sends successfully, but the recipient sees raw HTML source instead of rendered content.
Root cause: send-mail.py constructs the SMTP DATA payload with Content-Type: text/plain; charset=UTF-8 as a hardcoded string. Fixed 2026-07-04 to auto-detect <!DOCTYPE/<html prefix → set text/html.
Fix: Use send-html-mail.py instead (proper MIME: base64 Content-Transfer-Encoding, multi-line EHLO handling):
python3 ~/wiki/scripts/send-html-mail.py --to user@example.com --subject "Subject" file.html
send-html-mail.py was created 2026-07-04 specifically for Outlook-compatible HTML email delivery. It handles base64 encoding, multi-line EHLO responses, and Content-Type correctly.
Symptom: You pipe HTML content to send-mail.py (or call it with send-report.py), the email sends successfully, but the recipient sees raw HTML source in their email client instead of rendered content.
Root cause: send-mail.py constructs the SMTP DATA payload with Content-Type: text/plain; charset=UTF-8 as a hardcoded string. It does NOT auto-detect HTML content. Fixed 2026-07-04 to auto-detect, but send-html-mail.py is the preferred path for HTML.
Fix: Use send-html-mail.py instead:
python3 ~/wiki/scripts/send-html-mail.py --to user@example.com --subject "Subject" /path/to/file.html
If you must use send-mail.py: Ensure the body starts with <!DOCTYPE or <html> (the auto-detection patch checks these prefixes). Verify with:
head -1 /tmp/report.html
# Must start with <!DOCTYPE or <html
Pitfall: Even with correct Content-Type: text/html, send-mail.py declares Content-Transfer-Encoding: quoted-printable for HTML content but does NOT actually QP-encode the body — the raw HTML (with Chinese characters, long lines, emoji) is sent as-is. Email clients see the QP declaration but receive non-QP content → rendering fails (raw HTML source shown, garbled text, or blank body).
Fix: Change to Content-Transfer-Encoding: 8bit (correct for raw UTF-8 HTML). QP requires quopri / base64 encoding of the body before sending. Unless you properly encode, 8bit is the only safe choice:
# WRONG (declares QP, sends raw HTML — renders incorrectly):
cte = 'quoted-printable' if content_type == 'text/html' else '8bit'
# CORRECT (sends raw UTF-8 HTML, renders correctly on all major clients):
cte = '8bit'
Detection: The email arrives in the inbox but the HTML is not rendered — you see raw <html><body>...</body></html> tags instead of the rendered page, or the email appears blank. Check the email source for Content-Transfer-Encoding: quoted-printable followed by non-QP-encoded content (no = sign encoding, raw Chinese text).
Verified 2026-07-24: send-mail.py line 147 had cte = 'quoted-printable' if content_type == 'text/html' else '8bit'. Fixed to always use 8bit. All subsequent HTML emails rendered correctly.
Symptom: TLS + EHLO success, AUTH LOGIN hangs. Fix: Same fallback chain as P19.
Diagnosis — ALWAYS check first before retrying proxy paths:
nc -z -w 3 127.0.0.1 10808 2>&1 && echo "PROXY UP" || echo "PROXY DOWN"
If PROXY DOWN:
send-report.py, send-report-587.py with proxy env vars, and himalaya if http_proxy/https_proxy are set)smtplib (no proxy env vars) — Gmail ports 587/465 are directly reachable on this host even when the proxy is downimport smtplib, subprocess
from email.mime.text import MIMEText
password = subprocess.run(
['security', 'find-generic-password', '-a', 'geekqjg@gmail.com',
'-s', 'himalaya-smtp', '-w'], capture_output=True, text=True
).stdout.strip()
body = open('/Users/jinguo/wiki/cron-report.md').read()
msg = MIMEText(body, 'plain', 'utf-8')
msg['Subject'] = '[Hermes] Wiki 状态报告'
msg['From'] = 'jinguo <geekqjg@gmail.com>'
msg['To'] = 'geekqjg@gmail.com'
# Direct connection — NO proxy env vars, NO socket.create_connection override
with smtplib.SMTP('smtp.gmail.com', 587, timeout=30) as server:
server.ehlo()
server.starttls()
server.ehlo()
server.login('geekqjg@gmail.com', password)
server.send_message(msg)
Why himalaya also fails when proxy is down: Himalaya respects http_proxy/https_proxy env vars. If those are set in the environment (or injected by a wrapper), himalaya routes through the dead proxy and times out. The send-report.py script uses explicit socket-level CONNECT proxy routing. Both fail when the proxy port is unreachable.
Key insight (updated 2026-06-29): On this host, direct connections to smtp.gmail.com:587 and :465 have historically been reachable when the local xray proxy is down. However, this is NOT guaranteed — on 2026-06-29, direct connections also timed out even with the proxy UP. The direct-smtplib path may fail due to ISP-level SMTP port blocking, Google rate-limiting the source IP, or transient network issues. Always test direct reachability (nc -z -w 5 smtp.gmail.com 587) before assuming P21 will work.
Symptom: Proxy CONNECT succeeds (HTTP 200), but SSL handshake to port 465 hangs. Port 587 via proxy also times out. Direct connections to smtp.gmail.com:587/465 timeout too. All fallback paths exhausted.
Root cause: Unclear — could be proxy exit IP blocked by Google, ISP-level SMTP port filtering, or Google throttling. The proxy is functional for HTTP/HTTPS web traffic but SMTP TLS negotiation fails through it.
Diagnosis:
# 1. Confirm proxy is up
nc -z -w 3 127.0.0.1 10808 && echo "PROXY UP"
# 2. Test direct SMTP reachability
nc -z -w 5 smtp.gmail.com 587 && echo "DIRECT 587 OK" || echo "DIRECT 587 BLOCKED"
nc -z -w 5 smtp.gmail.com 465 && echo "DIRECT 465 OK" || echo "DIRECT 465 BLOCKED"
# 3. Test proxy SSL handshake (the failing step)
python3 -c "
import socket, ssl
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))
ctx = ssl.create_default_context()
try:
ctx.wrap_socket(s, server_hostname='smtp.gmail.com')
print('SSL OK')
except Exception as e:
print(f'SSL FAILED: {e}')
"
When all paths fail: Log the failure honestly in cron-status.log and save the report locally. Do NOT retry indefinitely — the issue is likely network-level and won't resolve by retrying. Flag for manual investigation (proxy node switch, ISP issue, Google account security).
Do this FIRST, before trying ANY email-sending tool:
nc -z -w 3 127.0.0.1 10808 2>&1 && echo "PROXY UP" || echo "PROXY DOWN"
Then follow the chain:
send-report-587.py, then himalaya.smtplib (P21 above) immediately.himalaya message send (bypasses proxy for SMTP — see pitfalls). If himalaya also fails, log failure, save report locally, flag for manual investigation. Do NOT retry.Anti-pattern (DO NOT): Wasting time trying himalaya, Cloudflare Email Service REST API, or other elaborate alternatives when the proxy is down. The direct smtplib approach in P21 works in ~2 seconds — use it.
http_proxy/https_proxy environment variables.socket.create_connection is used by many libraries; be specific about the patch lifetime (or restore after done) to avoid side effects.himalaya blocked by dead proxy: If http_proxy/https_proxy are set and proxy is down, himalaya will timeout. Either unset the env vars or use direct smtplib.message send folder error: If himalaya message send fails with "Folder doesn't exist" on [Gmail]/Sent Mail, this is a himalaya bug with IMAP folder alias resolution for the Sent-copy save. The email IS sent — the SMTP send completes before the IMAP folder-save fails (confirmed via --debug logs 2026-06-27: sending smtp message appears before the IMAP error). Gmail also auto-saves sent mail server-side. Verify with himalaya envelope list --folder "[Gmail]/Sent Mail". No need to fall back to smtplib for sending — only the local Sent copy is lost.himalaya message send (not himalaya send). It requires RFC 2822 headers (From/To/Subject) piped via stdin.send-mail.py (which explicitly connects through the HTTP CONNECT proxy on port 465), himalaya connects directly to smtp.gmail.com:587 with STARTTLS — no proxy involved. This makes himalaya a reliable fallback when the proxy is flaky (but not when http_proxy/https_proxy env vars are set and proxy is down, as himalaya respects those env vars).wrangler will also timeout. Not a useful fallback for proxy-down scenarios.For automated reports sent via email, use templates/daily-report.html as the base template. Outlook-compatible (table layout, inline CSS). Replace {{PLACEHOLDER}} vars with data. Colors: green (#2e7d32) OK, red (#c62828) error, orange (#e65100) warning, gray (#9e9e9e) paused.
~/.config/himalaya/config.toml → display-name = "..." (used by himalaya CLI sends)~/wiki/scripts/send-mail2.py → msg['From'] = f'... <{from_addr}>'~/wiki/scripts/send-html-mail.py → f'From: ... <{from_addr}>\r\n'
When the user asks to change their sender name, update ALL THREE, then verify no stale name remains with grep -rn "Old Name" ~/.config/himalaya ~/wiki/scripts/. Example: 2026-08-02 "Jing Guo" → "jinguo" required all three edits (the exact line numbers: himalaya config.toml line 3, send-mail2.py line 71, send-html-mail.py line 28). Note: the Gmail web UI account name is a separate Google Account profile setting (myaccount.google.com → Personal info → Name), not editable from local config — tell the user this distinction.msg['From'] = 'jinguo <geekqjg@gmail.com>' — update it in the same pass whenever the display name changes.