clee.sh/posthorn at main · Tangled
clee.sh
posthorn
Star
Fork
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
An IMAP+SMTP gateway for less urgent communications with LLMs
Star
Fork
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
Rust
96.4%
Python
3.6%
main
v0.1.0
Code
Clone this repository
Use permalink
HTTPS
https://tangled.org/clee.sh/posthorn<br>https://tangled.org/did:plc:veo3rwwhdkezwk6prj746dnz<br>getComputedStyle(s).display !== 'none').textContent)"<br>class="px-3 py-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"<br>title="Copy to clipboard"
SSH
git@knot.clee.sh:clee.sh/posthorn<br>git@knot.clee.sh:did:plc:veo3rwwhdkezwk6prj746dnz<br>getComputedStyle(s).display !== 'none').textContent)"<br>class="px-3 py-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"<br>title="Copy to clipboard"
For self-hosted knots, clone URLs may differ based on your setup.
Download tar.gz
Download .zip
src
29 minutes ago
test
2 days ago
.gitignore
4 days ago
Cargo.lock
2 days ago
Cargo.toml
2 days ago
LICENSE
4 days ago
README.md
2 days ago
Commits
Stricter IMAP login sequence
We were accidentally sending `+ \r\n` instead of `+ \r\n` which was<br>causing some versions of mutt to choke and error out with:
imap_auth_sasl: error base64-decoding server response.<br>imap_auth_sasl: IMAP4REV1 AUTH=PLAIN IDLE failed
Removing the errant extra space appears to fix the issue.
010c46f2
clee.sh
27 minutes ago
main
Update README
- more consistent demo password<br>- add asciinema.org demo thumbnail/link
4ca4435c
clee.sh
13 hours ago
Drop postgresql dependency
The mailstore is the source of truth.
6f6874dd
clee.sh
14 hours ago
Branches
main
27 minutes ago
Default
Tags
v0.1.0
Latest
README.md
posthorn#
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<br>completion, calls an OpenAI-compatible gateway, and delivers the reply to your<br>maildir — readable via IMAP from any standard mail client (mutt, Thunderbird,<br>etc.). Conversations thread correctly across round-trips.
How it works#
┌────────┐ SMTP ┌─────────┐ enqueue ┌─────────┐ HTTP ┌──────────┐<br>│ client │ ──────► │ smtp.rs │ ────────► │ llm_jobs│ ───────► │ gateway │<br>└────────┘ └─────────┘ └─────────┘ │ (ollama) │<br>▲ └────┬─────┘<br>│ IMAP │ chat<br>│ ┌─────▼─────┐<br>┌────┴─────┐ read ┌─────────┐ write ┌─────────┐ poll │ worker.rs │<br>│ client │ ◄────── │ imap.rs │ ◄─────── │ maildir │ ◄───── │ + lettre │<br>└──────────┘ └─────────┘ └─────────┘ └───────────┘
SMTP (src/smtp.rs) — minimal receiver; no AUTH, no TLS. Accepts mail to<br>model-id@provider-id. (with + standing in for : in the model-id,<br>so model+tag reaches model:tag), parses headers/body, resolves the thread<br>via In-Reply-To/References, and stores the message in maildir++.
Worker (src/worker.rs) - runs as a short-lived child process , one<br>per job. The serve dispatcher claims pending llm_jobs and spawns<br>posthorn worker --job-id N; the child builds the chat history from the<br>thread, resolves the provider's gateway by name, calls it, and composes the<br>reply email (correct Message-ID/In-Reply-To/References so clients<br>thread it).
IMAP (src/imap/) — IMAP4rev1 server using imap-codec for spec-correct<br>parsing and encoding. Supports SASL AUTHENTICATE PLAIN, SELECT/EXAMINE,<br>FETCH (with BODY.PEEK[HEADER.FIELDS ...] subsetting), STORE (\Seen/<br>\Deleted), EXPUNGE, CLOSE, SEARCH, IDLE, and LIST.
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<br>* N EXISTS the instant a message lands — so mutt shows new replies without a<br>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<br>Configure#
posthorn reads a TOML config (figment; also overridable via POSTHORN_* env<br>vars). Example (test/config.toml.example):
[daemon]<br>domain = "posthorn.localhost"<br>data_dir = "test/data"
[imap]<br>host = "127.0.0.1"<br>port = 11143
[smtp]<br>host = "127.0.0.1"<br>port = 11025<br>max_message_bytes = 10485760
[llm]<br>default_system_prompt = "You are a helpful test assistant. Keep replies short."<br>default_provider = "ollama"
# One block per OpenAI-compatible gateway. The `name` is the subdomain used<br># for routing: mail to `model-id@.` is sent to that gateway.<br>[[llm.providers]]<br>name = "ollama"<br>gateway_url = "http://127.0.0.1:11434"
[[llm.providers]]<br>name = "llama-cpp"<br>gateway_url = "http://127.0.0.1:8080"<br># api_key_env = "LLAMACPP_API_KEY" # optional; read from env at request time
[[users]]<br>email = "user@example.com"<br>password = "hunter2"<br>Users declared under [[users]] are...