sentio README

The project's README from github.com/truespar/sentio. As one markdown file: README.md.

Sentio

Sentio SMTP

Email inbox API for AI agents. Give every agent its own real email address, receive mail as structured webhooks, and reply in-thread over REST - then run thousands of those inboxes side by side, isolated per tenant.

License: MIT OR Apache-2.0 Rust

Agents increasingly need to be an email participant: receive a customer thread, act on it, reply as themselves. That normally means wiring an IMAP poller to a parser to an SMTP relay, and inheriting a legacy MTA's operational surface. Sentio is the whole path in one service - an inbox per agent, parsed and authenticated inbound delivered as a webhook, and sending over the same API.

flowchart LR
    customer([customer]) -- SMTP --> IN
    subgraph Sentio["Sentio · multi-tenant"]
        IN["inbound<br/>authenticate → scan → score → route"]
        OUT["outbound<br/>sign (DKIM) → queue → deliver"]
    end
    IN -- webhook --> agent([your agent])
    agent -- REST --> OUT
    OUT -- SMTP --> customer

Built for platforms. Tenancy reaches every layer: each domain, mailbox, API key, rate limit, suppression list, and spam profile belongs to a tenant. If you run an agentic platform or sell email as a feature, each of your customers gets isolated sending reputation, their own domains, and their own inboxes on one deployment.

It is also a complete mail server. Sentio implements the full protocol - inbound and outbound, DKIM/SPF/DMARC/ARC, MTA-STS, DANE, three-tier anti-spam - so the agent inbox rests on real mail infrastructure rather than a wrapper around someone else's API.


Contents


Quick start with Docker

The compose stack brings up Sentio plus every service it needs: PostgreSQL, Redis, NATS/JetStream, MinIO, ClamAV, and rspamd.

Requirements: Docker Engine 24+ with the Compose plugin, ~4 GB RAM, ~8 GB disk.

git clone https://github.com/truespar/sentio.git
cd sentio
docker compose up -d

That pulls a prebuilt image - ghcr.io/truespar/sentio, published for linux/amd64 and linux/arm64 - so there is nothing to compile. To build from your own checkout instead:

docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build

Expect that to take a while: it is a release build of the whole workspace. Pin a version with SENTIO_TAG=1.2.3 docker compose up -d.

Watch it come up - migrations run once, then the server starts:

docker compose logs -f sentio-migrate   # schema + bootstrap tenant
docker compose logs -f sentio           # the server

Confirm it is healthy:

curl localhost:8080/health/ready
# {"status":"ok","database":"ok","kv":"ok"}

Bootstrap credentials

Migration 002 seeds one admin tenant and one API key:

Tenant ID 00000000-0000-0000-0000-000000000001
API key sentio_bootstrap_admin_CHANGE_ME
curl -H "Authorization: Bearer sentio_bootstrap_admin_CHANGE_ME" \
     localhost:8080/v1/tenants

Rotate this key before exposing the host to anything untrusted - it has wildcard (*) scope. Create a replacement via POST /v1/tenants/{id}/api-keys, then delete the bootstrap one.

Ports

Port Purpose Notes
25 SMTP (MX) Inbound mail from other servers
465 SMTPS Implicit TLS - only binds when certificates are present
587 Submission STARTTLS
8080 REST API Also serves /openapi.json
9001 MinIO console Optional; remove from compose to hide

Already running something on port 25 (Postfix, Exim)? Either stop it or remap, by creating a docker-compose.override.yml:

services:
  sentio:
    ports: !override
      - "2525:25"
      - "4465:465"
      - "5587:587"
      - "8080:8080"

!override replaces the port list instead of appending to it. The file is gitignored and loaded automatically.

TLS

No certificates ship with the image, so Sentio starts in plaintext and skips port 465. Mount a certificate and key to enable TLS:

services:
  sentio:
    volumes:
      - ./tls:/etc/sentio/tls:ro   # expects cert.pem and key.pem

Shutting down

docker compose down       # stop, keep data
docker compose down -v    # stop and delete all volumes

Installing without Docker

Requirements

Build toolchain

Requirement Notes
Rust stable Edition 2021. Install via rustup
cmake Needed by aws-lc-rs, the rustls crypto backend
libclang-dev Needed by bindgen-based dependencies
A C toolchain build-essential or equivalent

Required services

Service Default address Purpose
PostgreSQL 18+ localhost:5432 All persistent state
Redis or Valkey localhost:6379 Rate limits, bans, greylist, reputation
NATS with JetStream localhost:4222 Delivery, retry, webhook, and event pipelines
S3-compatible storage localhost:9000 Raw .eml and attachment blobs

PostgreSQL 18 is a hard floor: the schema uses uuidv7(), which arrived as a built-in in 18. On an older server the migration stops with function uuidv7() does not exist, which does not otherwise explain itself. Check with psql -tAc 'SHOW server_version' before you start.

Any S3-compatible endpoint works - AWS S3, Cloudflare R2, MinIO, SeaweedFS, Ceph. For NATS, JetStream must be enabled (nats-server -js); Sentio creates its own streams on startup.

All four are required, and Sentio exits at startup if the KV store or the queue is unreachable. Install them before step 6, or run just those four from the compose file and keep Sentio itself on the host:

docker compose up -d postgres redis nats minio

Optional services - Sentio degrades gracefully if these are absent.

Service Default address Without it
ClamAV (clamd) localhost:3310 Attachments are not virus-scanned
rspamd localhost:11333 Falls back to the built-in Rust scorer

1. Install build dependencies

# Debian / Ubuntu
sudo apt-get install -y build-essential cmake libclang-dev pkg-config

# Fedora / RHEL
sudo dnf install -y gcc gcc-c++ cmake clang-devel pkgconf

# macOS
brew install cmake llvm

2. Build

git clone https://github.com/truespar/sentio.git
cd sentio
cargo build --release

The binary lands at target/release/sentio-smtp.

Compile-time-checked SQL is served from the committed .sqlx/ cache, so no database is needed to build. If you change a query, regenerate it with a live database:

echo 'DATABASE_URL=postgres://sentio:sentio@localhost:5432/sentio' > .env
cargo sqlx prepare --workspace

3. Create the database

sudo -u postgres createuser --pwprompt sentio
sudo -u postgres createdb --owner=sentio sentio

--pwprompt asks for a password interactively. It has to match the one in database.url, which ships as sentio. To script it, or to use a different password, set both together:

sudo -u postgres psql -c "CREATE ROLE sentio LOGIN PASSWORD 'sentio';"
sudo -u postgres createdb --owner=sentio sentio

4. Apply migrations

The binary embeds its migrations and applies them in order:

./target/release/sentio-smtp --config config/default.toml migrate

This creates the schema and seeds the bootstrap tenant and API key described above.

5. Configure

Copy the shipped defaults and edit:

sudo mkdir -p /etc/sentio
sudo cp config/default.toml /etc/sentio/sentio.toml

At minimum set server.hostname, database.url, and the [storage] credentials. See Configuration.

6. Run

./target/release/sentio-smtp --config /etc/sentio/sentio.toml serve

This needs PostgreSQL, Redis, NATS and the object store already running - see Requirements.

Binding ports below 1024 as a non-root user needs the capability rather than root. Grant it on whichever copy you are running:

sudo setcap 'cap_net_bind_service=+ep' target/release/sentio-smtp

Under systemd this is unnecessary - the unit grants AmbientCapabilities=CAP_NET_BIND_SERVICE instead.

7. Run as a service

A unit file is included at deploy/sentio-smtp.service:

sudo useradd --system --home-dir /var/lib/sentio --shell /usr/sbin/nologin sentio
sudo install -d -o sentio -g sentio /var/lib/sentio
sudo cp target/release/sentio-smtp /usr/local/bin/
sudo cp deploy/sentio-smtp.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now sentio-smtp
journalctl -u sentio-smtp -f

The unit runs as the sentio user created above and reads /etc/sentio/sentio.toml, so make that file readable by it. Check the Requires=/After= lines before enabling: they name postgresql.service, nats.service and redis-server.service, and systemd refuses to start a unit whose Requires= target does not exist. Drop or rename any you run differently - in a container, on another host, or under a different unit name.


Configuration

TOML, with every value overridable by environment variable. The env form is SENTIO__SECTION__KEY - a double underscore between levels, uppercased:

SENTIO__DATABASE__URL="postgres://sentio:[email protected]:5432/sentio"
SENTIO__SERVER__HOSTNAME="mail.example.com"
SENTIO__STORAGE__SECRET_KEY="…"
SENTIO__SPAM__RSPAMD__URL="http://127.0.0.1:11333"   # [spam.rspamd] url

Environment overrides win over the file, which makes secrets easy to keep out of config: leave them unset in TOML and inject them at runtime.

Key sections:

Section Controls
[server] Hostname, listener addresses, worker counts, session limits
[tls] Certificate paths, minimum version, SNI, optional ACME
[database] PostgreSQL URL and pool sizing
[kv] / [redis] KV backend selection and connection
[nats] JetStream URL, prefetch, stream retention
[storage] S3 endpoint, credentials, bucket, path-style addressing
[scanning] ClamAV host, size limits
[spam] Backend choice, score thresholds
[abuse] Rate limits, DNSBLs, greylisting, reputation thresholds
[delivery] Retry schedule, connection pooling, optional smart-host relay
[auth] DKIM/SPF/DMARC/ARC behaviour
[llm] Provider, model, and when classification runs
[observability] Log format and level, metrics, tracing
[tracking] Open and click tracking, branded tracking domains
[webhooks] Event dispatch: signing, retries, concurrency caps
[deliverability] FBL/ARF handling, BATV, one-click unsubscribe
[analytics] Rollup and retention for engagement data
[error_events] Error capture and how long it is kept
[listmonk] Optional bounce bridge to a Listmonk instance

Defaults live in config/default.toml; the container image ships config/oss.toml.


API reference and testing UI

The server documents itself. Two endpoints, both live as soon as it starts:

Endpoint What it is
/docs Interactive API reference with a built-in request client
/openapi.json The OpenAPI 3.1 document - 116 operations across 85 paths

Open http://localhost:8080/docs and you get every endpoint with its schemas, examples, and a Test Request button that calls your running server. Set the bearer token once in the auth panel and you can exercise the whole API from the browser without writing a line of curl.

The page is self-contained: its front-end bundle is embedded in the binary and served by Sentio itself, so /docs renders fine on a host with no outbound internet access.

A generated copy of the specification is committed at docs/openapi.json, so you can read the API, diff it across versions, or generate a client without starting anything. Export it yourself at any time - no config, database, or network required:

cargo run -- openapi > openapi.json          # from source
docker compose exec sentio sentio-smtp openapi   # from the container

Because it is a standard OpenAPI document, the usual generators work directly:

npx @openapitools/openapi-generator-cli generate \
    -i docs/openapi.json -g typescript-fetch -o ./client

What the API covers

Group Ops What it covers
Messages 9 Submit single, batch, raw, or multipart mail; read status, events, and raw source
Domains 7 Register sending/receiving domains, fetch the DNS records to publish, verify them
Mailboxes 5 Per-address inboxes on a domain, with forwarding and auto-reply
Inbound Routes 4 Match inbound mail (exact, domain, regex, catch-all) to a webhook
Tenants 6 Create and manage tenants, tiers, and status
API Keys 3 Scoped keys per tenant
SMTP Credentials 4 Username/password pairs for SMTP submission (argon2-hashed)
OAuth 5 OAuth 2.0 clients - authorization code with PKCE, and client credentials
DKIM Keys 5 Generate, rotate, and retire signing keys; export the DNS record
Webhooks 7 Subscribe to delivery and engagement events, HMAC-signed with retries
Suppressions 5 Bounce, complaint, and unsubscribe lists; check an address before sending
IP Pools 12 Dedicated and shared pools, tenant assignment, and IP warmup schedules
Reputation 3 Per-IP and per-domain reputation scores
Abuse 8 IP bans, whitelists, and reputation controls for the connection tier
Spam Training 2 Train the Bayesian classifier on spam and ham
Tracking 2 Open-pixel and click-redirect endpoints
Tracking Domains 7 Branded CNAME tracking domains with managed certificates
Queues 4 Inspect depth, list deferred mail, pause and resume delivery
Reports 7 Ingest and read DMARC aggregate, FBL/ARF, and TLS-RPT reports
Analytics 4 Delivery, engagement, and volume summaries
Errors 3 Captured error events with a summary endpoint
Health 2 Liveness and readiness probes

116 operations across 85 paths.

Authentication is a bearer token on every /v1/** route:

Authorization: Bearer <your-api-key>

MCP: give agents email as native tools

sentio-mcp is a standalone Model Context Protocol server that exposes the REST API as agent-callable tools, so MCP clients (Claude Desktop, Cursor, opencode, ...) get email capabilities without writing any integration code. It talks stdio, authenticates with a regular API key scoped to one tenant, and inherits all of Sentio's existing auth and rate limiting.

Tool What it does
list_messages List recent inbound/outbound messages
get_message Fetch one message by ID
send_message Send from an owned domain address
reply_message Reply in-thread (sets In-Reply-To/References)
list_mailboxes List mailboxes on a domain
create_mailbox Create a mailbox on an owned domain

Every release ships sentio-mcp as its own archive for Linux x86_64, Linux aarch64 and Windows x86_64, so an MCP client host needs no Rust toolchain:

VERSION=0.1.5   # or whatever the latest release tag is
curl -LO "https://github.com/truespar/sentio/releases/download/v${VERSION}/sentio-mcp-${VERSION}-x86_64-unknown-linux-gnu.tar.gz"
tar -xzf sentio-mcp-*.tar.gz

Or build it from a checkout:

cargo build --release -p sentio-mcp

Either way, point it at a running Sentio. It keeps no state of its own:

SENTIO_BASE_URL="http://localhost:8080" \
SENTIO_API_KEY="your-api-key" \
./sentio-mcp

Wire it into an MCP client config (example for Claude Desktop / opencode):

{
  "mcpServers": {
    "sentio": {
      "command": "/path/to/sentio-mcp",
      "env": {
        "SENTIO_BASE_URL": "http://localhost:8080",
        "SENTIO_API_KEY": "your-api-key"
      }
    }
  }
}

Give an agent its own inbox

A mailbox is an address that belongs to a tenant's domain. Give each agent one, and mail addressed to it arrives at your webhook already parsed, authenticated, and scored.

KEY="sentio_bootstrap_admin_CHANGE_ME"
API="http://localhost:8080"
TENANT="00000000-0000-0000-0000-000000000001"

# 1. A receiving domain for this tenant
DOMAIN_ID=$(curl -s -X POST "$API/v1/domains" \
  -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  -d '{"domain_name":"acme.example.com","use_for_receiving":true,"use_for_sending":true}' \
  | jq -r .data.id)

# 2. One mailbox per agent. metadata is yours - put the agent id in it.
curl -X POST "$API/v1/domains/$DOMAIN_ID/mailboxes" \
  -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  -d '{
    "address": "support-agent",
    "display_name": "Acme Support Agent",
    "metadata": {"agent_id": "agt_01H8..."}
  }'

# 3. Route the domain to your application
curl -X POST "$API/v1/tenants/$TENANT/inbound-routes" \
  -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  -d '{
    "match_type": "domain",
    "pattern": "acme.example.com",
    "webhook_url": "https://your-platform.example.com/hooks/inbound",
    "priority": 100
  }'

[email protected] is now live. Inbound mail is SPF/DKIM/DMARC verified, virus-scanned, and spam-scored before your webhook fires, and those verdicts arrive with the payload - so an agent never has to reason about whether a sender was forged.

Replying in thread

Pass the inbound Message-ID back as in_reply_to and the reply threads correctly in the recipient's client:

curl -X POST "$API/v1/messages/send" \
  -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  -d '{
    "from": "[email protected]",
    "to": ["[email protected]"],
    "subject": "Re: Order #1234",
    "text": "Refund processed - you should see it in 3-5 days.",
    "in_reply_to": "<[email protected]>",
    "references": ["<[email protected]>"]
  }'

Outbound is DKIM-signed with the tenant's own key, so replies authenticate as the customer's domain rather than yours.

Useful mailbox options

Field Effect
metadata Free-form JSON - the natural place for your agent_id
forward_to Forward inbound mail on to external addresses (see below)
auto_reply Immediate acknowledgement, threaded via In-Reply-To, while the agent works
status disabled stops delivery without deleting history

Forwarding a mailbox to external addresses

Set forward_to and ev

sentio (truespar/sentio)

Searching Database…

254 ·GitHub
Что это такое?Sentio — полноценный многопользовательский (multi-tenant) почтовый сервер для AI-агентов: он даёт каждому агенту собственный настоящий email-адрес, доставляет входящую почту структурированными вебхуками и позволяет агенту отвечать в треде через REST API.
Это библиотека почтового клиента или полноценный почтовый сервер, который я запускаю сам?Это автономный самохостинговый почтовый сервер на Rust, реализующий полный протокол SMTP (входящие и исходящие, DKIM/SPF/DMARC/ARC) плюс REST API, а не обёртка над email-API другого провайдера.
Как его запустить?Самый быстрый путь — стек Docker Compose: он подтягивает готовый образ и поднимает Sentio вместе с PostgreSQL, Redis, NATS, MinIO, ClamAV и rspamd примерно за пять минут. Без Docker вы собираете Rust-бинарник, создаёте базу данных, выполняете встроенные миграции и запускаете его при уже работающем стеке PostgreSQL/Redis/NATS/S3.
Какие сервисы нужны Sentio для работы?Обязательны PostgreSQL 18+ (нужна встроенная функция uuidv7()), Redis или Valkey, NATS с включённым JetStream и S3-совместимое объектное хранилище; без них Sentio отказывается стартовать. ClamAV и rspamd необязательны, и без них он деградирует мягко.
Есть ли подводные камни при настройке для настоящей почты?Запуск в качестве настоящего почтового сервера означает, что DNS, reverse-PTR-записи и биндинг привилегированного порта 25 придётся разбирать самому, а предзаполненный bootstrap API-ключ имеет wildcard-область действия и должен быть заменён до того, как хост будет открыт чему-то недоверенному.
Для кого это сделано?Он рассчитан на команды, которые строят агентные платформы или продают почту как фичу и где каждому тенанту нужны изолированная репутация отправителя, свои домены и свои почтовые ящики агентов на одном развёртывании.
Что добавляет сервер sentio-mcp?sentio-mcp — автономный сервер Model Context Protocol, который открывает REST API как вызываемые агентами инструменты через stdio, так что клиенты вроде Claude Desktop, Cursor или opencode могут перечислять сообщения, отправлять почту, отвечать в треде и управлять ящиками без собственного интеграционного кода.
Как устроена антиспам-система?Защита от злоупотреблений работает в три уровня: контроль на уровне соединений (IP-баны, DNSBL, грейлистинг, репутация), бэкенд скоринга спама — это может быть rspamd или встроенный байесовский классификатор, который можно обучать на спаме и нормальной почте (ham), — и списки подавления для отказов, жалоб и отписок.
Может ли агент ответить внутри существующей почтовой ветки?Да, API/инструмент replymessage автоматически проставляет заголовки In-Reply-To и References, чтобы ответы корректно складывались в тред во входящих получателя.
Можно ли изучить API, не написав ни строчки кода?Да, сервер отдаёт интерактивную страницу /docs со встроенным клиентом Test Request и полный документ OpenAPI 3.1 по адресу /openapi.json, покрывающий 116 операций в 85 путях; всё это встроено в бинарник и работает даже офлайн.
Creator
truespar
Size
2.2 MB
Stars
254
Age
1mo
Commits · 90 Days