Seed

Kiwi Mail

wraps Stalwart Mail Server

Full email, calendar & contacts via JMAP. The wrapper exposes 13 MCP tools so AI agents can search, read, send, and manage your mailbox — all through a single unified API.

License
AGPL-3.0
Protocol
JMAP
Port
8443
Language
Rust

MCP Tools

13 tools exposed to AI agents via the Kiwi MCP gateway. Grouped by domain.

Mail
mail.search
Full-text search across mailboxes with filters for sender, date range, and folder.
query mailbox from after before limit
mail.read
Fetch a single email by ID. Returns headers, body (text + HTML), and attachment metadata.
email_id format
mail.send
Compose and send an email. Supports plain text and HTML bodies, CC/BCC, and attachments by reference.
to subject body cc bcc attachments
mail.reply
Reply to an existing email thread. Automatically sets In-Reply-To and References headers.
email_id body reply_all
mail.archive
Move emails to the archive folder. Accepts a single ID or a list for batch operations.
email_ids
Calendar
calendar.list
List events in a date range. Returns title, time, attendees, and recurrence info.
start end calendar_id
calendar.create_event
Create a new calendar event with optional attendees and recurrence rules.
title start end attendees recurrence location
calendar.find_free_slots
Find available time slots across one or more calendars in a given date range.
start end duration_minutes attendees
calendar.reschedule
Move an existing event to a new time. Sends update notifications to attendees.
event_id new_start new_end
Contacts
contacts.search
Search the address book by name, email, or company. Returns vCard-style contact records.
query limit
contacts.create
Create a new contact entry with structured name, email, phone, and address fields.
name email phone company
contacts.merge
Merge duplicate contacts into a single record, combining fields intelligently.
contact_ids primary_id

Architecture

How the wrapper sits in front of Stalwart inside the LXC container.

Stalwart is a full-featured mail server licensed under AGPL-3.0. The KiwiStack wrapper communicates with it exclusively over HTTP (JMAP protocol on 127.0.0.1:8080), maintaining a strict network boundary between the AGPL-licensed upstream and the Apache-2.0 wrapper.

Because the wrapper never links against Stalwart’s code — it only speaks HTTP to it — the AGPL copyleft does not propagate to the wrapper or any other KiwiStack component. Stalwart runs as an unmodified binary from the upstream Alpine package.

The wrapper handles: JWT validation (tokens from Kiwi ID), JMAP translation (converting Kiwi API calls into JMAP method calls), health checking (polling Stalwart’s health endpoint with exponential backoff), and MCP tool registration (advertising the 13 tools to Kiwi MCP).

LXC container layout
┌──────────────────────────────────┐
  LXC: kiwi-mail                  
                                  
  Stalwart: 127.0.0.1:8080       AGPL-3.0
localhost HTTP (JMAP)   
  Wrapper: 10.10.20.11:8443     Apache-2.0
                                  
  Only wrapper is on the network 
└──────────────────────────────────┘

Config Reference

Wrapper configuration — lives at /etc/kiwi/config.toml.

config.toml
# kiwi-mail wrapper configuration
listen_addr = "10.10.20.11:8443"
upstream_addr = "127.0.0.1:8080"
upstream_bin = "/opt/upstream/bin/stalwart-mail"
upstream_config = "/opt/upstream/etc/config.toml"

health_check_interval = "5s"
health_check_timeout = "60s"

jwt_public_key = "/etc/kiwi/jwt-public.pem"
log_level = "info"
KeyTypeDescription
listen_addrstringBind address for the wrapper (private bridge)
upstream_addrstringStalwart JMAP endpoint on localhost
upstream_binpathPath to the upstream Stalwart binary
upstream_configpathPath to Stalwart’s own config file
health_check_intervaldurationHow often the wrapper polls Stalwart’s health endpoint
health_check_timeoutdurationMax wait for Stalwart to become healthy at startup
jwt_public_keypathKiwi ID public key for JWT validation
log_levelstringTracing level: error, warn, info, debug, trace

Filesystem Layout

How files are organized inside the LXC container.

/
├── opt/
│   ├── upstream/                     # Stalwart (AGPL-3.0)
│   │   ├── bin/stalwart-mail          # unmodified binary from Alpine pkg
│   │   ├── etc/config.toml            # Stalwart config (JMAP port, storage)
│   │   └── lib/                       # shared libs (rocksdb, etc.)
│   └── kiwi/                          # Kiwi wrapper (Apache 2.0)
│       ├── bin/kiwi-mail              # wrapper binary (static musl build)
│       └── version                    # "0.3.1"
├── etc/
│   └── kiwi/
│       ├── config.toml                # wrapper config (see above)
│       └── jwt-public.pem             # Kiwi ID public key
├── var/
│   ├── lib/
│   │   └── upstream/                  # Stalwart data
│   │       ├── db/                    # RocksDB / SQLite databases
│   │       ├── blobs/                 # email blobs (or S3 via kiwi-store)
│   │       └── queue/                 # SMTP delivery queue
│   └── log/
│       └── kiwi/                      # JSON structured logs
└── run/

License

Understanding the AGPL boundary.

Stalwart (upstream) is licensed under AGPL-3.0 — the strongest copyleft license. If you modify Stalwart and serve it over a network, you must share your modifications. However, KiwiStack does not modify Stalwart. It runs the unmodified binary and communicates via HTTP.

The Kiwi Mail wrapper is licensed under Apache-2.0. Because the wrapper only talks to Stalwart over a network socket (JMAP over HTTP), the AGPL copyleft does not propagate. This is the same principle that allows any web browser to talk to an AGPL-licensed server without becoming AGPL itself.

Rule: Never link AGPL code. Never import AGPL crates. Only communicate over the network boundary.

Example Request

An MCP tools/call for mail.search, and the response.

Request
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "mail.search",
    "arguments": {
      "query": "quarterly report",
      "after": "2026-01-01",
      "limit": 5
    }
  }
}
Response
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [{
      "type": "text",
      "text": "Found 3 emails matching 'quarterly report':\n\n1. [Feb 14] From: alice@example.com — Q4 Quarterly Report\n2. [Jan 28] From: bob@example.com — RE: Quarterly Report Draft\n3. [Jan 15] From: carol@example.com — Quarterly Report Template"
    }],
    "isError": false
  }
}

Compatibility

Pinned versions — from compatibility.toml.

kiwi-mail/compatibility.toml
# kiwi-mail/compatibility.toml

[upstream]
name = "stalwart"
version = "0.10.5"
checksum = "sha256:..."

[wrapper]
version = "0.1.0"
rust_edition = "2024"
msrv = "1.85"

[tested]
date = "2026-02-28"
kiwi_id = "0.1.0"