{"slug": "posthorn-a-self-contained-email-pen-pal-daemon-for-slow-self-hosted-llms", "title": "Posthorn – A self-contained email pen-pal daemon for slow, self-hosted LLMs", "summary": "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.", "body_md": "# posthorn[#](#posthorn)\n\nA self-contained email pen-pal daemon for slow, self-hosted LLMs.\n\nYou send an email to `model@provider.domain`\n\nover SMTP. posthorn queues an LLM\ncompletion, calls an OpenAI-compatible gateway, and delivers the reply to your\nmaildir — readable via IMAP from any standard mail client (mutt, Thunderbird,\netc.). Conversations thread correctly across round-trips.\n\n## How it works[#](#how-it-works)\n\n```\n┌────────┐  SMTP   ┌─────────┐  enqueue  ┌─────────┐  HTTP    ┌──────────┐\n│ client │ ──────► │ smtp.rs │ ────────► │ llm_jobs│ ───────► │ gateway  │\n└────────┘         └─────────┘           └─────────┘          │ (ollama) │\n     ▲                                                        └────┬─────┘\n     │ IMAP                                                        │ chat\n     │                                                       ┌─────▼─────┐\n┌────┴─────┐  read   ┌─────────┐  write   ┌─────────┐  poll  │ worker.rs │\n│ client   │ ◄────── │ imap.rs │ ◄─────── │ maildir │ ◄───── │ + lettre  │\n└──────────┘         └─────────┘          └─────────┘        └───────────┘\n```\n\n**SMTP**(`src/smtp.rs`\n\n) — minimal receiver; no AUTH, no TLS. Accepts mail to`model-id@provider-id.<domain>`\n\n(with`+`\n\nstanding in for`:`\n\nin the model-id, so`model+tag`\n\nreaches`model:tag`\n\n), parses headers/body, resolves the thread via`In-Reply-To`\n\n/`References`\n\n, and stores the message in maildir++.**Worker**(`src/worker.rs`\n\n) - runs as a**short-lived child process**, one per job. The`serve`\n\ndispatcher claims pending`llm_jobs`\n\nand spawns`posthorn worker --job-id N`\n\n; the child builds the chat history from the thread, resolves the provider's gateway by name, calls it, and composes the reply email (correct`Message-ID`\n\n/`In-Reply-To`\n\n/`References`\n\nso clients thread it).**IMAP**(`src/imap/`\n\n) — IMAP4rev1 server using`imap-codec`\n\nfor spec-correct parsing and encoding. Supports SASL`AUTHENTICATE PLAIN`\n\n,`SELECT`\n\n/`EXAMINE`\n\n,`FETCH`\n\n(with`BODY.PEEK[HEADER.FIELDS ...]`\n\nsubsetting),`STORE`\n\n(`\\Seen`\n\n/`\\Deleted`\n\n),`EXPUNGE`\n\n,`CLOSE`\n\n,`SEARCH`\n\n,`IDLE`\n\n, and`LIST`\n\n.**Storage**— maildir++ on disk for raw`.eml`\n\nfiles.\n\n## IDLE push (live new-mail notifications)[#](#idle-push-live-new-mail-notifications)\n\nInstead of polling, `IDLE`\n\nuses `inotify`\n\nto watch the user's maildir and pushes\n`* N EXISTS`\n\nthe instant a message lands — so mutt shows new replies without a\nmanual refresh. No polling, kernel-driven.\n\n## Requirements[#](#requirements)\n\n- Linux (inotify is Linux-only)\n- Rust (edition 2021, MSRV 1.84)\n- PostgreSQL 15+\n- An OpenAI-compatible gateway (e.g.\n[Ollama](https://ollama.com))\n\n## Build[#](#build)\n\n```\ncargo build -r\n```\n\n## Configure[#](#configure)\n\nposthorn reads a TOML config (figment; also overridable via `POSTHORN_*`\n\nenv\nvars). Example (`test/config.toml.example`\n\n):\n\n```\n[daemon]\ndomain = \"posthorn.localhost\"\ndata_dir = \"test/data\"\n\n[imap]\nhost = \"127.0.0.1\"\nport = 11143\n\n[smtp]\nhost = \"127.0.0.1\"\nport = 11025\nmax_message_bytes = 10485760\n\n[llm]\ndefault_system_prompt = \"You are a helpful test assistant. Keep replies short.\"\ndefault_provider = \"ollama\"\n\n# One block per OpenAI-compatible gateway. The `name` is the subdomain used\n# for routing: mail to `model-id@<name>.<domain>` is sent to that gateway.\n[[llm.providers]]\nname = \"ollama\"\ngateway_url = \"http://127.0.0.1:11434\"\n\n[[llm.providers]]\nname = \"llama-cpp\"\ngateway_url = \"http://127.0.0.1:8080\"\n# api_key_env = \"LLAMACPP_API_KEY\"   # optional; read from env at request time\n\n[[users]]\nemail = \"user@example.com\"\npassword = \"hunter2\"\n```\n\nUsers declared under `[[users]]`\n\nare seeded into the DB on startup. The mail\naddress for a model is `model-id@provider-id.<domain>`\n\n- e.g.\n`glm-4.7-flash@ollama.posthorn.localhost`\n\nor `llama@llama-cpp.posthorn.localhost`\n\n. Each provider\nname must match a `[[llm.providers]]`\n\nentry; the gateway URL is resolved from\nconfig at runtime, so different providers can point at different machines.\nModels are discovered automatically from each provider's gateway on startup.\n\nA model tag (e.g. an Ollama quant) or a multi-segment provider name is written\nwith `+`\n\nin place of `:`\n\n— the `+`\n\nis translated to `:`\n\nfor the gateway call,\nand translated back on the reply address so the thread round-trips. For example\n`glm-4.7-flash+q8_0@ollama.posthorn.localhost`\n\nreaches model\n`glm-4.7-flash:q8_0`\n\n, and `hf+zai-org/GLM-5.2@ollama.posthorn.localhost`\n\nreaches `hf:zai-org/GLM-5.2`\n\n. Replies come back from the `+`\n\nform, which your\nmail client threads normally.\n\n## Run[#](#run)\n\n```\n# The server subcommand (`serve`) is the default and runs SMTP + IMAP plus a\n# dispatcher that spawns a worker child process per claimed LLM job.\nRUST_LOG=info target/release/posthorn --config test/config.toml.example\n```\n\nThen point an mail client at the SMTP/IMAP ports and send mail to your model.\n\n## Client setup (mutt)[#](#client-setup-mutt)\n\n```\nset ssl_starttls = no\nset ssl_force_tls = no\nset imap_user = \"user@example.com\"\nset imap_pass = \"hunter2\"\nset folder = \"imap://127.0.0.1:11143\"\nset spoolfile = \"+INBOX\"\nset smtp_url = \"smtp://127.0.0.1:11025\"\nset from = \"user@example.com\"\nset imap_idle = yes      # live new-mail push\n```\n\n## Project layout[#](#project-layout)\n\n``` php\nsrc/\n  main.rs        wiring: config -> DB -> SMTP -> IMAP -> dispatcher (serve) or\n                 single-job runner (worker --job-id N)\n  cli.rs         serve / worker subcommands\n  config.rs      figment TOML + env config; LlmConfig + per-provider entries\n  smtp.rs        SMTP receiver + recipient parsing (model@provider.domain)\n  worker.rs      runs one LLM job: builds history, resolves gateway, replies\n  gateway.rs     reqwest-based HTTP SSE-streaming client (OpenAI-compatible)\n  maildir.rs     maildir++ storage for raw .eml files\n  mailparse.rs   header/body extraction for inbound mail\n  storage.rs     sqlx queries + migrations\n  imap/\n    mod.rs       ImapServer (accept) + ImapConn (per-connection dispatch)\n    proto.rs     pure IMAP value/sequence helpers\n    fetch.rs     FETCH response construction (envelope, body, sections)\n    messages.rs  maildir + Postgres backend glue; inotify IDLE watcher\nmigrations/      Postgres schema (sqlx)\ntest/            example config + end-to-end roundtrip test + mock gateway\n```\n\n## Status[#](#status)\n\nEarly, 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.", "url": "https://wpnews.pro/news/posthorn-a-self-contained-email-pen-pal-daemon-for-slow-self-hosted-llms", "canonical_source": "https://tangled.org/clee.sh/posthorn", "published_at": "2026-07-07 08:19:39+00:00", "updated_at": "2026-07-07 08:29:42.605153+00:00", "lang": "en", "topics": ["large-language-models", "ai-tools", "developer-tools"], "entities": ["Posthorn", "Ollama", "PostgreSQL", "Rust", "Linux", "IMAP", "SMTP", "OpenAI"], "alternates": {"html": "https://wpnews.pro/news/posthorn-a-self-contained-email-pen-pal-daemon-for-slow-self-hosted-llms", "markdown": "https://wpnews.pro/news/posthorn-a-self-contained-email-pen-pal-daemon-for-slow-self-hosted-llms.md", "text": "https://wpnews.pro/news/posthorn-a-self-contained-email-pen-pal-daemon-for-slow-self-hosted-llms.txt", "jsonld": "https://wpnews.pro/news/posthorn-a-self-contained-email-pen-pal-daemon-for-slow-self-hosted-llms.jsonld"}}