Rustmail Documentation
Welcome to the Rustmail documentation. This guide covers installation, configuration, usage, and development of the Rustmail Discord modmail bot.
Getting Started
New to Rustmail? Start here:
- Installation - Download and system requirements
- Configuration - Set up your
config.toml - First Steps - Launch the bot and verify setup
User Guides
Learn how to use Rustmail effectively:
| Guide | Description |
|---|---|
| Commands | Complete reference for all slash and text commands |
| Server Modes | Single-server vs dual-server architecture |
| Tickets | Managing support tickets and conversations |
| Web Panel | Using the administration interface |
Reference
Technical documentation for advanced users:
| Document | Description |
|---|---|
| Configuration Options | All config.toml settings explained |
| REST API | HTTP endpoints for integrations |
| Database Schema | SQLite table structures |
Deployment
Production deployment guides:
| Guide | Description |
|---|---|
| Docker | Container-based deployment |
| Production | Best practices for production environments |
Development
For contributors and developers:
| Document | Description |
|---|---|
| Architecture | Project structure and design |
| Building | Compile from source |
| Contributing | Contribution guidelines |
Quick Links
Installation
This guide covers downloading and setting up Rustmail on your system.
Prerequisites
Discord Bot Application
Before installing Rustmail, create a Discord application:
- Go to the Discord Developer Portal
- Click New Application and give it a name
- Navigate to Bot in the sidebar
- Click Add Bot
- Under Privileged Gateway Intents, enable:
- Presence Intent
- Server Members Intent
- Message Content Intent
- Copy the bot token (you will need it for configuration)
Bot Invitation
Invite the bot to your server(s) with the required permissions:
- Go to OAuth2 > URL Generator
- Select scopes:
bot,applications.commands - Select permissions:
- Manage Channels
- Read Messages/View Channels
- Send Messages
- Manage Messages
- Embed Links
- Attach Files
- Read Message History
- Add Reactions
- Use Slash Commands
- Copy the generated URL and open it in your browser
- Select your server and authorize
For dual-server mode, invite the bot to both servers.
Download
Pre-built Binaries
Download the latest release from GitHub Releases.
Available platforms:
- Linux (x86_64)
- Windows (x86_64)
- macOS (x86_64, ARM64)
Extract the archive to your desired installation directory.
Docker
Pull the official image:
docker pull ghcr.io/rustmail/rustmail:latest
See Docker Deployment for complete container setup.
Build from Source
See Building for compilation instructions.
Directory Structure
After extraction, your installation directory should contain:
rustmail/
├── rustmail # Main executable (rustmail.exe on Windows)
└── config.toml # Configuration file (create this)
On first run, the bot creates:
rustmail/
├── rustmail
├── config.toml
└── db
└── db.sqlite # SQLite database (auto-created)
Next Steps
Proceed to Configuration to set up your config.toml file.
Configuration
This guide explains how to configure Rustmail using the config.toml file.
Using the Configuration Generator
The easiest way to create your configuration is the online generator:
The generator walks you through each setting and produces a valid config.toml file. You can also build the
configurator locally from the rustmail_configurator repository.
Manual Configuration
If you prefer to create the configuration manually, copy config.example.toml and edit it:
cp config.example.toml config.toml
Below is an overview of each configuration section. For a complete reference of all options, see Configuration Reference.
Essential Settings
Bot Section
[bot]
token = "YOUR_BOT_TOKEN"
status = "DM for support"
welcome_message = "Your message has been received. Staff will respond shortly."
close_message = "This ticket has been closed. Thank you for contacting us."
| Field | Description |
|---|---|
token | Your Discord bot token from the Developer Portal |
status | Text displayed as the bot’s activity status |
welcome_message | Sent to users when they open a new ticket |
close_message | Sent to users when their ticket is closed |
Server Mode
Rustmail supports two operating modes. Choose based on your server structure.
Single-server mode - Everything on one Discord server:
[bot.mode]
type = "single"
guild_id = 123456789012345678
Dual-server mode - Separate community and staff servers:
[bot.mode]
type = "dual"
community_guild_id = 123456789012345678
staff_guild_id = 987654321098765432
In dual-server mode:
community_guild_idis where your users arestaff_guild_idis where ticket channels are created
See Server Modes for detailed guidance on choosing and configuring modes.
Thread Settings
[thread]
inbox_category_id = 123456789012345678
embedded_message = true
user_message_color = "3d54ff"
staff_message_color = "ff3126"
The inbox_category_id is required. Create a category in your staff server (or your single server) and copy its ID. All
ticket channels will be created under this category.
Web Panel Configuration
The web panel provides browser-based administration. Enabling it requires OAuth2 setup.
OAuth2 Setup
- In the Discord Developer Portal, select your application
- Go to OAuth2 > General
- Copy the Client ID and Client Secret
- Add a redirect URL (see below)
[bot]
enable_panel = true
client_id = 123456789012345678
client_secret = "your_oauth2_client_secret"
redirect_url = "http://localhost:3002/api/auth/callback"
Understanding redirect_url vs ip
These two fields serve different purposes and are often confused:
| Field | Purpose | Required |
|---|---|---|
redirect_url | Public URL for OAuth2 authentication and log links | Yes (if panel enabled) |
ip | Network interface binding address | No (defaults to auto-detect) |
The redirect_url Field (Important)
The redirect_url is your panel’s public URL. It is used for:
- OAuth2 authentication - Discord redirects users here after login
- Log links - Links to ticket logs sent in your logs channel
It must:
- Match exactly what you configured in the Discord Developer Portal
- End with
/api/auth/callback - Be accessible from the internet (or your network for local use)
Local development:
redirect_url = "http://localhost:3002/api/auth/callback"
Production with domain (behind reverse proxy):
redirect_url = "https://panel.example.com/api/auth/callback"
LAN access (no domain):
redirect_url = "http://192.168.1.100:3002/api/auth/callback"
The ip Field (Optional)
[bot]
ip = "192.168.1.100" # Optional
The ip field controls which network interface the panel server binds to. This is a technical setting for advanced
network configurations.
- If omitted, Rustmail auto-detects your local IP
- If the IP is invalid or unavailable, it falls back to
0.0.0.0(all interfaces)
When to set it manually:
- Running in Docker with host networking
- When auto-detection returns an incorrect interface
- When you need to bind to a specific network interface
For most users: Leave ip unset and focus on configuring redirect_url correctly.
Reverse Proxy Setup
For production deployments, you typically run Rustmail behind a reverse proxy (Nginx, Caddy, Traefik, NPM, etc.) with a custom domain.
Architecture
Internet → Reverse Proxy (443) → Rustmail (3002)
↓
SSL/TLS termination
Domain: panel.example.com
Nginx Proxy Manager (NPM)
-
Add Proxy Host:
- Domain:
panel.example.com - Scheme:
http - Forward Hostname/IP: Your server’s internal IP or
localhost - Forward Port:
3002 - Enable SSL with Let’s Encrypt
- Domain:
-
Configure Rustmail:
[bot] enable_panel = true redirect_url = "https://panel.example.com/api/auth/callback" -
Update Discord OAuth2:
- Add
https://panel.example.com/api/auth/callbackto your redirect URIs
- Add
Nginx Configuration
server {
listen 443 ssl http2;
server_name panel.example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://127.0.0.1:3002;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Caddy Configuration
panel.example.com {
reverse_proxy localhost:3002
}
Traefik Labels (Docker)
labels:
- "traefik.enable=true"
- "traefik.http.routers.rustmail.rule=Host(`panel.example.com`)"
- "traefik.http.routers.rustmail.tls.certresolver=letsencrypt"
- "traefik.http.services.rustmail.loadbalancer.server.port=3002"
Common Issues
OAuth2 redirect mismatch:
The redirect URL in config.toml must exactly match one of the URLs configured in Discord Developer Portal. Check for:
- Protocol mismatch (
httpvshttps) - Trailing slashes
- Port numbers
Panel not accessible:
- Verify the reverse proxy can reach port 3002
- Check firewall rules
- Ensure Rustmail is running and panel is enabled
Panel Administrators
Define super administrators who have full panel access:
[bot]
panel_super_admin_users = [123456789012345678]
panel_super_admin_roles = [987654321098765432]
panel_super_admin_users- List of Discord user IDspanel_super_admin_roles- List of Discord role IDs
Users matching either list have unrestricted panel access. Additional permissions can be granted through the panel itself.
Language Settings
[language]
default_language = "en"
fallback_language = "en"
supported_languages = ["en", "fr", "es", "de"]
Available language codes: en, fr, es, de, it, pt, ru, zh, ja, ko
Complete Example
[bot]
token = "YOUR_BOT_TOKEN"
status = "DM for support"
welcome_message = "Your message has been received. Staff will respond shortly."
close_message = "This ticket has been closed. Thank you."
typing_proxy_from_user = true
typing_proxy_from_staff = true
enable_logs = true
enable_features = false
enable_panel = true
client_id = 123456789012345678
client_secret = "your_client_secret"
redirect_url = "https://panel.example.com/api/auth/callback"
timezone = "Europe/Paris"
logs_channel_id = 123456789012345678
panel_super_admin_users = [123456789012345678]
panel_super_admin_roles = []
[bot.mode]
type = "dual"
community_guild_id = 123456789012345678
staff_guild_id = 987654321098765432
[command]
prefix = "!"
[thread]
inbox_category_id = 123456789012345678
embedded_message = true
user_message_color = "5865f2"
staff_message_color = "57f287"
system_message_color = "faa81a"
block_quote = true
time_to_close_thread = 0
create_ticket_by_create_channel = false
[language]
default_language = "en"
fallback_language = "en"
supported_languages = ["en", "fr"]
[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
[logs]
show_log_on_edit = true
show_log_on_delete = true
[reminders]
embed_color = "ffb800"
[error_handling]
show_detailed_errors = false
log_errors = true
send_error_embeds = true
auto_delete_error_messages = true
error_message_ttl = 30
Next Steps
After creating your configuration:
- Verify the file is named
config.tomland is in the same directory as the executable - Proceed to First Steps to launch the bot
First Steps
This guide walks you through launching Rustmail and verifying your setup.
Starting the Bot
Direct Execution
From your installation directory:
./rustmail
On Windows:
rustmail.exe
Expected Output
On successful startup, you will see:
[INFO] Database connection pool established
[INFO] Database connected!
[INFO] Starting rustmail...
[INFO] listening on 0.0.0.0:3002
[INFO] Configuration successfully validated!!
[INFO] Mode: Mono server (ID: 711880297272311856)
[INFO] Rustmail is online !
[INFO] All pending reminders have been scheduled.
[INFO] Updated 0 ticket statuses
If there are configuration errors, the bot will display specific messages indicating what needs to be fixed.
Verifying the Setup
1. Check Bot Status
In Discord, verify the bot appears online in your server’s member list. Its status should display the text you
configured in bot.status.
2. Test Slash Commands
In any channel where the bot has access:
- Type
/helpand press Enter - The bot should respond with a list of available commands
If slash commands don’t appear:
- Wait a few minutes (Discord caches command registrations)
- Verify the bot has the
applications.commandsscope - Check that the bot has permissions in the channel
3. Test Ticket Creation
- Send a direct message to the bot
- The bot should:
- Reply with your configured
welcome_message - Create a new channel in your inbox category
- Reply with your configured
- Staff can now respond using
/replyor!replyin the ticket channel
4. Access the Panel (if enabled)
Open your browser and navigate to:
- Local:
http://localhost:3002 - With reverse proxy:
https://your-panel-domain.com
Click Login to authenticate with Discord.
Common Startup Issues
Configuration Parse Error
Failed to parse config.toml: ...
Check your config.toml for:
- Missing required fields
- Incorrect TOML syntax (missing quotes, brackets)
- Invalid color hex codes (should be 6 characters without
#)
Invalid Server ID
Main server not found: ...
Verify:
- The guild IDs in your configuration are correct
- The bot has been invited to all configured servers
- You’re using the server ID, not a channel or user ID
Logs/Features Channel Required
'logs_channel_id' field is required if 'enable_logs' is true
Either:
- Set
enable_logs = falseto disable logging - Or provide a valid
logs_channel_id
The same applies to enable_features and features_channel_id.
OAuth2 Errors
If the panel login fails:
- Verify
client_idandclient_secretmatch your Discord application - Ensure
redirect_urlexactly matches what’s configured in Discord Developer Portal - Check that your application has the OAuth2 redirect URI added
Running as a Service
For production, run Rustmail as a background service.
Systemd (Linux)
Create /etc/systemd/system/rustmail.service:
[Unit]
Description=Rustmail Discord Bot
After=network.target
[Service]
Type=simple
User=rustmail
WorkingDirectory=/opt/rustmail
ExecStart=/opt/rustmail/rustmail
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
Enable and start:
sudo systemctl daemon-reload
sudo systemctl enable rustmail
sudo systemctl start rustmail
View logs:
sudo journalctl -u rustmail -f
Docker
See Docker Deployment for containerized operation.
Next Steps
Your bot is now running. Learn more about:
- Commands - All available commands
- Tickets - Managing support tickets
- Web Panel - Using the administration interface
Commands Reference
Rustmail provides both slash commands and text commands. Text commands use a configurable prefix (default: !).
Command Types
| Type | Example | Description |
|---|---|---|
| Slash | /reply hello | Discord’s native slash commands with autocomplete |
| Text | !reply hello | Traditional prefix-based commands |
Both types offer identical functionality. Slash commands provide better discoverability and parameter hints, while text commands may be faster for experienced users.
General Commands
help
Display available commands.
| Slash | Text |
|---|---|
/help | !help |
ping
Check bot responsiveness.
| Slash | Text |
|---|---|
/ping | !ping |
Ticket Management
new_thread
Create a new ticket for a user. Use when you need to initiate contact.
| Slash | Text |
|---|---|
/new_thread <user> | !new_thread <user_id> |
Parameters:
user- The Discord user to create a ticket for
close
Close the current ticket.
| Slash | Text |
|---|---|
/close [time] [silent] [cancel] | !close [time] [-s|--silent] [-c|--cancel] |
Parameters:
time- Schedule closure after duration (e.g.,1h,30m,2d)silent- Close without notifying the usercancel- Cancel a scheduled closure
Examples:
/close # Close immediately
/close time:2h # Close in 2 hours
/close silent:true # Close without notification
/close cancel:true # Cancel scheduled closure
!close # Close immediately
!close 2h # Close in 2 hours
!close -s # Close silently
!close cancel # Cancel scheduled closure
force_close
Close an orphaned ticket when the user has left the server.
| Slash | Text |
|---|---|
/force_close | !force_close |
move_thread
Move a ticket to a different category.
| Slash | Text |
|---|---|
/move_thread <category> | !move_thread <category_name> |
Parameters:
category- Target category name
recover
Manually trigger message recovery. Rustmail automatically recovers messages sent while the bot was offline, but you can force a recovery check with this command.
| Slash | Text |
|---|---|
/recover | !recover |
Messaging
reply
Send a message to the ticket user.
| Slash | Text |
|---|---|
/reply <content> [attachment] [anonymous] | !reply <content> |
Parameters:
content- Message textattachment- Optional file attachmentanonymous- Send without revealing staff identity
For text commands, attach files using Discord’s upload feature. For anonymous replies, use !anonreply.
anonreply
Send an anonymous reply (text command only).
| Text |
|---|
!anonreply <content> |
Equivalent to /reply anonymous:true.
edit
Edit a previous message you sent.
| Slash | Text |
|---|---|
/edit <message_id> <new_content> | !edit <message_id> <new_content> |
Parameters:
message_id- Rustmail message ID (shown in message footer)new_content- Updated message text
delete
Delete a message you sent.
| Slash | Text |
|---|---|
/delete <message_id> | !delete <message_id> |
Parameters:
message_id- Rustmail message ID (shown in message footer)
Staff Management
add_staff
Grant a staff member access to the current ticket.
| Slash | Text |
|---|---|
/add_staff <user> | !add_staff <user_id> |
Parameters:
user- Staff member to add
remove_staff
Remove a staff member’s access to the current ticket.
| Slash | Text |
|---|---|
/remove_staff <user> | !remove_staff <user_id> |
Parameters:
user- Staff member to remove
take
Assign yourself to the ticket (claim ownership).
| Slash | Text |
|---|---|
/take | !take |
release
Release your assignment from the ticket.
| Slash | Text |
|---|---|
/release | !release |
Reminders
add_reminder
Set a reminder for the current ticket.
| Slash | Text |
|---|---|
/add_reminder <time> [content] | !add_reminder <time> [content] |
Parameters:
time- When to trigger (e.g.,30m,2h,1d)content- Optional reminder message
Examples:
/add_reminder time:1h content:Follow up with user
!add_reminder 1h Follow up with user
remove_reminder
Cancel a scheduled reminder.
| Slash | Text |
|---|---|
/remove_reminder <reminder_id> | !remove_reminder <reminder_id> |
Parameters:
reminder_id- ID of the reminder to cancel
Alerts
alert
Get notified when the ticket receives a new message.
| Slash | Text |
|---|---|
/alert [cancel] | !alert [cancel] |
Parameters:
cancel- Remove an existing alert
Information
id
Display the ticket user’s Discord ID.
| Slash | Text |
|---|---|
/id | !id |
logs
View ticket history and logs.
| Slash | Text |
|---|---|
/logs | !logs |
status
View or change the bot’s operational status.
| Slash | Text |
|---|---|
/status [new_status] | !status [new_status] |
Snippets
Snippets are saved responses that can be quickly inserted.
snippet
Use a saved snippet.
| Slash | Text |
|---|---|
/snippet <key> | !snippet <key> |
Snippet management is done through the web panel or database directly.
Time Format Reference
For commands accepting time durations:
| Unit | Example | Description |
|---|---|---|
s | 30s | Seconds |
m | 15m | Minutes |
h | 2h | Hours |
d | 1d | Days |
Combinations are supported: 1d12h = 1 day and 12 hours.
Command Permissions
Commands are available to users with access to ticket channels. The bot respects Discord’s permission system:
- Only staff with channel access can use ticket commands
- Super admins (configured in
config.toml) have full access - Additional permissions can be configured through the panel
Server Modes
Rustmail supports two operating modes to fit different community structures.
Overview
| Mode | Servers | Use Case |
|---|---|---|
| Single | 1 | Small communities, simple setup |
| Dual | 2 | Large communities, staff privacy |
Single-Server Mode
Everything operates within one Discord server.
Configuration
[bot.mode]
type = "single"
guild_id = 123456789012345678
Structure
Your Server
├── #general (community channels)
├── #announcements
├── Tickets (category)
│ ├── #ticket-user1
│ └── #ticket-user2
└── #staff-chat
Advantages
- Simple setup
- No need for multiple server invitations
- Users can see ticket activity (if desired)
Considerations
- Staff channels visible in same server
- Ticket category permissions must be carefully managed
- Less separation between community and support operations
Permission Setup
- Create a category for tickets (e.g., “Tickets”)
- Set category permissions:
@everyone: Deny View Channel- Staff roles: Allow View Channel
- Use this category ID as
inbox_category_id
Dual-Server Mode
Separates your community server from your staff operations server.
Configuration
[bot.mode]
type = "dual"
community_guild_id = 123456789012345678
staff_guild_id = 987654321098765432
Structure
Community Server:
Community Server
├── #general
├── #announcements
└── #support-info
Staff Server:
Staff Server
├── Tickets (category)
│ ├── #ticket-user1
│ └── #ticket-user2
├── #staff-discussion
└── #logs
Advantages
- Complete separation of community and staff spaces
- Staff discussions remain private
- Cleaner community server
- Better for larger operations with many staff members
Considerations
- Requires managing two servers
- Bot must be invited to both servers
- Staff need access to the staff server
Setup Steps
- Create or designate your staff server
- Invite the bot to both servers
- In the staff server, create a category for tickets
- Configure:
[bot.mode] type = "dual" community_guild_id = <your_community_server_id> staff_guild_id = <your_staff_server_id> [thread] inbox_category_id = <category_id_in_staff_server>
Choosing a Mode
Choose Single-Server if:
- Your community is small (under 1,000 members)
- You have a small staff team
- You want simple setup and management
- Staff visibility in the community server is acceptable
Choose Dual-Server if:
- Your community is large
- You have a dedicated staff team
- You want complete separation of concerns
- Staff privacy is important
- You handle sensitive support topics
Migration Between Modes
Single to Dual
- Create a new staff server
- Invite the bot to the staff server
- Create a tickets category in the staff server
- Update
config.toml:[bot.mode] type = "dual" community_guild_id = <existing_server_id> staff_guild_id = <new_staff_server_id> [thread] inbox_category_id = <new_category_id> - Restart the bot
Existing tickets in the old location will remain but become inactive. New tickets will be created in the staff server.
Dual to Single
- Update
config.toml:[bot.mode] type = "single" guild_id = <your_server_id> [thread] inbox_category_id = <category_id_in_that_server> - Restart the bot
Getting Server and Category IDs
- Enable Developer Mode in Discord:
- User Settings > App Settings > Advanced > Developer Mode
- Right-click on a server icon > Copy Server ID
- Right-click on a category > Copy Category ID
Managing Tickets
This guide covers the ticket lifecycle and management features in Rustmail.
Ticket Lifecycle
1. Creation
A ticket is created when:
- A user sends a direct message to the bot
- Staff uses
/new_threadto initiate contact - (Optional) A user creates a channel in a designated category
When created:
- A channel is created in the inbox category
- The user receives the
welcome_message - Staff can see the new channel
2. Active Conversation
During the ticket:
- User messages appear in the ticket channel
- Staff respond with
/replyor!reply - Messages are tracked with unique IDs
- Edit and delete history is preserved
3. Closure
Tickets are closed when:
- Staff uses
/close - A scheduled closure triggers
- The user leaves the server (if
close_on_leaveis enabled)
On closure:
- The user receives the
close_message(unless silent) - The channel is archived or deleted
- Records are preserved in the database
Creating Tickets
User-Initiated
Users open tickets by sending a DM to the bot:
- User finds the bot in the server member list
- User sends a direct message
- Bot creates a ticket channel
- Bot sends
welcome_messageto the user
Staff-Initiated
Staff can create tickets for users:
/new_thread user:@Username
!new_thread 123456789012345678
Useful for:
- Proactive outreach
- Following up on issues
- Contacting users who can’t DM
Channel Creation (Optional)
If enabled in configuration:
[thread]
create_ticket_by_create_channel = true
Staff can create a channel in the inbox category to start a ticket. The channel name should be the user’s ID.
Responding to Tickets
Standard Reply
/reply content:Hello, how can I help you today?
!reply Hello, how can I help you today?
The message is sent to the user’s DMs and logged in the ticket channel.
Anonymous Reply
Hide your identity from the user:
/reply content:This is from the staff team. anonymous:true
!anonreply This is from the staff team.
Anonymous messages show a generic staff label instead of your username.
Attachments
Slash command:
/reply content:Here's the document you requested. attachment:<file>
Text command: Upload the file with your message:
!reply Here's the document you requested.
[Attached file]
Editing and Deleting Messages
Message IDs
Each message has a unique ID shown in the footer. Use this ID for edit and delete operations.
Editing
/edit message_id:42 new_content:Updated message text
!edit 42 Updated message text
Edits are:
- Reflected in the user’s DMs
- Logged (if
show_log_on_editis enabled) - Tracked in the database
Deleting
/delete message_id:42
!delete 42
Deletions:
- Remove the message from user’s DMs (if possible)
- Log the deletion (if
show_log_on_deleteis enabled) - Mark the record as deleted in database
Closing Tickets
Immediate Close
/close
!close
The user receives close_message and the ticket is archived.
Silent Close
/close silent:true
!close -s
No message is sent to the user.
Scheduled Close
Schedule automatic closure:
/close time:2h
!close 2h
Supported formats: 30m, 2h, 1d, 1d12h
Cancel Scheduled Close
/close cancel:true
!close -c
Force Close
For orphaned tickets (user left the server):
/force_close
!force_close
Staff Assignment
Claiming a Ticket
/take
!take
Marks you as the assigned staff member.
Releasing Assignment
/release
!release
Removes your assignment.
Adding/Removing Staff Access
Grant specific staff access:
/add_staff user:@StaffMember
!add_staff 123456789012345678
Remove access:
/remove_staff user:@StaffMember
!remove_staff 123456789012345678
Reminders and Alerts
Setting Reminders
Get pinged after a delay:
/add_reminder time:2h content:Check if user responded
!add_reminder 2h Check if user responded
Removing Reminders
/remove_reminder reminder_id:5
!remove_reminder 5
Alerts
Get notified on the next user response:
/alert
!alert
Cancel an alert:
/alert cancel:true
!alert cancel
Moving Tickets
Relocate a ticket to a different category:
/move_thread category:Escalated
!move_thread Escalated
Useful for:
- Escalation workflows
- Department routing
- Priority categorization
Ticket Information
User ID
/id
!id
Displays the ticket user’s Discord ID.
Logs
/logs
!logs
View the ticket’s history and activity log.
Message Recovery
If the bot goes offline, users may send messages that aren’t immediately processed. Rustmail automatically recovers these messages when restarting.
Manual recovery:
/recover
!recover
Configuration Options
Key settings affecting ticket behavior:
| Option | Description |
|---|---|
welcome_message | Sent when ticket opens |
close_message | Sent when ticket closes |
time_to_close_thread | Default scheduled close time |
close_on_leave | Auto-close when user leaves server |
embedded_message | Display messages as embeds |
block_quote | Use block quotes for messages |
See Configuration Reference for all options.
Web Panel
The Rustmail web panel provides browser-based administration for managing tickets, configuration, and permissions.
Enabling the Panel
The panel is optional and requires OAuth2 configuration. See Configuration for setup instructions.
[bot]
enable_panel = true
client_id = 123456789012345678
client_secret = "your_oauth2_client_secret"
redirect_url = "https://panel.example.com/api/auth/callback"
Accessing the Panel
Default URL
The panel runs on port 3002:
- Local:
http://localhost:3002 - Network:
http://<server-ip>:3002
With Reverse Proxy
When using a reverse proxy with a custom domain:
https://panel.example.com
See Configuration for proxy setup.
Authentication
Login Process
- Click Login on the panel homepage
- You are redirected to Discord’s OAuth2 authorization
- Authorize the application
- Discord redirects back to the panel
- A session is created
Sessions persist across browser restarts. Click Logout to end your session.
Access Requirements
Panel access requires one of:
- Being listed in
panel_super_admin_users - Having a role listed in
panel_super_admin_roles - Having been granted permissions through the panel
Panel Sections
Dashboard
The home view displays:
- Bot status (online/offline)
- Active ticket count
- Quick statistics
Tickets
View and manage active tickets:
- List of all open tickets
- User information
- Ticket creation time
- Quick actions
Configuration
Modify bot settings without editing config.toml:
- Change bot status/presence
- Update welcome and close messages
- Toggle features
Changes take effect immediately without restart.
API Keys
Manage API keys for external integrations:
- Create new API keys
- Set permissions per key
- Revoke or delete keys
- View last usage time
Administration
For super administrators:
- Manage panel permissions
- Grant access to users and roles
- View audit information
Permission System
Super Administrators
Defined in config.toml:
[bot]
panel_super_admin_users = [123456789012345678]
panel_super_admin_roles = [987654321098765432]
Super administrators have:
- Full panel access
- Ability to grant permissions to others
- Access to all tickets and settings
Granted Permissions
Super administrators can grant specific permissions to users or roles through the Administration section.
Available permissions:
- View tickets
- Manage tickets
- View configuration
- Edit configuration
- Manage API keys
API Keys
API keys allow external applications to create tickets in Rustmail without going through the panel or Discord.
What Are API Keys For?
API keys provide access to the External API (/api/externals/* endpoints). Common use cases:
- Website integration - Let users create support tickets from your website
- Cross-platform support - Connect Rustmail to other support tools
- Automation - Create tickets from scripts, forms, or other bots
API keys do not grant access to panel features (bot control, configuration, etc.). Those require logging in through the panel.
Creating a Key
- Navigate to API Keys in the panel
- Click Create New Key
- Enter a descriptive name (e.g., “Website Contact Form”)
- Optionally set an expiration date
- Copy the generated key immediately (it won’t be shown again)
Using API Keys
Include the key in the X-API-Key header when calling external endpoints:
X-API-Key: rustmail_your_api_key_here
Example: Create a ticket from an external source
curl --request POST \
--url 'https://panel.example.com/api/externals/tickets/create' \
--header 'Content-Type: application/json' \
--header 'X-API-Key: rustmail_350e97ec369e3b8afe133d1154d6eb8f...' \
--data '{"discord_id": "123456789012345678"}'
See API Reference for all available external endpoints.
Revoking Keys
- Revoke: Immediately invalidates the key but keeps it in records for audit purposes
- Delete: Permanently removes the key from the system
Revoke keys when they may have been compromised. Delete keys that are no longer needed.
Security Considerations
Session Security
- Sessions are stored server-side
- Session tokens are cryptographically random
- Sessions expire after the configured duration
Network Security
For production deployments:
- Use HTTPS - Run behind a reverse proxy with TLS
- Restrict access - Use firewall rules to limit who can reach the panel
- Strong secrets - Use a secure OAuth2 client secret
Access Control
- Regularly audit panel permissions
- Remove access for departed staff members
- Use role-based permissions when possible
Troubleshooting
Cannot Login
OAuth2 redirect mismatch:
- Verify
redirect_urlexactly matches Discord Developer Portal - Check for protocol (
httpvshttps) and trailing slash differences
Client ID/Secret incorrect:
- Regenerate the client secret in Discord Developer Portal
- Update
config.tomland restart
Session Expires Immediately
- Check system clock synchronization
- Verify the database is writable
- Check for cookie blocking in browser
Panel Not Loading
- Ensure
enable_panel = true - Check that port 3002 is not blocked
- Verify the bot process is running
- Check reverse proxy configuration if applicable
Docker Deployment
This guide covers running Rustmail in Docker containers.
Quick Start
Pull the Image
docker pull ghcr.io/rustmail/rustmail:latest
Run with Docker
docker run -d \
--name rustmail \
-p 3002:3002 \
-v /path/to/config.toml:/app/config.toml:ro \
-v rustmail-data:/app/db \
ghcr.io/rustmail/rustmail:latest
Docker Compose
Create a docker-compose.yml:
version: '3.8'
services:
rustmail:
image: ghcr.io/rustmail/rustmail:latest
container_name: rustmail
restart: unless-stopped
ports:
- "3002:3002"
volumes:
- ./config.toml:/app/config.toml:ro
- rustmail-data:/app/db
environment:
- TZ=Europe/Paris
volumes:
rustmail-data:
Start with:
docker-compose up -d
Configuration
Volume Mounts
| Path | Description |
|---|---|
/app/config.toml | Configuration file (required) |
/app/db | Database directory (persistent storage) |
Ports
| Port | Description |
|---|---|
3002 | Web panel and API |
Environment Variables
| Variable | Description |
|---|---|
TZ | Container timezone |
Building the Image
To build from source:
# Clone the repository
git clone https://github.com/Rustmail/rustmail.git
cd rustmail
# Build the image
docker build -t rustmail:local .
The Dockerfile uses a multi-stage build with Debian Bookworm Slim as the runtime base.
Docker Compose with Reverse Proxy
With Traefik
version: '3.8'
services:
rustmail:
image: ghcr.io/rustmail/rustmail:latest
container_name: rustmail
restart: unless-stopped
volumes:
- ./config.toml:/app/config.toml:ro
- rustmail-data:/app/db
labels:
- "traefik.enable=true"
- "traefik.http.routers.rustmail.rule=Host(`panel.example.com`)"
- "traefik.http.routers.rustmail.tls=true"
- "traefik.http.routers.rustmail.tls.certresolver=letsencrypt"
- "traefik.http.services.rustmail.loadbalancer.server.port=3002"
networks:
- traefik
networks:
traefik:
external: true
volumes:
rustmail-data:
With Nginx Proxy Manager
version: '3.8'
services:
rustmail:
image: ghcr.io/rustmail/rustmail:latest
container_name: rustmail
restart: unless-stopped
volumes:
- ./config.toml:/app/config.toml:ro
- rustmail-data:/app/db
networks:
- npm_network
networks:
npm_network:
external: true
volumes:
rustmail-data:
Then in NPM, create a proxy host pointing to rustmail:3002.
With Caddy
version: '3.8'
services:
rustmail:
image: ghcr.io/rustmail/rustmail:latest
container_name: rustmail
restart: unless-stopped
volumes:
- ./config.toml:/app/config.toml:ro
- rustmail-data:/app/db
networks:
- caddy
caddy:
image: caddy:alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
networks:
- caddy
networks:
caddy:
volumes:
rustmail-data:
caddy-data:
Caddyfile:
panel.example.com {
reverse_proxy rustmail:3002
}
Health Checks
Add health monitoring:
services:
rustmail:
image: ghcr.io/rustmail/rustmail:latest
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3002/api/panel/check"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
Logging
View logs:
# Follow logs
docker logs -f rustmail
# Last 100 lines
docker logs --tail 100 rustmail
Configure log rotation in Docker daemon or use a logging driver:
services:
rustmail:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
Backup
Database Backup
# Stop container for consistent backup
docker stop rustmail
# Copy database
docker cp rustmail:/app/db/db.sqlite ./backup-$(date +%Y%m%d).sqlite
# Restart
docker start rustmail
Volume Backup
docker run --rm \
-v rustmail-data:/data:ro \
-v $(pwd):/backup \
alpine tar czf /backup/rustmail-backup.tar.gz /data
Updates
Pull New Image
docker-compose pull
docker-compose up -d
Manual Update
docker pull ghcr.io/rustmail/rustmail:latest
docker stop rustmail
docker rm rustmail
# Run new container with same volumes
Troubleshooting
Container Won’t Start
Check logs:
docker logs rustmail
Common issues:
- Invalid
config.tomlsyntax - Missing required configuration fields
- Permission issues on mounted volumes
Container Stops After “Database connected!”
If logs only show:
Database connection pool established
Database connected!
The bot cannot load config.toml. The most common cause is an incorrect volume mount:
# WRONG - relative path creates a directory instead of mounting the file
docker run -v config.toml:/app/config.toml ...
# CORRECT - use absolute path
docker run -v $(pwd)/config.toml:/app/config.toml ...
docker run -v /home/user/rustmail/config.toml:/app/config.toml ...
Verify the mount is correct:
# Should show a file, not a directory
docker exec rustmail ls -la /app/config.toml
# Should display your config content
docker exec rustmail cat /app/config.toml
If /app/config.toml is a directory, remove the container and recreate with the correct path:
docker rm -f rustmail
docker run -d --name rustmail -v $(pwd)/config.toml:/app/config.toml:ro ...
Cannot Connect to Panel
- Verify port 3002 is exposed:
docker port rustmail - Check container is running:
docker ps - Verify network connectivity to container
Database Errors
- Ensure
/app/dbvolume is writable - Check disk space on host
- Verify volume mount is correct
Permission Denied
The container runs as user rustmail (UID 1000). Ensure mounted volumes are accessible:
# Fix permissions on host
chown -R 1000:1000 /path/to/data
Production Deployment
Best practices and recommendations for running Rustmail in production.
Checklist
Before deploying to production:
- Tested configuration locally
- Verified bot permissions in Discord
- Set up HTTPS for the panel
- Configured backups
- Set up monitoring
- Documented your configuration
System Requirements
Minimum
- 1 CPU core
- 512 MB RAM
- 1 GB disk space
Recommended
- 2 CPU cores
- 1 GB RAM
- 5 GB disk space (for database growth)
Rustmail is lightweight. Resource usage grows with ticket volume and message history.
Security
HTTPS
Always use HTTPS for the panel in production. Options:
-
Reverse proxy with TLS termination (recommended)
- Nginx, Caddy, Traefik
- Automatic certificate renewal with Let’s Encrypt
-
Cloud load balancer
- AWS ALB, GCP Load Balancer, Cloudflare
See Configuration for proxy setup.
Firewall
Restrict access to necessary ports only:
# Allow SSH
ufw allow 22/tcp
# Allow HTTPS (reverse proxy)
ufw allow 443/tcp
# Block direct access to bot port (if using reverse proxy)
# ufw deny 3002/tcp
ufw enable
Secrets Management
Protect sensitive configuration:
- Bot token
- OAuth2 client secret
- Database file
Options:
- File permissions:
chmod 600 config.toml - Docker secrets
- Environment-based secret injection at deployment
Updates
Keep Rustmail updated for security fixes:
# Check current version
./rustmail --version
# Update (Docker)
docker pull ghcr.io/rustmail/rustmail:latest
High Availability
Rustmail is designed as a single-instance application. For high availability:
Database
- Regular backups
- Store backups off-server
- Test restore procedures
Process Management
Use a process manager to ensure the bot restarts on failure:
Systemd (Linux):
[Unit]
Description=Rustmail Discord Bot
After=network.target
[Service]
Type=simple
User=rustmail
WorkingDirectory=/opt/rustmail
ExecStart=/opt/rustmail/rustmail
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
Docker:
services:
rustmail:
restart: unless-stopped
Monitoring
Monitor bot status:
# Simple health check
curl -f http://localhost:3002/api/bot/status
Integration with monitoring systems:
- Prometheus metrics endpoint (future feature)
- External uptime monitoring
- Discord webhook alerts
Backup Strategy
What to Backup
| Item | Location | Frequency |
|---|---|---|
| Database | db.sqlite | Daily |
| Configuration | config.toml | On change |
Automated Backups
Example backup script:
#!/bin/bash
BACKUP_DIR="/backups/rustmail"
DATE=$(date +%Y%m%d_%H%M%S)
# Create backup directory
mkdir -p "$BACKUP_DIR"
# Backup database
cp /opt/rustmail/db.sqlite "$BACKUP_DIR/db_$DATE.sqlite"
# Backup config
cp /opt/rustmail/config.toml "$BACKUP_DIR/config_$DATE.toml"
# Keep last 30 days
find "$BACKUP_DIR" -type f -mtime +30 -delete
Add to crontab:
0 3 * * * /opt/rustmail/backup.sh
Off-site Backup
Sync backups to remote storage:
# rsync to remote server
rsync -avz /backups/rustmail/ backup-server:/backups/rustmail/
# AWS S3
aws s3 sync /backups/rustmail/ s3://your-bucket/rustmail-backups/
Logging
Log Levels
Rustmail outputs logs to stdout. In production:
# Redirect to file
./rustmail >> /var/log/rustmail/rustmail.log 2>&1
Log Rotation
With logrotate (/etc/logrotate.d/rustmail):
/var/log/rustmail/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 0640 rustmail rustmail
}
Centralized Logging
For Docker deployments, use logging drivers:
services:
rustmail:
logging:
driver: "syslog"
options:
syslog-address: "udp://logserver:514"
tag: "rustmail"
Performance Tuning
SQLite Optimization
The database uses SQLite with default settings. For high-volume deployments:
-- Run these optimizations
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA cache_size = -64000; -- 64MB cache
Connection Pooling
Rustmail uses SQLx with connection pooling. Default settings are suitable for most deployments.
Maintenance
Planned Downtime
For updates requiring restart:
- Close active tickets or notify staff
- Stop the bot
- Backup database
- Apply update
- Verify configuration
- Start the bot
- Verify functionality
Database Maintenance
Periodically optimize the database:
# Stop bot first
sqlite3 db.sqlite "VACUUM;"
sqlite3 db.sqlite "ANALYZE;"
Cleanup Old Data
If needed for storage or compliance:
-- Delete closed tickets older than 2 years
DELETE FROM thread_messages
WHERE thread_id IN (
SELECT id FROM threads
WHERE status = 0
AND closed_at < datetime('now', '-2 years')
);
DELETE FROM threads
WHERE status = 0
AND closed_at < datetime('now', '-2 years');
VACUUM;
Troubleshooting
Bot Goes Offline
- Check process status:
systemctl status rustmail - Check logs for errors
- Verify Discord API status
- Check network connectivity
- Verify token validity
Panel Inaccessible
- Check bot process is running
- Verify port 3002 is listening:
netstat -tlnp | grep 3002 - Check reverse proxy configuration
- Verify SSL certificate validity
- Check firewall rules
Performance Issues
- Monitor CPU/memory usage
- Check database size
- Review ticket volume
- Consider database maintenance
Data Recovery
From backup:
# Stop bot
systemctl stop rustmail
# Restore database
cp /backups/rustmail/db_20240115.sqlite /opt/rustmail/db.sqlite
# Verify
sqlite3 /opt/rustmail/db.sqlite "SELECT COUNT(*) FROM threads;"
# Start bot
systemctl start rustmail
Configuration Reference
Complete reference for all config.toml options.
Bot Section
[bot]
Core Settings
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
token | string | Yes | - | Discord bot token from Developer Portal |
status | string | Yes | - | Bot’s activity status message |
welcome_message | string | Yes | - | Message sent to users when opening a ticket |
close_message | string | Yes | - | Message sent to users when ticket is closed |
Typing Indicators
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
typing_proxy_from_user | bool | Yes | - | Show typing indicator in ticket when user types |
typing_proxy_from_staff | bool | Yes | - | Show typing indicator in DM when staff types |
Feature Toggles
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
enable_logs | bool | Yes | - | Enable logging to a channel |
enable_features | bool | Yes | - | Enable feature request tracking |
enable_panel | bool | Yes | - | Enable web administration panel |
Channel Configuration
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
logs_channel_id | u64 | Conditional | - | Channel for bot logs. Required if enable_logs = true |
features_channel_id | u64 | Conditional | - | Channel for feature requests. Required if enable_features = true |
OAuth2 (Panel)
Required when enable_panel = true.
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
client_id | u64 | Conditional | - | Discord application client ID |
client_secret | string | Conditional | - | Discord application client secret |
redirect_url | string | Conditional | - | OAuth2 callback URL (must match Discord config) |
Network
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
ip | string | No | Auto-detected | Network interface to bind the server |
Note: The server attempts to bind to the configured IP address. If the IP is invalid or unavailable, it falls back to 0.0.0.0:3002 (all interfaces). Set this manually when:
- Running in Docker with host networking
- Auto-detection returns wrong interface
- You need to bind to a specific network interface
Panel Administrators
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
panel_super_admin_users | [u64] | No | [] | User IDs with full panel access |
panel_super_admin_roles | [u64] | No | [] | Role IDs with full panel access |
Timezone
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
timezone | string | No | UTC | Timezone for timestamps (IANA format) |
Examples: Europe/Paris, America/New_York, Asia/Tokyo
Server Mode Section
[bot.mode]
Single Server Mode
[bot.mode]
type = "single"
guild_id = 123456789012345678
| Option | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Must be "single" |
guild_id | u64 | Yes | Your Discord server ID |
Dual Server Mode
[bot.mode]
type = "dual"
community_guild_id = 123456789012345678
staff_guild_id = 987654321098765432
| Option | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Must be "dual" |
community_guild_id | u64 | Yes | Server where users are |
staff_guild_id | u64 | Yes | Server where tickets are created |
Command Section
[command]
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
prefix | string | Yes | "!" | Prefix for text commands |
Thread Section
[thread]
Required Settings
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
inbox_category_id | u64 | Yes | - | Category where ticket channels are created |
Message Display
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
embedded_message | bool | Yes | - | Display messages as Discord embeds |
user_message_color | string | Yes | "5865f2" | Hex color for user messages (without #) |
staff_message_color | string | Yes | "57f287" | Hex color for staff messages (without #) |
system_message_color | string | Yes | "faa81a" | Hex color for system messages (without #) |
block_quote | bool | Yes | - | Use block quotes for message content |
Ticket Behavior
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
time_to_close_thread | u64 | Yes | 0 | Default minutes until auto-close (0 = disabled) |
create_ticket_by_create_channel | bool | Yes | - | Allow ticket creation by making a channel |
close_on_leave | bool | No | false | Auto-close when user leaves server |
auto_archive_duration | u16 | No | 10080 | Thread auto-archive time in minutes |
Language Section
[language]
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
default_language | string | Yes | "en" | Default language code |
fallback_language | string | Yes | "en" | Fallback when translation missing |
supported_languages | [string] | Yes | ["en", "fr"] | Available languages |
Available Language Codes
| Code | Language |
|---|---|
en | English |
fr | French |
es | Spanish |
de | German |
it | Italian |
pt | Portuguese |
ru | Russian |
zh | Chinese |
ja | Japanese |
ko | Korean |
Notifications Section
[notifications]
Control feedback messages shown to staff.
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
show_success_on_edit | bool | Yes | true | Confirm successful edits |
show_partial_success_on_edit | bool | Yes | true | Notify on partial edit success |
show_failure_on_edit | bool | Yes | true | Notify on edit failure |
show_success_on_reply | bool | Yes | true | Confirm sent replies |
show_success_on_delete | bool | Yes | true | Confirm deletions |
show_success | bool | No | true | General success notifications |
show_error | bool | No | true | General error notifications |
Logs Section
[logs]
Control what actions are logged.
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
show_log_on_edit | bool | Yes | true | Log message edits |
show_log_on_delete | bool | Yes | true | Log message deletions |
Reminders Section
[reminders]
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
embed_color | string | Yes | "ffcc00" | Hex color for reminder embeds (without #) |
Error Handling Section
[error_handling]
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
show_detailed_errors | bool | Yes | true | Show technical details in errors |
log_errors | bool | Yes | true | Log errors to console |
send_error_embeds | bool | Yes | true | Send errors as embeds |
auto_delete_error_messages | bool | Yes | false | Auto-delete error messages |
error_message_ttl | u64 | No | - | Seconds before error deletion |
display_errors | bool | No | true | Show errors to users |
Complete Example
[bot]
token = "YOUR_BOT_TOKEN"
status = "DM for support"
welcome_message = "Your message has been received. Staff will respond shortly."
close_message = "This ticket has been closed. Thank you for contacting us."
typing_proxy_from_user = true
typing_proxy_from_staff = true
enable_logs = true
enable_features = false
enable_panel = true
client_id = 123456789012345678
client_secret = "your_client_secret_here"
redirect_url = "https://panel.example.com/api/auth/callback"
timezone = "Europe/Paris"
logs_channel_id = 123456789012345678
panel_super_admin_users = [123456789012345678]
panel_super_admin_roles = []
[bot.mode]
type = "dual"
community_guild_id = 123456789012345678
staff_guild_id = 987654321098765432
[command]
prefix = "!"
[thread]
inbox_category_id = 123456789012345678
embedded_message = true
user_message_color = "5865f2"
staff_message_color = "57f287"
system_message_color = "faa81a"
block_quote = true
time_to_close_thread = 0
create_ticket_by_create_channel = false
close_on_leave = false
auto_archive_duration = 10080
[language]
default_language = "en"
fallback_language = "en"
supported_languages = ["en", "fr", "es", "de"]
[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
[logs]
show_log_on_edit = true
show_log_on_delete = true
[reminders]
embed_color = "ffb800"
[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
Environment Variables
Rustmail does not currently support environment variable substitution in config.toml. For sensitive values in containerized environments, consider mounting the config file as a secret.
REST API Reference
Rustmail exposes a REST API on port 3002 for external integrations and the web panel.
Base URL
http://localhost:3002/api
Or with a reverse proxy:
https://panel.example.com/api
Authentication
Session-Based (Panel)
The web panel and most API endpoints (/api/bot/*, /api/admin/*, /api/user/*, etc.) use Discord OAuth2 with session
cookies. These endpoints are designed for the panel interface, not external integrations.
API Key (External Integrations)
API keys are used exclusively for the External API (/api/externals/* endpoints). They allow third-party
applications to interact with Rustmail without going through the panel.
Use cases for API keys:
- Create tickets from an external website or application
- Integrate Rustmail with other support systems
- Automate ticket creation from forms, bots, or scripts
Important: API keys only grant access to /api/externals/* endpoints. They cannot be used to access panel endpoints
like /api/bot/status or /api/admin/*.
Include the key in the X-API-Key header:
X-API-Key: rustmail_your_api_key_here
Required headers:
| Header | Value | Description |
|---|---|---|
X-API-Key | rustmail_... | Your API key |
Content-Type | application/json | Required for POST requests |
Example:
curl --request POST \
--url 'https://panel.example.com/api/externals/tickets/create' \
--header 'Content-Type: application/json' \
--header 'X-API-Key: rustmail_350e97ec369e3b8afe133d1154d6eb8f2e779bd9' \
--data '{"discord_id": "123456789012345678"}'
Endpoints
Authentication
GET /api/auth/login
Initiates Discord OAuth2 flow. Redirects to Discord authorization.
Response: 302 Redirect to Discord
GET /api/auth/callback
OAuth2 callback handler. Discord redirects here after authorization.
Query Parameters:
code- Authorization code from Discordstate- Redirect URL after authentication
Response: 302 Redirect to panel home
GET /api/auth/logout
Ends the current session.
Response: 302 Redirect to panel home
Bot Control
GET /api/bot/status
Get current bot status.
Response:
{
"status": "running",
"presence": "online"
}
| Field | Type | Description |
|---|---|---|
status | string | Bot state: "running" or "stopped" |
presence | string | Current presence: online, idle, dnd, etc. |
POST /api/bot/start
Start the bot (if stopped).
Response (success):
"Bot is starting"
Response (already running):
"Bot is already running"
POST /api/bot/stop
Stop the bot.
Response (success):
"Bot stopped"
Response (not running):
"Bot is not running"
POST /api/bot/restart
Restart the bot. Stops the bot and starts it again.
POST /api/bot/presence
Update bot presence status.
Request Body:
{
"status": "online"
}
| Value | Description |
|---|---|
online | Online status, shows configured activity |
idle | Idle/Away status |
dnd | Do Not Disturb status |
invisible | Invisible/Offline status |
maintenance | Maintenance mode (DND + maintenance activity) |
Response:
{
"status": "online"
}
Configuration
GET /api/bot/config
Retrieve current configuration. Sensitive fields (token, client_secret) are partially masked.
Response:
{
"bot": {
"token": "MTIz...4567",
"status": "DM for support",
"welcome_message": "...",
"close_message": "...",
"enable_panel": true,
"client_id": 123456789012345678,
"client_secret": "abc1...xyz9",
"redirect_url": "https://panel.example.com/api/auth/callback",
"timezone": "Europe/Paris"
},
"command": {
...
},
"thread": {
...
},
"language": {
...
},
"error_handling": {
...
},
"notifications": {
...
},
"reminders": {
...
},
"logs": {
...
}
}
PUT /api/bot/config
Update configuration. Send the full configuration object. Masked fields (containing ...) will preserve their original
values.
Request Body: Full ConfigResponse object
Response:
{
"success": true,
"message": "Configuration saved successfully. Restart the bot to apply changes."
}
Tickets
GET /api/bot/tickets
List tickets with pagination and filtering.
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
id | string | - | Get a specific ticket by ID |
page | int | 1 | Page number |
page_size | int | 50 | Items per page (max 200) |
status | int | 1 | Filter: 1 = open, 0 = closed |
category_id | string | - | Filter by category ID |
sort_by | string | created_at | Sort field: created_at, closed_at, user_name |
sort_order | string | DESC | Sort order: asc or desc |
Response (list):
{
"threads": [
{
"id": "abc123",
"user_id": 123456789012345678,
"user_name": "Username",
"channel_id": "987654321098765432",
"created_at": 1705312200,
"new_message_number": 5,
"status": 0,
"user_left": false,
"closed_at": null,
"closed_by": null,
"category_id": "111222333444555666",
"category_name": "Support",
"required_permissions": null,
"messages": [
...
]
}
],
"total": 150,
"page": 1,
"page_size": 50,
"total_pages": 3
}
Response (single ticket with ?id=abc123):
{
"id": "abc123",
"user_id": 123456789012345678,
"user_name": "Username",
"channel_id": "987654321098765432",
"created_at": 1705312200,
"new_message_number": 5,
"status": 0,
"user_left": false,
"closed_at": null,
"closed_by": null,
"category_id": null,
"category_name": null,
"required_permissions": null,
"messages": [
{
"id": 1,
"thread_id": "abc123",
"user_id": 123456789012345678,
"user_name": "Username",
"is_anonymous": false,
"dm_message_id": "111222333",
"inbox_message_id": "444555666",
"message_number": 1,
"created_at": "2024-01-15 10:30:00",
"content": "Hello, I need help",
"is_internal": false
}
]
}
External Ticket Creation
POST /api/externals/tickets/create
Create a ticket from an external source. Useful for integrating Rustmail with external support systems, websites, or automation tools.
Headers:
Content-Type: application/json
X-API-Key: rustmail_your_api_key_here
Request Body:
{
"discord_id": "123456789012345678",
"staff_discord_id": "987654321098765432"
}
| Field | Type | Required | Description |
|---|---|---|---|
discord_id | string | Yes | Discord user ID to create a ticket for |
staff_discord_id | string | No | Staff member to ping in the ticket (optional) |
Full Example:
curl --request POST \
--url 'https://panel.example.com/api/externals/tickets/create' \
--header 'Content-Type: application/json' \
--header 'X-API-Key: rustmail_350e97ec369e3b8afe133d1154d6eb8f2e779bd9' \
--data '{
"discord_id": "689149284871962727",
"staff_discord_id": "123456789012345678"
}'
Response:
{
"success": true,
"channel_id": "987654321098765432",
"user_id": "689149284871962727",
"username": "Username",
"message": "Ticket created successfully"
}
Error Responses:
| Status | Error |
|---|---|
| 400 | Invalid Discord ID format |
| 403 | User is not a member of the community guild |
| 404 | Discord user not found |
| 409 | User already has an active ticket |
API Keys
GET /api/apikeys
List all API keys.
Response:
[
{
"id": 1,
"name": "Integration Key",
"permissions": [
"CreateTicket"
],
"created_at": 1705312200,
"expires_at": null,
"last_used_at": 1705398600,
"is_active": true,
"key_preview": "a1b2c3d4e5f6..."
}
]
POST /api/apikeys
Create a new API key.
Request Body:
{
"name": "My Integration",
"permissions": [
"CreateTicket"
],
"expires_at": null
}
| Permission | Description |
|---|---|
CreateTicket | Can create tickets via API |
Response:
{
"api_key": "rustmail_350e97ec369e3b8afe133d1154d6eb8f2e779bd9214a6800509d72c91a13f3e5",
"id": 2,
"name": "My Integration",
"permissions": [
"CreateTicket"
],
"created_at": 1705312200,
"expires_at": null
}
The api_key field is only returned once at creation. Store it securely as it cannot be retrieved later.
POST /api/apikeys/{id}/revoke
Revoke an API key (deactivates it but keeps the record).
Response: 204 No Content
DELETE /api/apikeys/
Permanently delete an API key.
Response: 204 No Content
Administration
GET /api/admin/members
List server members (for permission management).
Response:
[
{
"user_id": "123456789012345678",
"username": "StaffMember",
"discriminator": "0",
"avatar": "abc123def456",
"roles": [
"111222333444555666",
"777888999000111222"
]
}
]
GET /api/admin/roles
List server roles.
Response:
[
{
"role_id": "123456789012345678",
"name": "Moderator",
"color": 3447003,
"position": 5
}
]
Roles are sorted by position (highest first).
GET /api/admin/permissions
List granted panel permissions.
Response:
[
{
"id": 1,
"subject_type": "User",
"subject_id": "123456789012345678",
"permission": "ViewPanel",
"granted_by": "987654321098765432",
"granted_at": 1705312200
}
]
Permission values:
ViewPanel- Can access the panelManageBot- Can start/stop/restart the botManageConfig- Can edit configurationManageTickets- Can manage ticketsManageApiKeys- Can create/revoke API keysManagePermissions- Can grant/revoke permissions
Subject types:
User- Permission granted to a specific userRole- Permission granted to all members with a role
POST /api/admin/permissions
Grant a permission.
Request Body:
{
"subject_type": "User",
"subject_id": "123456789012345678",
"permission": "ViewPanel"
}
Response:
{
"success": true
}
DELETE /api/admin/permissions/
Revoke a permission.
Response:
{
"success": true
}
User
GET /api/user/avatar
Get current user’s avatar URL.
Response:
{
"avatar_url": "https://cdn.discordapp.com/avatars/123456789012345678/abc123.png"
}
GET /api/user/permissions
Get current user’s panel permissions.
Response:
[
"ViewPanel",
"ManageTickets"
]
Returns an array of permission strings. Super admins and server admins receive all permissions.
Panel
GET /api/panel/check
Verify panel access. This endpoint requires authentication.
Response:
{
"authorized": true
}
Error Responses
All endpoints return errors in a consistent format:
{
"error": "Error message"
}
HTTP Status Codes
| Code | Meaning |
|---|---|
| 200 | Success |
| 204 | Success (no content) |
| 400 | Bad request (invalid parameters) |
| 401 | Unauthorized (missing/invalid authentication) |
| 403 | Forbidden (insufficient permissions) |
| 404 | Not found |
| 409 | Conflict (e.g., ticket already exists) |
| 500 | Internal server error |
Rate Limiting
The API does not currently implement rate limiting. For high-volume integrations, implement client-side throttling to avoid overwhelming the bot.
Webhooks
Rustmail does not currently support outgoing webhooks. Use polling with the tickets endpoint for integration needs.
Database Reference
Rustmail uses SQLite for persistent storage. The database file db.sqlite is created automatically on first run.
Overview
The database stores:
- Ticket threads and messages
- Staff alerts and reminders
- Scheduled closures
- Panel sessions and permissions
- API keys
- Snippets
Tables
threads
Stores ticket information.
| Column | Type | Description |
|---|---|---|
id | TEXT | Primary key (UUID) |
user_id | INTEGER | Discord user ID |
user_name | TEXT | Username at ticket creation |
channel_id | TEXT | Discord channel ID |
created_at | DATETIME | Ticket creation timestamp |
next_message_number | INTEGER | Counter for message numbering |
status | INTEGER | Ticket status (1=open, 0=closed) |
user_left | BOOLEAN | Whether user left the server |
closed_at | DATETIME | Closure timestamp (nullable) |
closed_by | TEXT | Staff who closed (nullable) |
category_id | TEXT | Current category ID (nullable) |
category_name | TEXT | Current category name (nullable) |
required_permissions | TEXT | Permission requirements (nullable) |
thread_messages
Stores all messages in tickets.
| Column | Type | Description |
|---|---|---|
id | INTEGER | Primary key (auto-increment) |
thread_id | TEXT | Foreign key to threads |
user_id | INTEGER | Author’s Discord ID |
user_name | TEXT | Author’s username |
is_anonymous | BOOLEAN | Whether sent anonymously |
dm_message_id | TEXT | Discord message ID in DM |
inbox_message_id | TEXT | Discord message ID in ticket channel |
message_number | INTEGER | Sequential message number |
created_at | DATETIME | Message timestamp |
content | TEXT | Message content |
thread_status | INTEGER | Thread status when sent |
blocked_users
Stores blocked users who cannot create tickets.
| Column | Type | Description |
|---|---|---|
user_id | TEXT | Primary key (Discord user ID) |
user_name | TEXT | Username when blocked |
blocked_by | TEXT | Staff who blocked |
blocked_at | DATETIME | Block timestamp |
expires_at | DATETIME | Block expiration |
staff_alerts
Stores alert subscriptions for tickets.
| Column | Type | Description |
|---|---|---|
id | INTEGER | Primary key |
staff_user_id | INTEGER | Staff Discord ID |
thread_user_id | INTEGER | Ticket user Discord ID |
created_at | DATETIME | Alert creation time |
used | BOOLEAN | Whether alert was triggered |
reminders
Stores scheduled reminders.
| Column | Type | Description |
|---|---|---|
id | INTEGER | Primary key |
thread_id | TEXT | Foreign key to threads |
user_id | BIGINT | Staff Discord ID |
channel_id | BIGINT | Channel Discord ID |
guild_id | BIGINT | Server Discord ID |
reminder_content | TEXT | Reminder message |
trigger_time | INTEGER | Unix timestamp to trigger |
created_at | INTEGER | Creation Unix timestamp |
completed | BOOLEAN | Whether reminder fired |
scheduled_closures
Stores scheduled ticket closures.
| Column | Type | Description |
|---|---|---|
id | INTEGER | Primary key |
thread_id | TEXT | Foreign key to threads |
scheduled_time | INTEGER | Unix timestamp for closure |
silent | BOOLEAN | Close without notification |
created_by | TEXT | Staff who scheduled |
snippets
Stores saved response templates.
| Column | Type | Description |
|---|---|---|
id | INTEGER | Primary key |
key | TEXT | Unique snippet identifier |
content | TEXT | Snippet text |
created_by | TEXT | Creator Discord ID |
created_at | DATETIME | Creation timestamp |
updated_at | DATETIME | Last update timestamp |
sessions_panel
Stores web panel sessions.
| Column | Type | Description |
|---|---|---|
session_id | TEXT | Primary key (session token) |
user_id | TEXT | Discord user ID |
access_token | TEXT | Discord OAuth2 access token |
refresh_token | TEXT | Discord OAuth2 refresh token |
expires_at | INTEGER | Session expiration Unix timestamp |
avatar_hash | TEXT | User’s avatar hash |
api_keys
Stores API keys for external access.
| Column | Type | Description |
|---|---|---|
id | INTEGER | Primary key |
key_hash | TEXT | Hashed API key (unique) |
name | TEXT | Key description |
permissions | TEXT | JSON array of permissions |
created_at | INTEGER | Creation Unix timestamp |
expires_at | INTEGER | Expiration timestamp (nullable) |
last_used_at | INTEGER | Last usage timestamp (nullable) |
is_active | INTEGER | Whether key is active |
panel_permissions
Stores granted panel permissions.
| Column | Type | Description |
|---|---|---|
id | INTEGER | Primary key |
subject_type | TEXT | “user” or “role” |
subject_id | TEXT | Discord user/role ID |
permission | TEXT | Permission name |
granted_by | TEXT | Who granted it |
granted_at | INTEGER | Grant Unix timestamp |
features_messages
Stores feature request tracking.
| Column | Type | Description |
|---|---|---|
id | INTEGER | Primary key |
message_id | TEXT | Discord message ID |
content | TEXT | Feature description |
thread_status
Stores thread status history.
| Column | Type | Description |
|---|---|---|
id | INTEGER | Primary key |
thread_id | TEXT | Foreign key to threads |
status | INTEGER | Status value |
changed_at | DATETIME | Change timestamp |
Indexes
Performance indexes on frequently queried columns:
threads_id_keyonthreads(id)thread_messages_id_keyonthread_messages(id)idx_api_keys_hashonapi_keys(key_hash)idx_api_keys_activeonapi_keys(is_active)idx_snippets_keyonsnippets(key)idx_panel_perms_subjectonpanel_permissions(subject_type, subject_id)idx_panel_perms_permissiononpanel_permissions(permission)
Migrations
Database schema is managed through SQLx migrations in the migrations/ directory. Migrations run automatically on bot startup.
Migration files are named with timestamps:
migrations/
├── 20250815145017_create_tables.sql
├── 20250815161000_unique_open_and_metadata.sql
├── 20250816120000_message_number_unique_and_cleanup.sql
└── ...
Backup
The database is a single file (db.sqlite). To backup:
# Stop the bot first for consistency
cp db.sqlite db.sqlite.backup
For production, consider scheduled backups:
# Example cron job (daily at 3 AM)
0 3 * * * cp /opt/rustmail/db.sqlite /backups/rustmail-$(date +\%Y\%m\%d).sqlite
Direct Access
You can query the database directly with SQLite tools:
sqlite3 db.sqlite
# Example queries
sqlite3 db.sqlite "SELECT COUNT(*) FROM threads WHERE status = 1;"
sqlite3 db.sqlite "SELECT * FROM threads ORDER BY created_at DESC LIMIT 10;"
Warning: Avoid modifying data while the bot is running to prevent corruption.
Data Retention
Rustmail does not automatically delete old data. For compliance or storage management, you may need to implement your own retention policies:
-- Example: Delete closed tickets older than 1 year
DELETE FROM thread_messages
WHERE thread_id IN (
SELECT id FROM threads
WHERE status = 0
AND closed_at < datetime('now', '-1 year')
);
DELETE FROM threads
WHERE status = 0
AND closed_at < datetime('now', '-1 year');
Architecture Overview
This document describes the technical architecture of Rustmail.
Project Structure
Rustmail is a Rust workspace with three crates:
rustmail/
├── rustmail/ # Main bot application
├── rustmail_panel/ # Web panel (Yew/WASM)
├── rustmail_types/ # Shared type definitions
├── migrations/ # SQLite migrations
├── docs/ # Documentation
├── Cargo.toml # Workspace manifest
└── Dockerfile
Crates
rustmail (Main Bot)
The core Discord bot application.
Key dependencies:
serenity- Discord API clientsqlx- Async SQLite databaseaxum- HTTP server for panel/APItokio- Async runtime
Structure:
rustmail/src/
├── main.rs # Entry point
├── config.rs # Configuration loading
├── api/ # REST API
│ ├── handler/ # Request handlers
│ └── routes/ # Route definitions
├── commands/ # Discord commands
├── database/ # Database operations
├── handlers/ # Discord event handlers
├── i18n/ # Internationalization
├── modules/ # Background tasks
├── prelude/ # Common imports
└── utils/ # Utility functions
rustmail_panel (Web UI)
Single-page application built with Yew framework, compiled to WebAssembly.
Key dependencies:
yew- Rust/WASM frameworkyew-router- Client-side routingwasm-bindgen- JavaScript interop
Structure:
rustmail_panel/src/
├── main.rs # App entry point
├── app.rs # Root component
├── components/ # UI components
├── pages/ # Page components
├── i18n/ # Translations
└── utils/ # Client utilities
rustmail_types (Shared Types)
Type definitions shared between crates.
rustmail_types/src/
├── lib.rs
├── api/ # API types
│ └── panel_permissions.rs
└── config/ # Configuration types
├── bot.rs
├── commands.rs
├── error_handling.rs
├── languages.rs
├── logs.rs
├── notifications.rs
├── reminders.rs
└── threads.rs
Runtime Architecture
┌─────────────────────────────────────────────────────────────┐
│ Rustmail Process │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Discord │ │ Axum │ │ SQLite │ │
│ │ Gateway │ │ Server │ │ Database │ │
│ │ (Serenity) │ │ (API) │ │ (SQLx) │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └─────────┬─────────┴─────────┬─────────┘ │
│ │ │ │
│ ┌──────┴───────┐ ┌──────┴───────┐ │
│ │ Shared │ │ Background │ │
│ │ State │ │ Tasks │ │
│ └──────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Discord API │ │ Web Browser │
│ (Bot) │ │ (Panel) │
└─────────────────┘ └─────────────────┘
Discord Integration
Gateway Events
Rustmail reacts to various Discord gateway events. Here are the main ones:
| Event | Handler | Purpose |
|---|---|---|
ready | ReadyHandler | Initialize bot state |
message_create | GuildMessagesHandler | Process DMs and commands |
interaction_create | InteractionHandler | Handle slash commands |
typing_start | TypingProxyHandler | Forward typing indicators |
All event handlers are located in rustmail/src/handlers/.
Commands System
Commands are defined in rustmail/src/commands/:
commands/
├── mod.rs # Command registration
├── help/
├── reply/
├── close/
├── new_thread/
└── ...
Each command module contains:
- Command definition (slash command builder)
- Text command parser
- Handler function
API Architecture
The HTTP server uses Axum with these route groups:
/api
├── /auth # OAuth2 flow
│ ├── /login
│ ├── /callback
│ └── /logout
├── /bot # Bot control
│ ├── /status
│ ├── /start
│ ├── /stop
│ ├── /restart
│ ├── /config
│ └── /tickets
├── /apikeys # API key management
├── /admin # Administration
├── /user # User info
├── /panel # Panel data
└── /externals # External integrations
└── /tickets/create
Middleware
- Session authentication (cookie-based)
- API key authentication (header-based)
- Permission checking
Database Layer
Connection Pool
SQLx manages a connection pool to the SQLite database:
#![allow(unused)]
fn main() {
let pool = SqlitePool::connect("sqlite:db.sqlite").await?;
}
Migrations
Schema changes use SQLx migrations:
migrations/
├── 20250815145017_create_tables.sql
├── 20250815161000_unique_open_and_metadata.sql
└── ...
Migrations run automatically at startup.
Query Pattern
Database operations in rustmail/src/database/:
#![allow(unused)]
fn main() {
pub async fn get_thread_by_id(pool: &SqlitePool, id: &str) -> Result<Thread> {
sqlx::query_as!(Thread, "SELECT * FROM threads WHERE id = ?", id)
.fetch_one(pool)
.await
}
}
Internationalization
Bot (rustmail)
Internal i18n system in rustmail/src/i18n/:
#![allow(unused)]
fn main() {
pub enum Language {
English,
French,
Spanish,
// ...
}
impl Language {
pub fn get_message(&self, key: &str) -> &str {
// Translation lookup
}
}
}
Panel (rustmail_panel)
JSON-based translations loaded at runtime:
rustmail_panel/src/i18n/
├── mod.rs
└── translations/
├── en.json
└── fr.json
Background Tasks
Long-running tasks managed by Tokio:
| Task | Module | Purpose |
|---|---|---|
| Reminders | modules/reminders.rs | Check and fire reminders |
| Scheduled closures | modules/scheduled_closures.rs | Auto-close tickets |
| Thread status | modules/threads_status_updates.rs | Update thread metadata |
| Features polling | modules/features_polling.rs | Track feature requests |
State Management
Shared State
Global state shared across handlers:
#![allow(unused)]
fn main() {
pub struct Config {
pub bot: BotConfig,
pub thread: ThreadConfig,
// ...
pub db_pool: Option<SqlitePool>,
pub thread_locks: Arc<Mutex<HashMap<u64, Arc<Mutex<()>>>>>,
}
}
Thread Locking
Prevents race conditions on ticket operations:
#![allow(unused)]
fn main() {
let lock = config.thread_locks
.lock()
.entry(thread_id)
.or_insert_with(|| Arc::new(Mutex::new(())))
.clone();
let _guard = lock.lock().await;
// Thread-safe operations
}
Build Pipeline
Development
# Bot only
cargo build -p rustmail
# Panel (requires trunk)
cd rustmail_panel
trunk build
# All
cargo build --workspace
Release
# Optimized build
cargo build --release -p rustmail
# Panel with optimization
trunk build --release
CI/CD
GitHub Actions workflow:
- Build and test all crates
- Build panel WASM
- Create release binaries
- Build and push Docker image
Extension Points
Adding Commands
- Create module in
rustmail/src/commands/ - Implement slash and text command handlers
- Register in
commands/mod.rs
Adding API Endpoints
- Create handler in
rustmail/src/api/handler/ - Add route in
rustmail/src/api/routes/ - Apply middleware as needed
Adding Translations
- Add language to
Languageenum - Implement translations
- Add to
supported_languagesconfig
Building from Source
This guide covers compiling Rustmail from source code.
Prerequisites
Rust Toolchain
Install Rust via rustup:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Minimum version: Rust 1.85+ (Edition 2024)
Verify installation:
rustc --version
cargo --version
WASM Target (for Panel)
If building the web panel:
rustup target add wasm32-unknown-unknown
cargo install trunk
System Dependencies
Debian/Ubuntu:
apt-get install build-essential pkg-config libssl-dev
Fedora:
dnf install gcc openssl-devel
macOS:
xcode-select --install
Windows:
- Visual Studio Build Tools with C++ workload
- Or use WSL2 with Linux instructions
Clone Repository
git clone https://github.com/Rustmail/rustmail.git
cd rustmail
Build Commands
Bot Only (Quick)
cargo build -p rustmail --release
Output: target/release/rustmail
Panel Only
cd rustmail_panel
trunk build --release --dist ../rustmail/static
Output: rustmail/static/
Full Build (Panel + Bot)
The panel must be built first and placed in rustmail/static/ so the bot can embed it:
# Build panel and output to bot's static folder
cd rustmail_panel
trunk build --release --dist ../rustmail/static
cd ..
# Build bot (embeds the panel)
cargo build -p rustmail --release
Output: target/release/rustmail (single binary with embedded panel)
Development Build
For faster iteration during development:
# Debug build (faster compile, slower runtime)
cargo build -p rustmail
# Run directly
cargo run -p rustmail
# With automatic recompilation
cargo install cargo-watch
cargo watch -x "run -p rustmail"
Panel Development
cd rustmail_panel
# Development server with hot reload
trunk serve
# Opens at http://localhost:8080
Running Tests
# All tests
cargo test --workspace
# Specific crate
cargo test -p rustmail
# With output
cargo test -- --nocapture
Checking Code
# Type checking without building
cargo check --workspace
# Linting
cargo clippy --workspace
# Formatting
cargo fmt --all --check
Build Optimization
Custom Release Profile
You can add a custom release profile to your workspace Cargo.toml for optimized builds:
[profile.release]
lto = true
codegen-units = 1
opt-level = 3
Smaller Binary
For reduced binary size, use:
[profile.release]
strip = true
lto = true
codegen-units = 1
opt-level = "z"
Note: These profiles increase compile time but produce smaller/faster binaries.
Cross-Compilation
Linux to Windows
rustup target add x86_64-pc-windows-gnu
cargo build -p rustmail --release --target x86_64-pc-windows-gnu
Linux to macOS
Cross-compilation to macOS requires additional setup. Consider using GitHub Actions or a macOS machine.
Using Cross
For easier cross-compilation:
cargo install cross
# Build for various targets
cross build -p rustmail --release --target x86_64-unknown-linux-musl
cross build -p rustmail --release --target aarch64-unknown-linux-gnu
Docker Build
Build the Docker image locally:
docker build -t rustmail:local .
Multi-platform build:
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t rustmail:local .
Troubleshooting
OpenSSL Errors
error: failed to run custom build command for `openssl-sys`
Install OpenSSL development files:
# Debian/Ubuntu
apt-get install libssl-dev
# Fedora
dnf install openssl-devel
# macOS
brew install openssl
Trunk Not Found
cargo install trunk
Ensure ~/.cargo/bin is in your PATH.
WASM Build Errors
# Ensure target is installed
rustup target add wasm32-unknown-unknown
# Update trunk
cargo install trunk --force
Out of Memory
Large builds may need more memory:
# Limit parallel jobs
cargo build -j 2 --release
SQLx Compile-Time Verification
SQLx verifies queries at compile time. If you see database errors:
# Set DATABASE_URL for offline builds
export DATABASE_URL="sqlite:db.sqlite"
cargo build -p rustmail
Or use offline mode:
cargo sqlx prepare --workspace
IDE Setup
RustRover / IntelliJ
The repository includes a pre-configured run configuration in .run/Run rustmail.run.xml.
This configuration:
- Builds the panel with Trunk
- Outputs to
rustmail/static/ - Runs the bot
To use it:
- Open the project in RustRover
- Select “Run rustmail” from the run configurations dropdown
- Click Run
The equivalent command:
cd rustmail_panel && trunk build --release --dist ../rustmail/static && cd .. && cargo run -p rustmail
VS Code
Recommended extensions:
- rust-analyzer
- CodeLLDB (debugging)
- Even Better TOML
Settings (.vscode/settings.json):
{
"rust-analyzer.cargo.features": "all",
"rust-analyzer.check.command": "clippy"
}
Project Layout Reference
rustmail/
├── Cargo.toml # Workspace manifest
├── Cargo.lock # Dependency lock file
├── rustmail/ # Main bot crate
│ ├── Cargo.toml
│ ├── src/
│ └── static/ # Panel build output (embedded)
├── rustmail_panel/ # Web panel crate (Yew/WASM)
│ ├── Cargo.toml
│ ├── Trunk.toml
│ ├── index.html
│ └── src/
├── rustmail_types/ # Shared types crate
│ ├── Cargo.toml
│ └── src/
├── rustmail-i18n/ # Internationalization resources
├── migrations/ # SQLite migrations
├── docs/ # Documentation (mdBook)
├── .run/ # IDE run configurations
├── config.example.toml # Example configuration
└── Dockerfile
Contributing Guide
Thank you for your interest in contributing to Rustmail. This guide explains how to contribute effectively.
Getting Started
- Fork the repository on GitHub
- Clone your fork locally
- Set up the development environment (see Building)
- Create a branch for your changes
git clone https://github.com/YOUR_USERNAME/rustmail.git
cd rustmail
git checkout -b feature/your-feature-name
Development Workflow
1. Find or Create an Issue
- Check existing issues for something to work on
- For new features, open an issue first to discuss the approach
- Bug fixes can go directly to a pull request
2. Make Changes
- Write clean, readable code
- Follow existing patterns in the codebase
- Add tests for new functionality
- Update documentation as needed
3. Test Your Changes
# Run tests
cargo test --workspace
# Check formatting
cargo fmt --all --check
# Run linter
cargo clippy --workspace
4. Commit
Write clear commit messages:
feat: add snippet management command
- Add /snippet command for using saved responses
- Add snippet CRUD operations in database
- Include tests for snippet operations
Commit message prefixes:
feat:- New featurefix:- Bug fixdocs:- Documentation changesrefactor:- Code refactoringtest:- Test additions/changeschore:- Build/tooling changes
5. Submit Pull Request
- Push your branch to your fork
- Open a pull request against
main - Fill out the PR template
- Wait for review
Code Style
Rust
- Follow standard Rust conventions
- Use
cargo fmtfor formatting - Address
cargo clippywarnings - Prefer explicit types over inference when it aids readability
#![allow(unused)]
fn main() {
// Good
let thread_id: u64 = message.channel_id.get();
// Also acceptable when obvious
let content = message.content.clone();
}
Documentation
- Document public APIs with doc comments
- Include examples for complex functions
- Keep comments current with code
#![allow(unused)]
fn main() {
/// Creates a new ticket for the specified user.
///
/// # Arguments
///
/// * `user_id` - Discord user ID
/// * `initial_message` - Optional first message content
///
/// # Returns
///
/// The created thread's channel ID.
pub async fn create_ticket(user_id: u64, initial_message: Option<&str>) -> Result<u64> {
// ...
}
}
Adding Features
New Commands
- Create a module in
rustmail/src/commands/ - Implement the command handler
- Add slash command definition
- Add text command parser
- Register in
commands/mod.rs - Add to documentation
New API Endpoints
- Create handler in
rustmail/src/api/handler/ - Define route in
rustmail/src/api/routes/ - Add authentication/authorization as needed
- Document in
docs/reference/api.md
Database Changes
- Create migration in
migrations/ - Update relevant structs in
rustmail_types - Add database functions
- Test migration up and down
Pull Request Guidelines
Before Submitting
- Code compiles without errors
- All tests pass
- Code is formatted (
cargo fmt) - No clippy warnings
- Documentation updated
- Commit messages are clear
PR Description
Include:
- What the change does
- Why it’s needed
- How to test it
- Any breaking changes
Review Process
- Maintainers will review the PR
- Address feedback with additional commits
- Once approved, the PR will be merged
Testing
Unit Tests
Place tests in the same file as the code:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_duration() {
assert_eq!(parse_duration("1h"), Some(3600));
assert_eq!(parse_duration("30m"), Some(1800));
}
}
}
Integration Tests
For tests requiring multiple components, use tests/ directory.
Running Specific Tests
# Single test
cargo test test_name
# Tests in a module
cargo test module_name::
# With output
cargo test -- --nocapture
Translations
Adding a New Language
- Add variant to
Languageenum inrustmail/src/i18n/ - Create translation module
- Add JSON file for panel in
rustmail_panel/src/i18n/translations/ - Test all strings render correctly
Updating Translations
- Keep all languages in sync
- Use English as the source
- Maintain consistency in terminology
Reporting Issues
Bug Reports
Include:
- Rustmail version
- Operating system
- Steps to reproduce
- Expected vs actual behavior
- Relevant logs
Feature Requests
Include:
- Use case description
- Proposed solution
- Alternative approaches considered
Communication
- GitHub Issues - Bug reports, feature requests
- GitHub Discussions - Questions, ideas
- Discord Server - Real-time chat
License
By contributing, you agree that your contributions will be licensed under the AGPLv3 license.
Recognition
Contributors are recognized in:
- Git history
- Release notes for significant contributions
- README acknowledgments for major features