Files
hermes-agent/website/docs/user-guide/messaging/email.md
John Paul Soliva b5a300fe34 fix(email): pairing, decline and gateway grants reach the gateway instead of dying in the adapter pre-gate
EmailAdapter._sender_accepted runs before any MessageEvent exists and
read only EMAIL_ALLOWED_USERS. Unset, it dropped every sender unless
allow-all was on; set, it dropped everyone not listed. The gateway's
own handling therefore never ran for email:
platforms.email.unauthorized_dm_behavior "pair" (the setup wizard's
"Use DM pairing") and "decline" sent nothing, and a sender admitted by
GATEWAY_ALLOWED_USERS or an approved pairing was dropped. bb304b4914
turned the empty-allowlist branch into drop-all after #50568 had made
"pair" email's explicit opt-in.

The gate now keeps a sender listed by address in EMAIL_ALLOWED_USERS
or GATEWAY_ALLOWED_USERS, a sender the registered gateway
authorization check admits (that is the only reader of the pairing
store), and, under an explicit pair or decline, an unknown sender the
gateway will answer. The default "ignore" still drops unknown senders
before a MessageEvent exists, so the mail-loop guard from fd9c32c0f2
holds.

Three guards keep the wider gate from widening access, and close two
forged-From: paths main already had:
- A sender admitted only so the gateway can answer it (pair or
  decline) must authenticate its From:, open access or not: the
  pairing code or refusal is mailed back to that address. A granted
  sender still needs it short of open access, since a pairing grant
  keys on From: just as the allowlist does. Open access follows the
  gateway's own order: EMAIL_ALLOW_ALL_USERS wins over a list, while
  GATEWAY_ALLOW_ALL_USERS beside a list admits nobody extra, so it no
  longer exempts a listed address from From: authentication either
  (on main a forged From: of a listed address got through there).
- Open access comes from the gateway's own verdict when a check is
  registered. GATEWAY_ALLOW_ALL_USERS beside a GATEWAY_ALLOWED_USERS
  list grants a stranger nothing there, so the env flag alone no
  longer exempts one from From: authentication (that path mailed a
  pairing code to a forged From: on main too).
- A sender whose local part alone matches an allowlist entry is
  dropped. The gateway's check also matches an address by its bare
  local part (#119446), so without this, GATEWAY_ALLOWED_USERS=alice
  (a chat username) would admit or pair alice@<any domain>. The lists
  are parsed as the gateway parses them, JSON list literals included,
  or '["alice"]' would slip past this guard.

_allowlist_in_effect only served the old condition and is removed.
The scope tests now assert the same scoped reads through
_sender_accepted, with GATEWAY_ALLOWED_USERS covered as well.

Measured end to end with the real GatewayRunner callback wired
(adapter -> gateway ingress):
- pair, decline, GATEWAY_ALLOWED_USERS and an approved pairing each
  went from 0 events reaching the gateway to 1. pair mails a pairing
  code, decline mails one refusal.
- An unauthenticated From: in pair mode, for a paired address or for a
  GATEWAY_ALLOWED_USERS address still reaches nothing.
- A bare GATEWAY_ALLOWED_USERS=stranger entry lets nothing from
  stranger@<domain> through, under ignore or pair. Without the
  local-part guard that mail reached the gateway in both.
- The same holds for a JSON-literal list, and a pair-mode stranger
  with a forged From: under allow-all beside an EMAIL_ or
  GATEWAY_ALLOWED_USERS list reaches nothing.
- The default still drops.
2026-09-23 07:48:32 -07:00

10 KiB

sidebar_position, title, description
sidebar_position title description
7 Email Set up Hermes Agent as an email assistant via IMAP/SMTP

Email Setup

Hermes can receive and reply to emails using standard IMAP and SMTP protocols. Send an email to the agent's address and it replies in-thread — no special client or bot API needed. Works with Gmail, Outlook, Yahoo, Fastmail, or any provider that supports IMAP/SMTP.

:::info Gateway adapter only: no external dependencies This page covers the Email gateway adapter, which uses Python's built-in imaplib, smtplib, and email modules. No additional packages or external services are required for this gateway path. :::

This is separate from the bundled Himalaya email skill, which lets the agent manage email through terminal commands and requires the external himalaya CLI plus a Himalaya config file.

Use case What to configure External dependency
Let people email the Hermes agent and receive replies Email gateway adapter on this page None beyond an IMAP/SMTP email account
Let the agent inspect, compose, move, and manage mailbox messages from terminal tools Himalaya email skill himalaya CLI and ~/.config/himalaya/config.toml

Prerequisites

  • A dedicated email account for your Hermes agent (don't use your personal email)
  • IMAP enabled on the email account
  • An app password if using Gmail or another provider with 2FA

Gmail Setup

  1. Enable 2-Factor Authentication on your Google Account
  2. Go to App Passwords
  3. Create a new App Password (select "Mail" or "Other")
  4. Copy the 16-character password — you'll use this instead of your regular password

Outlook / Microsoft 365

  1. Go to Security Settings
  2. Enable 2FA if not already active
  3. Create an App Password under "Additional security options"
  4. IMAP host: outlook.office365.com, SMTP host: smtp.office365.com

Other Providers

Most email providers support IMAP/SMTP. Check your provider's documentation for:

  • IMAP host and port (usually port 993 with SSL)
  • SMTP host and port (usually port 587 with STARTTLS)
  • Whether app passwords are required

Proton Mail Bridge / local relays

Proton Mail Bridge (and similar local relays such as a self-hosted MTA) listen on loopback with STARTTLS and a self-signed certificate, so the defaults (implicit TLS on IMAP 993, verified certificates) won't connect. Override the transport in ~/.hermes/config.yaml:

platforms:
  email:
    enabled: true
    extra:
      imap_host: 127.0.0.1
      imap_security: starttls     # tls (default) | starttls | plain
      imap_tls_verify: false      # Bridge uses a self-signed cert
      smtp_host: 127.0.0.1
      smtp_security: starttls     # default: tls on port 465, starttls otherwise
      smtp_tls_verify: false

and set EMAIL_IMAP_PORT=1143 / EMAIL_SMTP_PORT=1025 alongside your Bridge credentials in ~/.hermes/.env. Unknown *_security values log a warning and fall back to the secure default. Only disable *_tls_verify for loopback hosts — Hermes logs a warning when verification is off for any other host.


Step 1: Configure Hermes

The easiest way:

hermes gateway setup

Select Email from the platform menu. The wizard prompts for your email address, password, IMAP/SMTP hosts, and allowed senders.

Manual Configuration

Add to ~/.hermes/.env:

# Required
EMAIL_ADDRESS=hermes@gmail.com
EMAIL_PASSWORD=abcd efgh ijkl mnop    # App password (not your regular password)
EMAIL_IMAP_HOST=imap.gmail.com
EMAIL_SMTP_HOST=smtp.gmail.com

# Security (recommended)
EMAIL_ALLOWED_USERS=your@email.com,colleague@work.com

# Optional
EMAIL_IMAP_PORT=993                    # Default: 993 (IMAP SSL)
EMAIL_SMTP_PORT=587                    # Default: 587 (SMTP STARTTLS)
EMAIL_POLL_INTERVAL=15                 # Seconds between inbox checks (default: 15)
EMAIL_HOME_ADDRESS=your@email.com      # Default delivery target for cron jobs

Step 2: Start the Gateway

hermes gateway              # Run in foreground
hermes gateway install      # Install as a user service
sudo hermes gateway install --system   # Linux only: boot-time system service

On startup, the adapter:

  1. Tests IMAP and SMTP connections
  2. Marks all existing inbox messages as "seen" (only processes new emails)
  3. Starts polling for new messages

How It Works

Receiving Messages

The adapter polls the IMAP inbox for UNSEEN messages at a configurable interval (default: 15 seconds). For each new email:

  • Subject line is included as context (e.g., [Subject: Deploy to production])
  • Reply emails (subject starting with Re:) skip the subject prefix — the thread context is already established
  • Attachments are cached locally:
    • Images (JPEG, PNG, GIF, WebP) → available to the vision tool
    • Documents (PDF, ZIP, etc.) → available for file access
  • HTML-only emails have tags stripped for plain text extraction
  • Self-messages are filtered out to prevent reply loops
  • Automated/noreply senders are silently ignored — noreply@, mailer-daemon@, bounce@, no-reply@, and emails with Auto-Submitted, Precedence: bulk, or List-Unsubscribe headers

Sending Replies

Replies are sent via SMTP with proper email threading:

  • In-Reply-To and References headers maintain the thread
  • Subject line preserved with Re: prefix (no double Re: Re:)
  • Message-ID generated with the agent's domain
  • Responses are sent as plain text (UTF-8)

File Attachments

The agent can send file attachments in replies. Include MEDIA:/path/to/file in the response and the file is attached to the outgoing email.

Skipping Attachments

To ignore all incoming attachments (for malware protection or bandwidth savings), add to your config.yaml:

platforms:
  email:
    skip_attachments: true

When enabled, attachment and inline parts are skipped before payload decoding. The email body text is still processed normally.


Access Control

Email access is stricter by default than chat-style platforms:

  1. EMAIL_ALLOWED_USERS set → only emails from those addresses (and from GATEWAY_ALLOWED_USERS or an approved pairing) are processed
  2. No allowlist set → unknown senders are ignored silently
  3. EMAIL_ALLOW_ALL_USERS=true → any sender is accepted (use with caution)
  4. platforms.email.unauthorized_dm_behavior: pair → unknown senders receive a pairing code
  5. platforms.email.unauthorized_dm_behavior: decline → an unknown sender receives one polite refusal, then nothing more for 24 hours

Allowlist entries match whole addresses. A bare entry such as alice (a chat username in GATEWAY_ALLOWED_USERS, say) never admits alice@ at any domain, and mail from such an address is dropped rather than paired or declined.

Unless open access is on, Hermes acts on a message only when the Authentication-Results header stamped by your receiving server authenticates its From: domain (DMARC, or aligned SPF/DKIM). GATEWAY_ALLOW_ALL_USERS counts as open access only while no allowlist is set, as it does for the gateway itself. Pairing codes and declines need an authenticated From: even with open access on, so neither is mailed to a forged address. If your mail server does not stamp that header, set platforms.email.require_authenticated_sender: false to accept the risk.

:::warning Use a dedicated inbox and configure EMAIL_ALLOWED_USERS for normal operation. Email pairing is opt-in because shared inboxes often contain unrelated unread messages, and Hermes should not reply to those contacts by default. :::


Troubleshooting

Problem Solution
"IMAP connection failed" at startup Verify EMAIL_IMAP_HOST and EMAIL_IMAP_PORT. Ensure IMAP is enabled on the account. For Gmail, enable it in Settings → Forwarding and POP/IMAP.
"SMTP connection failed" at startup Verify EMAIL_SMTP_HOST and EMAIL_SMTP_PORT. Check that your password is correct (use App Password for Gmail).
Messages not received Check EMAIL_ALLOWED_USERS includes the sender's email. Check spam folder — some providers flag automated replies.
"Authentication failed" For Gmail, you must use an App Password, not your regular password. Ensure 2FA is enabled first.
Duplicate replies Ensure only one gateway instance is running. Check hermes gateway status.
Slow response The default poll interval is 15 seconds. Reduce with EMAIL_POLL_INTERVAL=5 for faster response (but more IMAP connections).
Replies not threading The adapter uses In-Reply-To headers. Some email clients (especially web-based) may not thread correctly with automated messages.

Security

:::warning Use a dedicated email account. Don't use your personal email — the agent stores the password in .env and has full inbox access via IMAP. :::

  • Use App Passwords instead of your main password (required for Gmail with 2FA)
  • Set EMAIL_ALLOWED_USERS to restrict who can interact with the agent
  • The password is stored in ~/.hermes/.env — protect this file (chmod 600)
  • IMAP uses SSL (port 993) and SMTP uses STARTTLS (port 587) by default — connections are encrypted

Environment Variables Reference

Variable Required Default Description
EMAIL_ADDRESS Yes — Agent's email address
EMAIL_PASSWORD Yes — Email password or app password
EMAIL_IMAP_HOST Yes — IMAP server host (e.g., imap.gmail.com)
EMAIL_SMTP_HOST Yes — SMTP server host (e.g., smtp.gmail.com)
EMAIL_IMAP_PORT No 993 IMAP server port
EMAIL_SMTP_PORT No 587 SMTP server port
EMAIL_POLL_INTERVAL No 15 Seconds between inbox checks
EMAIL_ALLOWED_USERS No — Comma-separated allowed sender addresses
EMAIL_HOME_ADDRESS No — Default delivery target for cron jobs
EMAIL_ALLOW_ALL_USERS No false Allow all senders (not recommended)