smailnail
smailnail is a Go repository for working with IMAP mailboxes and generated test email.
It currently contains three CLIs:
smailnail: search, fetch, mirror, and process mail with a YAML DSL or direct CLI flags
mailgen: generate synthetic email from YAML templates and optionally append it to IMAP
imap-tests: helper commands for creating mailboxes and storing fixture messages
There is now also an initial hosted application binary:
smailnaild: hosted app backend with account CRUD, account tests, mailbox previews, rule CRUD, and rule dry-runs
There is also a dedicated MCP binary for the JavaScript runtime:
smailnail-imap-mcp: exposes executeIMAPJS and getIMAPJSDocumentation
The repository now also contains an initial reusable JavaScript surface:
pkg/services/smailnailjs: a Go service package for rule parsing/building and JS-friendly result shaping
pkg/js/modules/smailnail: a native go-go-goja module exposed as require("smailnail")
Build
cd /home/manuel/workspaces/2026-03-08/update-imap-mcp/smailnail
go build -tags sqlite_fts5 ./cmd/smailnail ./cmd/mailgen ./cmd/imap-tests ./cmd/smailnail-imap-mcp ./cmd/smailnaild
smailnail now requires the sqlite_fts5 build tag because the local mirror and search index depend on SQLite FTS5 being compiled in.
Commands
smailnail
Rule-driven execution:
go run -tags sqlite_fts5 ./cmd/smailnail mail-rules \
--rule examples/smailnail/recent-emails.yaml \
--server imap.example.com \
--username user@example.com \
--password secret \
--mailbox INBOX \
--output json
Direct fetch via flags:
go run -tags sqlite_fts5 ./cmd/smailnail fetch-mail \
--server imap.example.com \
--username user@example.com \
--password secret \
--mailbox INBOX \
--subject-contains "invoice" \
--output json
Local mirror via SQLite plus raw .eml storage:
go run -tags sqlite_fts5 ./cmd/smailnail mirror \
--server imap.example.com \
--username user@example.com \
--password secret \
--mailbox INBOX \
--sqlite-path ./smailnail-mirror.sqlite \
--mirror-root ./smailnail-mirror \
--output json
Run a cautious first sync against a real account:
go run -tags sqlite_fts5 ./cmd/smailnail --log-level info mirror \
--server imap.example.com \
--username user@example.com \
--password secret \
--mailbox INBOX \
--sqlite-path ./smailnail-mirror.sqlite \
--mirror-root ./smailnail-mirror \
--since-days 30 \
--max-messages 200 \
--output json
Run a wider account sync with mailbox filters and partial-failure continuation:
go run -tags sqlite_fts5 ./cmd/smailnail --log-level info mirror \
--server imap.example.com \
--username user@example.com \
--password secret \
--all-mailboxes \
--mailbox-pattern 'Archive/*' \
--exclude-mailbox-pattern 'Archive/Spam*' \
--stop-on-error=false \
--sqlite-path ./smailnail-mirror.sqlite \
--mirror-root ./smailnail-mirror \
--output json
Print the mirror plan without creating local files:
go run -tags sqlite_fts5 ./cmd/smailnail mirror \
--server imap.example.com \
--username user@example.com \
--password secret \
--mailbox INBOX \
--print-plan \
--output json
Reconcile the full mailbox and mark locally mirrored rows as remote_deleted when the server no longer reports them:
go run -tags sqlite_fts5 ./cmd/smailnail mirror \
--server imap.example.com \
--username user@example.com \
--password secret \
--mailbox INBOX \
--sqlite-path ./smailnail-mirror.sqlite \
--mirror-root ./smailnail-mirror \
--reconcile-full-mailbox \
--output json
mailgen
go run ./cmd/mailgen generate \
--configs examples/mailgen/simple.yaml \
--write-files \
--output-dir ./output \
--output json
To append generated mail to IMAP:
go run ./cmd/mailgen generate \
--configs examples/mailgen/simple.yaml \
--store-imap \
--server imap.example.com \
--username user@example.com \
--password secret \
--mailbox INBOX \
--output json
imap-tests
Create a mailbox:
go run ./cmd/imap-tests create-mailbox \
--server imap.example.com \
--username user@example.com \
--password secret \
--new-mailbox Scratch \
--output json
Store a text message:
go run ./cmd/imap-tests store-text-message \
--server imap.example.com \
--username user@example.com \
--password secret \
--mailbox INBOX \
--from "Sender <sender@example.com>" \
--to "Recipient <recipient@example.com>" \
--subject "Fixture message" \
--output json
smailnail-imap-mcp
List the exposed MCP tools:
go run ./cmd/smailnail-imap-mcp mcp list-tools
Start the server over stdio, SSE, or streamable HTTP:
go run ./cmd/smailnail-imap-mcp mcp start --transport stdio
go run ./cmd/smailnail-imap-mcp mcp start --transport sse --port 3201
go run ./cmd/smailnail-imap-mcp mcp start
The default HTTP deployment shape now is:
- transport:
streamable_http
- port:
3201
The server intentionally exposes only two tools:
executeIMAPJS: run JavaScript against require("smailnail")
getIMAPJSDocumentation: query embedded package/symbol/example/concept docs or render markdown
Production packaging and Coolify deployment notes are in docs/deployments/smailnail-imap-mcp-coolify.md. The repository root Dockerfile is now the Coolify-facing build entrypoint for this MCP service.
smailnaild
The hosted backend now requires an encryption key for stored IMAP credentials.
Pass it through the Glazed encryption section:
ENCRYPTION_KEY="$(openssl rand -base64 32)"
go run ./cmd/smailnaild serve \
--encryption-key-base64 "$ENCRYPTION_KEY"
Start the hosted backend with the default SQLite app database:
go run ./cmd/smailnaild serve \
--encryption-key-base64 "$ENCRYPTION_KEY"
That defaults to:
- bind address
0.0.0.0:8080
- application DB
smailnaild.sqlite
Useful endpoints:
GET /healthz
GET /readyz
GET /api/info
GET /api/accounts
POST /api/accounts
POST /api/accounts/:id/test
GET /api/accounts/:id/mailboxes
GET /api/accounts/:id/messages
GET /api/accounts/:id/messages/:uid
GET /api/rules
POST /api/rules
POST /api/rules/:id/dry-run
Use Clay SQL flags to point it at another database. For example, Postgres via DSN:
go run ./cmd/smailnaild serve \
--listen-host 0.0.0.0 \
--listen-port 8080 \
--dsn 'postgres://user:pass@localhost:5432/smailnail?sslmode=disable'
Or SQLite with an explicit file path:
go run ./cmd/smailnaild serve \
--db-type sqlite \
--database ./data/smailnaild.sqlite
Local hosted-account testing notes and curl examples are in docs/smailnaild-local-account-flow.md.
For the React/Vite UI in ui/, start the backend and dev server separately:
go run ./cmd/smailnaild serve \
--encryption-key-base64 "$ENCRYPTION_KEY"
cd ui
pnpm run dev
The UI now defaults to http://localhost:5050 and proxies /api to http://localhost:8080.
To point the UI at another backend, copy ui/.env.example to ui/.env.local and set either:
SMAILNAIL_UI_BACKEND_URL=http://localhost:3001
- or
SMAILNAIL_UI_BACKEND_PORT=3001
Environment variables
The Cobra parser is configured with app name smailnail, so shared IMAP settings can be supplied with SMAILNAIL_* variables such as:
SMAILNAIL_SERVER
SMAILNAIL_PORT
SMAILNAIL_USERNAME
SMAILNAIL_PASSWORD
SMAILNAIL_MAILBOX
SMAILNAIL_INSECURE
The hosted binary uses app name smailnaild, so its flags can also be supplied through SMAILNAILD_* environment variables.
Important hosted-backend encryption fields:
--encryption-key-base64: base64-encoded 32-byte AES-GCM key for encrypting stored IMAP passwords
--encryption-key-id: logical key identifier stored with encrypted secrets
Because these are normal Glazed fields, they can also be supplied through the command environment layer, for example:
SMAILNAILD_ENCRYPTION_KEY_BASE64
SMAILNAILD_ENCRYPTION_KEY_ID
Examples
- DSL examples:
examples/smailnail/*.yaml
- mailgen configs:
examples/mailgen/*.yaml
- quick start:
examples/smailnail/QUICK-START.md
Docker IMAP fixture
The maintained smoke script looks for the Dovecot fixture in this order:
DOCKER_IMAP_FIXTURE_ROOT
../docker-test-dovecot relative to the smailnail repo root
Start it with:
cd /path/to/docker-test-dovecot
docker compose up -d --build
The default test users are a, b, c, and d, each with password pass.
To run the maintained end-to-end smoke test:
cd /path/to/smailnail
make smoke-docker-imap
If the fixture lives somewhere else locally, override it with DOCKER_IMAP_FIXTURE_ROOT=/path/to/docker-test-dovecot.
To validate the local mirror against the bundled Docker Compose fixture in this repo:
cd /home/manuel/workspaces/2026-04-01/smailnail-sqlite/smailnail
docker compose -f docker-compose.local.yml up -d dovecot
go run ./cmd/imap-tests store-text-message \
--server 127.0.0.1 \
--port 993 \
--username a \
--password pass \
--mailbox INBOX \
--insecure \
--from seed@example.com \
--to a@test.local \
--subject "Mirror fixture message" \
--output json
go run -tags sqlite_fts5 ./cmd/smailnail mirror \
--server 127.0.0.1 \
--port 993 \
--username a \
--password pass \
--mailbox INBOX \
--insecure \
--sqlite-path /tmp/smailnail-mirror.sqlite \
--mirror-root /tmp/smailnail-mirror \
--output json
docker compose -f docker-compose.local.yml down
Local Dovecot + Keycloak Stack
For hosted-app and OIDC work, the repo now includes a local Docker Compose stack with:
- Dovecot test fixture on the usual local IMAP ports
- Keycloak on
http://127.0.0.1:18080
- PostgreSQL backing Keycloak persistence
Start it with:
cd /home/manuel/workspaces/2026-03-08/update-imap-mcp/smailnail
docker compose -f docker-compose.local.yml up -d
Useful endpoints and defaults:
- Dovecot IMAPS:
127.0.0.1:993
- Dovecot test users:
a, b, c, d
- Dovecot password:
pass
- Keycloak admin:
http://127.0.0.1:18080/admin
- Keycloak bootstrap admin username:
admin
- Keycloak bootstrap admin password:
admin
- Imported realm:
smailnail-dev
- Realm issuer:
http://127.0.0.1:18080/realms/smailnail-dev
The stack also imports two initial OIDC clients in the smailnail-dev realm:
smailnail-web
smailnail-mcp
Predictable local auth defaults after a fresh Keycloak reset:
- Keycloak admin user:
admin
- Keycloak admin password:
admin
smailnail-web client secret: smailnail-web-secret
- local test user:
alice
- local test user password:
secret
Key references for the shared-identity slice:
Stop it with:
docker compose -f docker-compose.local.yml down
To run the hosted-backend integration suite against the local Dovecot fixture:
cd /home/manuel/workspaces/2026-03-08/update-imap-mcp/smailnail
export SMAILNAILD_ENCRYPTION_KEY_BASE64="$(openssl rand -base64 32)"
SMAILNAILD_DOVECOT_TEST=1 go test ./pkg/smailnaild/...
SMAILNAILD_DOVECOT_TEST=1 go test -tags sqlite_fts5 ./...
To run the full shared OIDC + stored-account + local Dovecot smoke:
cd /home/manuel/workspaces/2026-03-08/update-imap-mcp/smailnail
SMAILNAIL_LOCAL_STACK_TEST=1 go test ./pkg/mcp/imapjs -run TestExecuteIMAPJSAgainstLocalKeycloakAndDovecot -v
JavaScript module smoke
To validate the initial JavaScript service/module slice:
cd /path/to/smailnail
make smoke-js-module
That smoke path runs the service-layer tests and the goja runtime integration tests that prove require("smailnail") works.
To validate the dedicated MCP binary and docs registry:
cd /path/to/smailnail
make smoke-imap-js-mcp