Rustmail
Rewriting an entire Discord bot from scratch. In a language I didnβt know yet.
Modmailbot was the go-to solution for Discord modmail systems - 767 stars, 41 contributors, used by thousands of servers. But it was written in JavaScript, hadnβt seen meaningful updates in years, and the codebase was showing its age. I needed a modmail bot, but more importantly, I wanted to learn Rust properly. So I built Rustmail.
Why Rust?
Three reasons pushed me toward Rust:
-
Learning - Reading tutorials only gets you so far. Building a real project with complex async operations, database interactions, and API integrations forces you to actually understand the language.
-
Performance - JavaScript bots eat memory. A Rust binary runs for months without restarts, uses minimal resources, and handles concurrent operations without breaking a sweat.
-
Reliability - Rustβs type system catches bugs at compile time that would crash a JavaScript bot at 3 AM. No more
undefined is not a function.
What it does
Rustmail connects users with server staff through private DM conversations. User sends a DM β bot creates a ticket channel β staff responds β bot relays messages back. Simple concept, complex implementation.
Core features
| Feature | Description |
|---|---|
| Dual-server mode | Separate community and staff servers |
| Single-server mode | Everything on one server |
| Web panel | Full admin dashboard in the browser |
| REST API | HTTP endpoints for external integrations |
| 10 languages | EN, FR, ES, DE, IT, PT, RU, ZH, JA, KO |
| Message editing | Edit/delete with full change tracking |
| Scheduled closures | Auto-close tickets after delay |
| Reminders & alerts | Never forget to follow up |
| Anonymous replies | Staff can respond without revealing identity |
Architecture
One binary. Everything included.
The entire application - Discord bot, HTTP server, web panel, database engine - compiles into a single executable. No runtime dependencies, no npm install, no external services. Just drop the binary and a config file on any machine and it runs.
rustmail/
βββ rustmail/ # Main bot (Serenity + Axum)
βββ rustmail_panel/ # Web UI (Yew/WASM, embedded in binary)
βββ rustmail_types/ # Shared types
βββ migrations/ # SQLite schema (embedded)
βββ Dockerfile
The web panel is compiled to WebAssembly and embedded directly into the binary. SQLite migrations are baked in at compile time. A single Rust process handles Discord gateway events (Serenity), serves the REST API and panel (Axum), and manages the database (SQLx) - all concurrently via Tokio.
Key dependencies
serenity- Discord API clientaxum- HTTP serversqlx- Async databasetokio- Async runtimeyew- WASM frontend framework
Commands
Both slash commands and text commands. Same functionality, different interface.
# General
/help # Display available commands
/ping # Check bot responsiveness
# Ticket management
/new_thread <user> # Create ticket for user
/close [time] [silent] [cancel] # Close ticket (scheduled or cancel)
/force_close # Force close an orphan ticket
/move_thread <category> # Move to different category
# Messaging
/reply <content> [attachment] [anonymous] # Reply to user
/anonreply <content> # Anonymous reply (text command only)
/edit <message_id> <new_content> # Edit previous message
/delete <message_id> # Delete message
/recover # Force message recovery
/snippet <key> # Use a saved response
# Staff
/add_staff <user> # Add staff to ticket
/remove_staff <user> # Remove staff from ticket
/take # Claim ticket
/release # Release assignment
# Utilities
/add_reminder <time> [content] # Set reminder with optional message
/remove_reminder <reminder_id> # Cancel a reminder
/alert [cancel] # Get notified on reply (or cancel)
/logs # View ticket history
/id # Display user's Discord ID
/status [new_status] # View or change bot status
Web Panel
Full administration without touching Discord:
- Dashboard with statistics
- Real-time ticket management
- Configuration editor
- API key management
- Permission system by user/role
- OAuth2 authentication via Discord
The panel is served directly by the bot on port 3002. Works behind reverse proxies (Nginx, Caddy, Traefik) with SSL termination.
REST API
External integrations through HTTP:
# Create ticket from external source
curl -X POST https://panel.example.com/api/externals/tickets/create \
-H "Content-Type: application/json" \
-H "X-API-Key: rustmail_your_key_here" \
-d '{"discord_id": "123456789012345678"}'
Endpoints for bot control, configuration, tickets, API keys, and administration. Session-based auth for the panel, API keys for external tools.
Configuration
TOML-based config with an online generator:
[bot]
token = ""
status = "DM FOR SUPPORT"
welcome_message = "Your message has been received by the Staff team! We will respond as soon as possible."
close_message = "Thank you for contacting support! Your ticket is now closed."
typing_proxy_from_user = true
typing_proxy_from_staff = true
enable_logs = true
enable_features = true
enable_panel = true
client_id = 0
client_secret = ""
redirect_url = ""
timezone = "Europe/Paris"
panel_super_admin_users = []
panel_super_admin_roles = []
[bot.mode]
type = "single"
guild_id = 0
[command]
prefix = "!"
[thread]
inbox_category_id = 0
embedded_message = true
user_message_color = "3d54ff"
staff_message_color = "ff3126"
system_message_color = "00ff00"
block_quote = true
time_to_close_thread = 5
create_ticket_by_create_channel = true
close_on_leave = false
auto_archive_duration = 10080
[language]
default_language = "en"
fallback_language = "en"
supported_languages = [
"en",
"fr",
]
[error_handling]
show_detailed_errors = false
log_errors = true
send_error_embeds = true
auto_delete_error_messages = true
error_message_ttl = 30
display_errors = true
[notifications]
show_success_on_edit = false
show_partial_success_on_edit = true
show_failure_on_edit = true
show_success_on_reply = false
show_success_on_delete = false
show_success = true
show_error = true
[reminders]
embed_color = "ffb800"
[logs]
show_log_on_edit = true
show_log_on_delete = true
Deployment
Drop and run. Thatβs it.
# Just two files needed
./rustmail # The binary
./config.toml # Your configuration
# Or Docker
docker run -v ./config.toml:/app/config.toml ghcr.io/rustmail/rustmail:latest
No Node.js, no Python, no Java runtime. No npm install that downloads half the internet. The SQLite database is created automatically on first run. Works on Linux, macOS, and Windows.
What I learned
Building Rustmail taught me more about Rust than any book could:
- Async Rust - Tokio, futures,
async/awaitpatterns. Handling concurrent Discord events without data races. - Error handling -
Result<T, E>everywhere. Propagating errors properly instead of swallowing them. - Ownership - Fighting the borrow checker until it clicked. Now it feels like a safety net, not a constraint.
- Macros - SQLx compile-time query checking. Proc macros for command definitions.
- WASM - Building a full frontend in Rust with Yew. Sharing types between backend and frontend.
Compared to modmailbot
| Aspect | modmailbot (JS) | Rustmail |
|---|---|---|
| Language | JavaScript | Rust |
| Deployment | Node.js + npm install | Single binary |
| Memory usage | High | Minimal |
| Web panel | No | Yes |
| REST API | No | Yes |
| Multi-language | Plugin required | Native (10 langs) |
| Type safety | Runtime errors | Compile-time |
Links
Takeaways
- Rewriting a project in a new language is the best way to learn that language
- Rustβs learning curve is steep, but the payoff is real - no 3 AM crashes
- A web panel changes everything for user experience
- SQLite is underrated for single-instance applications
- Good documentation (MdBook) matters as much as good code
- Open source under AGPL-3.0 means anyone can self-host and contribute