Skip to content

fix(email): SMTP SSL + IPv4 + HTML email support (#11941) - #46626

Closed
swissly wants to merge 4 commits into
NousResearch:mainfrom
swissly:fix/email-smtp-ssl-ipv4
Closed

fix(email): SMTP SSL + IPv4 + HTML email support (#11941)#46626
swissly wants to merge 4 commits into
NousResearch:mainfrom
swissly:fix/email-smtp-ssl-ipv4

Conversation

@swissly

@swissly swissly commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes email adapter reliability issues and adds HTML email support with proper configuration.

Changes

1. SMTP SSL & IPv4 Fix (closes #11941)

  • Use SMTP_SSL for port 465 connections (instead of SMTP + starttls)
  • Force IPv4 socket connections to prevent IPv6 hangs
  • Thread-safe _connect_smtp_socket() method

2. HTML Email Support (optional)

  • Convert Markdown bodies to styled HTML with inline CSS
  • multipart/alternative emails (plain text + HTML)
  • Config toggle: platforms.email.html_format: true (default: enabled)
  • Gmail/Outlook compatible inline styles
  • Graceful fallback: HTML conversion failure → plain text only

3. Security Note
Added code comment explaining why we don't sanitize HTML:

  • Hermes generates its own email bodies (LLM output, not user input)
  • Emails are sent to the configured address (self-to-self)
  • Config toggle provides kill-switch: html_format: false

Configuration

platforms:
  email:
    html_format: true    # enable HTML emails (default: true)
    skip_attachments: false

Testing

  • ✅ Syntax check passed
  • ✅ IPv4 socket connection tested
  • ✅ HTML email generation verified
  • ✅ Graceful fallback on conversion failure

Related

swissly added 3 commits June 14, 2026 07:13
The email adapter used smtplib.SMTP() + starttls() for all SMTP connections,
which fails on port 465 (implicit TLS). Port 465 requires SMTP_SSL from the
start. Additionally, when IPv6 is unreachable but AAAA records exist,
socket.create_connection hangs. This adds a _connect_smtp() helper that
selects SMTP_SSL for port 465 and forces IPv4 resolution.

Fixes NousResearch#46018
…text cache

- Replace socket.getaddrinfo monkey-patch with manual socket creation
  that prefers IPv4 but falls back to default resolution (IPv6-safe)
- Pre-connect socket and pass to SMTP/SMTP_SSL via sock assignment,
  with proper banner reading (getreply) and ehlo
- Add try/finally in health check to prevent socket leaks on login fail
- Cache SSLContext on self._smtp_ssl_ctx instead of creating per-call
- Wrap SSL socket manually for port 465 (bypassing _get_socket)
- Add platforms.email.html_format config option (default: true)
- Gate HTML conversion behind config check
- Document security trade-off: no bleach needed for self-to-self emails
- Config toggle provides kill-switch for paranoid deployments

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR enhances the email platform adapter by adding optional Markdown→styled-HTML email rendering and introducing a custom SMTP connection path that prefers IPv4 and reuses an SSL context.

Changes:

  • Added HTML email generation (Markdown conversion + inline styling) with a plain-text fallback.
  • Implemented a custom SMTP socket/connect path (IPv4-first) and reused an SSLContext across sends.
  • Updated SMTP connection testing and send paths to use the new connector.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread gateway/platforms/email.py Outdated
Comment on lines +419 to +427
# Try IPv4 first
addrs = socket.getaddrinfo(
self._smtp_host, self._smtp_port, socket.AF_INET, socket.SOCK_STREAM,
)
if not addrs:
# No A records — fall back to default (may include IPv6)
addrs = socket.getaddrinfo(
self._smtp_host, self._smtp_port, type=socket.SOCK_STREAM,
)
Comment on lines +81 to +102
_HERMES_EMAIL_ELEMENT_STYLES = [
("h1", 'style="font-size:24px;font-weight:700;color:#1a202c;margin:24px 0 12px;border-bottom:2px solid #667eea;padding-bottom:8px;"'),
("h2", 'style="font-size:20px;font-weight:700;color:#2d3748;margin:24px 0 10px;border-bottom:1px solid #e2e8f0;padding-bottom:6px;"'),
("h3", 'style="font-size:17px;font-weight:600;color:#4a5568;margin:18px 0 8px;"'),
("h4", 'style="font-size:15px;font-weight:600;color:#718096;margin:14px 0 6px;"'),
("p", 'style="margin:0 0 12px;"'),
("ul", 'style="margin:0 0 12px;padding-left:24px;"'),
("ol", 'style="margin:0 0 12px;padding-left:24px;"'),
("li", 'style="margin-bottom:4px;"'),
("blockquote", 'style="margin:12px 0;padding:12px 16px;border-left:4px solid #667eea;background:#f7fafc;color:#4a5568;font-style:italic;"'),
("table", 'style="border-collapse:collapse;width:100%;margin:12px 0;font-size:14px;"'),
("th", 'style="background:#667eea;color:#fff;padding:8px 12px;text-align:left;font-weight:600;"'),
("td", 'style="padding:8px 12px;border-bottom:1px solid #e2e8f0;"'),
("tr:nth-child(even)", 'style="background:#f7fafc;"'),
("code", 'style="background:#edf2f7;padding:2px 5px;border-radius:3px;font-size:13px;font-family:Menlo,Monaco,Consolas,monospace;"'),
("pre", 'style="background:#2d3748;color:#e2e8f0;padding:16px;border-radius:6px;overflow-x:auto;font-size:13px;line-height:1.5;"'),
("pre code", 'style="background:transparent;padding:0;color:inherit;"'),
("a", 'style="color:#667eea;text-decoration:none;"'),
("strong", 'style="color:#1a202c;"'),
("em", 'style="color:#4a5568;"'),
("hr", 'style="border:none;border-top:1px solid #e2e8f0;margin:20px 0;"'),
]
Comment on lines +109 to +113
html = re.sub(
rf'<{tag}(\s|>)',
rf'<{tag} {style}\1',
html,
)
Comment thread gateway/platforms/email.py Outdated
Comment on lines +37 to +38
import markdown as _md

Comment on lines +706 to +711
if self._html_format:
try:
html_body = _md.markdown(
body,
extensions=["tables", "fenced_code", "nl2br"],
)
Comment on lines +339 to +346
# SECURITY NOTE: The Markdown library passes through raw HTML by default.
# We intentionally do NOT sanitize with bleach/allowlists because:
# 1. Hermes generates its own email bodies (LLM output, not user input)
# 2. Emails are sent to the configured address (self-to-self)
# 3. The config toggle provides a kill-switch: set html_format: false
# If the threat model changes (e.g. forwarding untrusted content),
# add bleach.clean() here with an allowed-tags list.
self._html_format = extra.get("html_format", True)
@alt-glitch alt-glitch added type/bug Something isn't working comp/gateway Gateway runner, session dispatch, delivery platform/email Email (IMAP/SMTP) adapter P2 Medium — degraded but workaround exists duplicate This issue or pull request already exists labels Jun 15, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Duplicate of #27680 — the SMTP_SSL-for-port-465 + force-IPv4 reliability fix in this PR is the same change as the canonical open #27680 (fix(email): use implicit TLS for SMTPS port 465). Your own earlier #46019 (SMTP_SSL + IPv4) was already marked a duplicate of #27680.

The HTML email support (Markdown→HTML, multipart/alternative, html_format config toggle) is a genuinely new, separate feature that is NOT in #27680. Bundling it with the SMTP reliability fix makes this PR harder to review. Recommend: let #27680 land the SMTP_SSL/IPv4 fix, and re-submit the HTML email support as a focused standalone PR (it can be evaluated on its own merits — note the un-sanitized-HTML security tradeoff will want review).

Related: #46019 (your prior SMTP_SSL+IPv4 attempt).

- Wrap socket.getaddrinfo AF_INET in try/except gaierror (IPv6-only hosts)
- Remove CSS selectors (tr:nth-child, pre code) from style list
- Use negative lookahead to skip tags with existing style= attribute
- Special handling for <code> inside <pre> (transparent override)
- Lazy-import markdown only when html_format is enabled
@swissly

swissly commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

Addressed Copilot Review Round 2

Fixes Applied

  1. socket.getaddrinfo gaierror - Wrapped in try/except for IPv6-only hosts
  2. CSS selectors removed - Replaced tr:nth-child(even) and pre code with proper tag handling
  3. Duplicate style= prevention - Negative lookahead skips tags with existing style
  4. Lazy markdown import - Only imported when html_format is enabled

Verification

  • Syntax check passed
  • markdown no longer imported at module level
  • IPv4 fallback handles gaierror
  • Style injection won't create duplicates

@swissly

swissly commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

Closing — the SMTP_SSL/IPv4 fix is a duplicate of #27680.

Will re-submit the HTML email support as a focused standalone PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery duplicate This issue or pull request already exists P2 Medium — degraded but workaround exists platform/email Email (IMAP/SMTP) adapter type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: HTML email support for email gateway (multipart/alternative + Markdown rendering)

3 participants