cd /news/large-language-models/posthorn-a-self-contained-email-pen-… · home topics large-language-models article
[ARTICLE · art-49114] src=tangled.org ↗ pub= topic=large-language-models verified=true sentiment=· neutral

Posthorn – A self-contained email pen-pal daemon for slow, self-hosted LLMs

Posthorn, a self-contained email pen-pal daemon for slow, self-hosted LLMs, has been released. It allows users to send emails to an LLM via SMTP, which queues the request, calls an OpenAI-compatible gateway, and delivers the reply to a maildir accessible via IMAP. The tool supports threaded conversations, live new-mail notifications via inotify, and requires Linux, Rust, PostgreSQL, and an OpenAI-compatible gateway like Ollama.

read4 min views1 publishedJul 7, 2026
Posthorn – A self-contained email pen-pal daemon for slow, self-hosted LLMs
Image: source

A self-contained email pen-pal daemon for slow, self-hosted LLMs.

You send an email to model@provider.domain

over SMTP. posthorn queues an LLM completion, calls an OpenAI-compatible gateway, and delivers the reply to your maildir — readable via IMAP from any standard mail client (mutt, Thunderbird, etc.). Conversations thread correctly across round-trips.

How it works# #

┌────────┐  SMTP   ┌─────────┐  enqueue  ┌─────────┐  HTTP    ┌──────────┐
│ client │ ──────► │ smtp.rs │ ────────► │ llm_jobs│ ───────► │ gateway  │
└────────┘         └─────────┘           └─────────┘          │ (ollama) │
     ▲                                                        └────┬─────┘
     │ IMAP                                                        │ chat
     │                                                       ┌─────▼─────┐
┌────┴─────┐  read   ┌─────────┐  write   ┌─────────┐  poll  │ worker.rs │
│ client   │ ◄────── │ imap.rs │ ◄─────── │ maildir │ ◄───── │ + lettre  │
└──────────┘         └─────────┘          └─────────┘        └───────────┘

SMTP(src/smtp.rs

) — minimal receiver; no AUTH, no TLS. Accepts mail tomodel-id@provider-id.<domain>

(with+

standing in for:

in the model-id, somodel+tag

reachesmodel:tag

), parses headers/body, resolves the thread viaIn-Reply-To

/References

, and stores the message in maildir++.Worker(src/worker.rs

) - runs as ashort-lived child process, one per job. Theserve

dispatcher claims pendingllm_jobs

and spawnsposthorn worker --job-id N

; the child builds the chat history from the thread, resolves the provider's gateway by name, calls it, and composes the reply email (correctMessage-ID

/In-Reply-To

/References

so clients thread it).IMAP(src/imap/

) — IMAP4rev1 server usingimap-codec

for spec-correct parsing and encoding. Supports SASLAUTHENTICATE PLAIN

,SELECT

/EXAMINE

,FETCH

(withBODY.PEEK[HEADER.FIELDS ...]

subsetting),STORE

(\Seen

/\Deleted

),EXPUNGE

,CLOSE

,SEARCH

,IDLE

, andLIST

.Storage— maildir++ on disk for raw.eml

files.

IDLE push (live new-mail notifications)# #

Instead of polling, IDLE

uses inotify

to watch the user's maildir and pushes * N EXISTS

the instant a message lands — so mutt shows new replies without a manual refresh. No polling, kernel-driven.

Requirements# #

  • Linux (inotify is Linux-only)
  • Rust (edition 2021, MSRV 1.84)
  • PostgreSQL 15+
  • An OpenAI-compatible gateway (e.g. Ollama)

Build# #

cargo build -r

Configure# #

posthorn reads a TOML config (figment; also overridable via POSTHORN_*

env vars). Example (test/config.toml.example

):

[daemon]
domain = "posthorn.localhost"
data_dir = "test/data"

[imap]
host = "127.0.0.1"
port = 11143

[smtp]
host = "127.0.0.1"
port = 11025
max_message_bytes = 10485760

[llm]
default_system_prompt = "You are a helpful test assistant. Keep replies short."
default_provider = "ollama"

[[llm.providers]]
name = "ollama"
gateway_url = "http://127.0.0.1:11434"

[[llm.providers]]
name = "llama-cpp"
gateway_url = "http://127.0.0.1:8080"

[[users]]
email = "user@example.com"
password = "hunter2"

Users declared under [[users]]

are seeded into the DB on startup. The mail address for a model is model-id@provider-id.<domain>

  • e.g. glm-4.7-flash@ollama.posthorn.localhost

or llama@llama-cpp.posthorn.localhost

. Each provider name must match a [[llm.providers]]

entry; the gateway URL is resolved from config at runtime, so different providers can point at different machines. Models are discovered automatically from each provider's gateway on startup.

A model tag (e.g. an Ollama quant) or a multi-segment provider name is written with +

in place of :

— the +

is translated to :

for the gateway call, and translated back on the reply address so the thread round-trips. For example glm-4.7-flash+q8_0@ollama.posthorn.localhost

reaches model glm-4.7-flash:q8_0

, and hf+zai-org/GLM-5.2@ollama.posthorn.localhost

reaches hf:zai-org/GLM-5.2

. Replies come back from the +

form, which your mail client threads normally.

Run# #

RUST_LOG=info target/release/posthorn --config test/config.toml.example

Then point an mail client at the SMTP/IMAP ports and send mail to your model.

Client setup (mutt)# #

set ssl_starttls = no
set ssl_force_tls = no
set imap_user = "user@example.com"
set imap_pass = "hunter2"
set folder = "imap://127.0.0.1:11143"
set spoolfile = "+INBOX"
set smtp_url = "smtp://127.0.0.1:11025"
set from = "user@example.com"
set imap_idle = yes      # live new-mail push

Project layout# #

src/
  main.rs        wiring: config -> DB -> SMTP -> IMAP -> dispatcher (serve) or
                 single-job runner (worker --job-id N)
  cli.rs         serve / worker subcommands
  config.rs      figment TOML + env config; LlmConfig + per-provider entries
  smtp.rs        SMTP receiver + recipient parsing (model@provider.domain)
  worker.rs      runs one LLM job: builds history, resolves gateway, replies
  gateway.rs     reqwest-based HTTP SSE-streaming client (OpenAI-compatible)
  maildir.rs     maildir++ storage for raw .eml files
  mailparse.rs   header/body extraction for inbound mail
  storage.rs     sqlx queries + migrations
  imap/
    mod.rs       ImapServer (accept) + ImapConn (per-connection dispatch)
    proto.rs     pure IMAP value/sequence helpers
    fetch.rs     FETCH response construction (envelope, body, sections)
    messages.rs  maildir + Postgres backend glue; inotify IDLE watcher
migrations/      Postgres schema (sqlx)
test/            example config + end-to-end roundtrip test + mock gateway

Status# #

Early, single-purpose, no TLS. Designed for a trusted home network talking to a self-hosted LLM. Passwords are plaintext (v1) — use per-user tokens, not real credentials.

── more in #large-language-models 4 stories · sorted by recency
── more on @posthorn 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/posthorn-a-self-cont…] indexed:0 read:4min 2026-07-07 ·