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…

255 ·GitHub
这是什么?Sentio 是一个面向 AI 智能体的完整多租户邮件服务器:它为每个智能体提供自己的真实电子邮件地址,把收到的邮件以结构化 webhook 送达,并让智能体通过 REST API 在线程内回复。
这是邮件客户端库,还是需要我自己运行的完整邮件服务器?它是一个用 Rust 编写的独立自托管邮件服务器,实现了完整的 SMTP 协议(收信和发信,DKIM/SPF/DMARC/ARC)以及 REST API,不是对其他提供商邮件 API 的封装。
怎么让它跑起来?最快的方式是 Docker Compose 堆栈:它拉取预构建镜像,在大约五分钟内启动 Sentio 以及 PostgreSQL、Redis、NATS、MinIO、ClamAV 和 rspamd。不用 Docker 的话,你需要构建 Rust 二进制、创建数据库、运行内置迁移,并在已运行 PostgreSQL/Redis/NATS/S3 堆栈的情况下启动它。
Sentio 运行需要哪些后端服务?PostgreSQL 18+(需要其内置的 uuidv7() 函数)、Redis 或 Valkey、启用 JetStream 的 NATS,以及 S3 兼容对象存储都是必需的;缺少它们 Sentio 会拒绝启动。ClamAV 和 rspamd 是可选的,缺失时它会优雅降级。
把它配置为真实邮件服务有什么要注意的?把它当作真实邮件服务器运行,意味着你要自己处理 DNS、反向 PTR 记录并绑定特权的 25 端口;而且预置的引导 API 密钥具有通配符作用域,在把主机暴露给任何不受信任的对象之前必须轮换。
这是为谁打造的?它面向构建智能体平台或把电子邮件作为功能出售的团队:每个租户在同一个部署上需要隔离的发信信誉、自己的域名和自己的智能体收件箱。
sentio-mcp 服务器带来了什么?sentio-mcp 是一个独立的 Model Context Protocol 服务器,通过 stdio 把 REST API 暴露为智能体可调用的工具,因此 Claude Desktop、Cursor 或 opencode 等客户端无需定制集成代码,就能列出消息、发送邮件、在线程内回复和管理邮箱。
反垃圾邮件系统是如何工作的?滥用防护分三层:连接级控制(IP 封禁、DNSBL、灰名单、信誉);垃圾评分后端,可以是 rspamd 或内置的贝叶斯分类器(可用垃圾邮件/正常邮件训练);以及针对退信、投诉和退订的抑制列表。
智能体能在已有邮件线程内回复吗?可以,replymessage API/工具会自动设置 In-Reply-To 和 References 头,确保回复在收件人的收件箱中正确串成线程。
有没有不写代码就能探索 API 的方法?有,服务器提供一个交互式 /docs 页面(内置 Test Request 客户端)以及 /openapi.json 上的完整 OpenAPI 3.1 文档,涵盖 85 个路径的 116 个操作,两者都嵌入在二进制中,离线也能使用。
Creator
truespar
Size
2.2 MB
Stars
255
Age
1mo
Commits · 90 Days