Skip to main content
Glama
Th3R3alDuk3

OWUI-Email-MCP

by Th3R3alDuk3
README.md
# OWUI-Email-MCP

[![Docker](https://github.com/Th3R3alDuk3/OWUI-Email-MCP/actions/workflows/docker.yml/badge.svg)](https://github.com/Th3R3alDuk3/OWUI-Email-MCP/actions/workflows/docker.yml)
[![Version](https://img.shields.io/github/v/tag/Th3R3alDuk3/OWUI-Email-MCP?label=version)](https://github.com/Th3R3alDuk3/OWUI-Email-MCP/tags)
[![Python](https://img.shields.io/badge/python-3.12%2B-blue)](pyproject.toml)
[![License](https://img.shields.io/github/license/Th3R3alDuk3/OWUI-Email-MCP)](LICENSE)

> Email drafts via MCP for OpenWebUI. Template-driven, plain text or HTML, nothing sent.

Email drafting for OpenWebUI over the Model Context Protocol. The model fills
a predefined template; the server stores the finished draft behind a **short
link**. With `HTML_FORMAT=true` the link downloads a ready-to-open `.eml`
file (HTML), otherwise it redirects straight to a `mailto:` draft (plain
text) — either way an editable draft in the user's own mail client. Nothing is
sent automatically, and the model never has to stream the whole email token
by token.

---

## ✨ Highlights

- **Fast by design** — the model outputs a ~10-token short link; the draft
  is built server-side and only lives behind that link
- **Two formats, one switch** — `HTML_FORMAT=true`: `.eml` download marked
  `X-Unsent: 1` that opens as an editable HTML draft (images, inline
  styles); `HTML_FORMAT=false`: instant `mailto:` redirect (plain text)
- **One click to the draft** — either way the draft opens in the user's
  default mail client; links expire after a TTL
- **Nothing is sent** — the user's own mail client opens the draft; the
  user stays in control
- **Multi-user by design** — JWT auth against OpenWebUI's secret, per-user
  rate limiting
- **Stateless-ish** — drafts live in a TTL cache in memory
  (powered by cachetools); no database, no files

## 🚀 Setup

```bash
uv sync
cp .env.example .env
```

`.env.example` documents every setting; the ones you must set:

- `JWT_SECRET` → OpenWebUI's `WEBUI_SECRET_KEY`
- `PUBLIC_BASE_URL` → URL of this server as reachable **from the user's
  browser** (it is embedded in the short links), e.g. `http://192.168.1.10:8000`
- `HTML_FORMAT` → `true` for HTML drafts via `.eml` download, `false` for
  plain text via instant `mailto:` redirect

## 🏃 Run

```bash
uv run python main.py
```

The server listens on `0.0.0.0:8000`:

| Endpoint | Auth | Purpose |
|---|---|---|
| `/mcp` | OpenWebUI JWT | MCP (streamable HTTP) for OpenWebUI's External Tools |
| `/m/{id}` | none | the draft: `mailto:` redirect (txt) or `.eml` download (html) |
| `/static/*` | none | static assets (e.g. the logo referenced by the email templates) |

In OpenWebUI: Admin Settings → External Tools → add server of type
**MCP (Streamable HTTP)** with URL `http://<host>:8000/mcp` and auth
**Session**. The model gets `list_email_templates` and `compose_email` and
is instructed to reply with the short link only.

## 🐳 Docker (optional)

Prebuilt images are published to **ghcr.io** on pushes to `main` (`latest`)
and on version tags (`X.Y.Z`):

```bash
docker run -d -p 8000:8000 \
--restart unless-stopped \
--env-file .env \
-v ./templates:/app/templates \
--name owui-email-mcp \
ghcr.io/th3r3alduk3/owui-email-mcp:latest
```

Or build the image locally: `docker build -t owui-email-mcp .` — the volume
mount keeps the templates editable without rebuilding the image.

## 🛠️ Tools

| Tool | Description |
|---|---|
| `compose_email` | fill a template, store the draft, return the short link |
| `list_email_templates` | list templates (name → template text) |

## 📝 Templates

Templates live in `templates/` (name = filename without suffix) and
are read fresh on every call — edit them without restarting. `HTML_FORMAT`
selects which files are used:

- `HTML_FORMAT=false` — `*.txt` files: first line = subject, blank line,
  then the body; the short link redirects to a `mailto:` draft
- `HTML_FORMAT=true` — `*.html` files: the `<title>` is the subject; the
  short link downloads an `.eml` draft. Use inline styles only (a `<style>`
  block's CSS braces would clash with `str.format`); images referenced as
  `src="/static/..."` are rewritten to absolute `PUBLIC_BASE_URL` links so
  mail clients can load them
- `{placeholders}` are allowed anywhere and are filled via `str.format`
  with the values from the compose call; literal braces are written `{{`
  and `}}`

## ⚠️ Limits

- Drafts are held in memory only and expire after `LINK_TTL`
  (default 24 h); at most `MAX_STORED_EMAILS` are kept. A restart clears
  all links.
- The draft link is unauthenticated by design (the browser opening it has
  no OpenWebUI session): anyone with the unguessable link can read the
  draft until it expires. Don't put secrets in templates.
- MCP requests are rate-limited per user (`RATE_LIMIT_RPS`/`RATE_LIMIT_BURST`),
  keyed on the JWT's `id` claim; tool errors never leak internals
  (`mask_error_details`).
- The server speaks plain HTTP — put it behind a reverse proxy for TLS.