A Cloud Native SMTP mail sender for Kubernetes and modern infrastructure.
[!NOTE] Due to limitations of AWS, GCP, etc. on port 25, this project will not work on cloud providers that block port 25.
Planned:
A single SendHTML / SendTemplate API call creates one Batch with N Deliveries (one per Recipient). Deliveries flow through the Pool, are built into Envelopes by the Dispatcher, and transmitted by the SMTPSender. See CONTEXT.md for the full shared language (Batch, Recipient, Delivery, Envelope, Domain, Template) and the per-Delivery outcome state machine (Validated → Delivered / Bounced, plus Opened / Clicked engagement events).
Kannon is composed of several microservices and workers:
SendHTML / SendTemplate and creates Batches with N Deliveries; together with the Admin API (Domains, Templates, API Keys) and Stats API, it forms the gRPC surface.to_validate, validates the recipient address, and either schedules or rejects them.kannon.stats.* events and persists them.All components can be enabled/disabled via CLI flags or config.
See
ARCHITECTURE.mdfor a full breakdown of modules, NATS streams, topics, consumers, and message flows.
flowchart TD
subgraph Core
API["gRPC API (Mailer / Admin / Stats)"]
SMTPServer["SMTPServer (inbound DSN/bounce)"]
SMTPSender["SMTPSender (outbound)"]
Dispatcher["Dispatcher"]
Validator["Validator"]
Tracker["Tracker (open/click)"]
Stats["Stats"]
end
DB[(PostgreSQL)]
NATS[(NATS)]
API <--> DB
Dispatcher <--> DB
SMTPSender <--> DB
Validator <--> DB
Stats <--> DB
API <--> NATS
SMTPSender <--> NATS
Dispatcher <--> NATS
SMTPServer <--> NATS
Stats <--> NATS
Validator <--> NATS
Tracker <--> NATS
Run all Kannon components in a single process with embedded NATS (only PostgreSQL required):
git clone https://github.com/kannon-email/kannon.git
cd kannon
go build -o kannon .
./kannon standalone --config ./config.yaml
This mode:
git clone https://github.com/kannon-email/kannon.git
cd kannon
go build -o kannon .
./kannon --run-api --run-smtp --run-sender --run-dispatcher --config ./config.yaml
Note: This mode requires an external NATS server configured in your config file.
See examples/docker-compose/ for ready-to-use files.
docker-compose -f examples/docker-compose/docker-compose.yaml up
examples/docker-compose/kannon.yaml to configure your environment.make test — Run all testsmake generate — Generate DB and proto codemake lint — Run lintersKannon can be configured via YAML file, environment variables, or CLI flags. Precedence: CLI > Env > YAML.
Main config options:
| Key / Env Var | Type | Default | Description |
|---|---|---|---|
database_url / K_DATABASE_URL |
string | (required) | PostgreSQL connection string |
nats_url / K_NATS_URL |
string | (required) | NATS server URL for internal messaging |
debug / K_DEBUG |
bool | false | Enable debug logging |
api.port / K_API_PORT |
int | 50051 | gRPC API port |
sender.hostname / K_SENDER_HOSTNAME |
string | (required) | Hostname for outgoing mail |
sender.max_jobs / K_SENDER_MAX_JOBS |
int | 10 | Max parallel sending jobs |
sender.demo_sender / K_SENDER_DEMO_SENDER |
bool | false | Enable demo sender mode for testing |
smtp.address / K_SMTP_ADDRESS |
string | :25 | SMTP server listen address |
smtp.domain / K_SMTP_DOMAIN |
string | localhost | SMTP server domain |
smtp.read_timeout / K_SMTP_READ_TIMEOUT |
duration | 10s | SMTP read timeout |
smtp.write_timeout / K_SMTP_WRITE_TIMEOUT |
duration | 10s | SMTP write timeout |
smtp.max_payload / K_SMTP_MAX_PAYLOAD |
size | 1024kb | Max SMTP message size |
smtp.max_recipients / K_SMTP_MAX_RECIPIENTS |
int | 50 | Max recipients per SMTP message |
tracker.port / K_TRACKER_PORT |
int | 8080 | Open/click tracking HTTP server port |
run-api / K_RUN_API |
bool | false | Enable API server |
run-smtp / K_RUN_SMTP |
bool | false | Enable SMTP server |
run-sender / K_RUN_SENDER |
bool | false | Enable sender worker |
run-dispatcher / K_RUN_DISPATCHER |
bool | false | Enable dispatcher worker |
run-validator / K_RUN_VALIDATOR |
bool | false | Enable validator worker |
run-tracker / K_RUN_TRACKER |
bool | false | Enable tracker worker |
run-stats / K_RUN_STATS |
bool | false | Enable stats worker |
config |
string | ~/.kannon.yaml | Path to config file |
examples/docker-compose/kannon.yaml for a full example.Deprecated aliases:
run-verifier/K_RUN_VERIFIERcontinue to work as aliases forrun-validator/K_RUN_VALIDATOR;run-bounce/K_RUN_BOUNCEcontinue to work as aliases forrun-tracker/K_RUN_TRACKER; and thebump:YAML section /K_BUMP_PORTenv var continue to work as aliases fortracker:/K_TRACKER_PORT. They will be removed in a future major version.
Kannon requires a PostgreSQL database. Main tables (physical names retained for backward compatibility; see CONTEXT.md for the corresponding domain entities):
See db/migrations/ for full schema and migrations.
Kannon exposes a gRPC API for sending mail, managing domains/templates, and retrieving stats.
SendHTML: Send a raw HTML emailSendTemplate: Send an email using a stored templateGetDomains, GetDomain, CreateDomainCreateTemplate, UpdateTemplate, DeleteTemplate, GetTemplate, GetTemplatesCreateAPIKey, ListAPIKeys, GetAPIKey, DeactivateAPIKeyGetStats, GetStatsAggregatedAll gRPC APIs use Basic Auth with your domain and API key:
token = base64(<your domain>:<your api key>)
Pass this in the Authorization metadata for gRPC calls:
{
"Authorization": "Basic <your token>"
}
{
"sender": {
"email": "no-reply@yourdomain.com",
"alias": "Your Name"
},
"recipients": ["user@example.com"],
"subject": "Test",
"html": "<h1>Hello</h1><p>This is a test.</p>",
"attachments": [
{ "filename": "file.txt", "content": "base64-encoded-content" }
],
"fields": { "custom": "value" },
"headers": {
"to": ["visible-recipient@example.com"],
"cc": ["cc@example.com"]
}
}
The optional headers field allows overriding the To and adding a Cc header on sent emails. The SMTP envelope recipient (actual delivery target) remains the pool recipient, but the visible mail headers will use the values from headers:
to: Overrides the To header displayed in the email clientcc: Adds a Cc header to the emailThis is useful for scenarios where you want the email to appear addressed to a group or alias while delivering to individual recipients.
See the proto files for all fields and options.
k8s/deployment.yaml for a production-ready manifest.examples/docker-compose/ for local or test deployments.To send mail, you must register a sender domain and configure DNS:
<SENDER_NAME> → your server IP<SENDER_NAME><SENDER_NAME> → v=spf1 ip4:<YOUR SENDER IP> -allsmtp._domainkey.<YOUR_DOMAIN> → k=rsa; p=<YOUR DKIM KEY HERE>SendHTML or SendTemplate) with Basic Auth as above.Kannon includes a demo sender mode for testing and development without actually sending emails. This is particularly useful for:
Set sender.demo_sender: true in your configuration:
sender:
hostname: kannon.example.com
max_jobs: 10
demo_sender: true # Enable demo sender mode
Or via environment variable:
export K_SENDER_DEMO_SENDER=true
When demo mode is enabled:
This mode mocks the SMTP client and does not actually send emails.
IMPROVEMENTS:
# Start Kannon with demo sender enabled
./kannon --run-api --run-sender --run-dispatcher \
--sender-demo-sender \
--sender-hostname test.example.com \
--config ./config.yaml
# Send test emails via API
# Emails will be processed but not actually sent
# Statistics will be collected normally
This makes it easy to test your email integration without setting up SMTP servers or worrying about deliverability during development.
We welcome contributions! Please:
go build -o kannon .make test or go test ./...make generatemake lintinternal/, pkg/, and run with go test ./...For developers building integrations with Kannon, we provide a complete local testing environment using Docker Compose with demo sender mode. This allows you to test the entire email pipeline without actually sending emails or requiring SMTP server setup.
The simplest way to get started is using the provided Docker Compose configuration:
cd examples/docker-compose/
docker-compose up
This will start:
The API will be available at localhost:50051 (gRPC).
When using the docker-compose setup, demo sender mode is automatically enabled (demo_sender: true in kannon.yaml), which means:
The demo environment uses the configuration in examples/docker-compose/kannon.yaml:
sender:
hostname: kannon.ludusrusso.dev
max_jobs: 100
demo_sender: true # Mock SMTP sending
# All components enabled for full testing
run-smtp: true
run-tracker: true
run-dispatcher: true
run-validator: true
run-sender: true
run-api: true
run-stats: true
Start the environment:
cd examples/docker-compose/
docker-compose up -d
Create a test domain (using grpcurl or your gRPC client):
# Register a domain for testing
grpcurl -plaintext -d '{"domain":"test.example.com"}' \
localhost:50051 kannon.admin.apiv1.AdminApiV1Service/CreateDomain
Send test emails via the API:
# Send HTML email (will be processed but not actually sent)
grpcurl -plaintext \
-H "Authorization: Basic $(echo -n 'test.example.com:your-domain-key' | base64)" \
-d '{"sender":{"email":"test@test.example.com","alias":"Test"},"recipients":["user@example.com"],"subject":"Test Email","html":"<h1>Hello World</h1>"}' \
localhost:50051 kannon.mailer.apiv1.MailerApiV1Service/SendHTML
Check statistics:
# View delivery stats (will show processed emails)
grpcurl -plaintext \
-H "Authorization: Basic $(echo -n 'test.example.com:your-domain-key' | base64)" \
localhost:50051 kannon.stats.apiv1.StatsApiV1Service/GetStats
localhost:50051 as your Kannon endpointSendTemplate API with your template designsTo modify the demo environment:
examples/docker-compose/kannon.yaml for Kannon configurationexamples/docker-compose/docker-compose.yaml for infrastructure changesdocker-compose down && docker-compose upWhen ready for production, simply change demo_sender: false in your configuration and provide real SMTP credentials. Your integration code remains unchanged.
Kannon is licensed under the Apache 2.0 License. See LICENSE for details.