Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. Installation - Download and system requirements
  2. Configuration - Set up your config.toml
  3. First Steps - Launch the bot and verify setup

User Guides

Learn how to use Rustmail effectively:

GuideDescription
CommandsComplete reference for all slash and text commands
Server ModesSingle-server vs dual-server architecture
TicketsManaging support tickets and conversations
Web PanelUsing the administration interface

Reference

Technical documentation for advanced users:

DocumentDescription
Configuration OptionsAll config.toml settings explained
REST APIHTTP endpoints for integrations
Database SchemaSQLite table structures

Deployment

Production deployment guides:

GuideDescription
DockerContainer-based deployment
ProductionBest practices for production environments

Development

For contributors and developers:

DocumentDescription
ArchitectureProject structure and design
BuildingCompile from source
ContributingContribution guidelines

Installation

This guide covers downloading and setting up Rustmail on your system.


Prerequisites

Discord Bot Application

Before installing Rustmail, create a Discord application:

  1. Go to the Discord Developer Portal
  2. Click New Application and give it a name
  3. Navigate to Bot in the sidebar
  4. Click Add Bot
  5. Under Privileged Gateway Intents, enable:
    • Presence Intent
    • Server Members Intent
    • Message Content Intent
  6. Copy the bot token (you will need it for configuration)

Bot Invitation

Invite the bot to your server(s) with the required permissions:

  1. Go to OAuth2 > URL Generator
  2. Select scopes: bot, applications.commands
  3. Select permissions:
    • Manage Channels
    • Read Messages/View Channels
    • Send Messages
    • Manage Messages
    • Embed Links
    • Attach Files
    • Read Message History
    • Add Reactions
    • Use Slash Commands
  4. Copy the generated URL and open it in your browser
  5. 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:

config.rustmail.rs

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."
FieldDescription
tokenYour Discord bot token from the Developer Portal
statusText displayed as the bot’s activity status
welcome_messageSent to users when they open a new ticket
close_messageSent 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_id is where your users are
  • staff_guild_id is 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

  1. In the Discord Developer Portal, select your application
  2. Go to OAuth2 > General
  3. Copy the Client ID and Client Secret
  4. 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:

FieldPurposeRequired
redirect_urlPublic URL for OAuth2 authentication and log linksYes (if panel enabled)
ipNetwork interface binding addressNo (defaults to auto-detect)

The redirect_url Field (Important)

The redirect_url is your panel’s public URL. It is used for:

  1. OAuth2 authentication - Discord redirects users here after login
  2. 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)

  1. 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
  2. Configure Rustmail:

    [bot]
    enable_panel = true
    redirect_url = "https://panel.example.com/api/auth/callback"
    
  3. Update Discord OAuth2:

    • Add https://panel.example.com/api/auth/callback to your redirect URIs

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 (http vs https)
  • 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 IDs
  • panel_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:

  1. Verify the file is named config.toml and is in the same directory as the executable
  2. 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:

  1. Type /help and press Enter
  2. 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.commands scope
  • Check that the bot has permissions in the channel

3. Test Ticket Creation

  1. Send a direct message to the bot
  2. The bot should:
    • Reply with your configured welcome_message
    • Create a new channel in your inbox category
  3. Staff can now respond using /reply or !reply in 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 = false to 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_id and client_secret match your Discord application
  • Ensure redirect_url exactly 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 Reference

Rustmail provides both slash commands and text commands. Text commands use a configurable prefix (default: !).


Command Types

TypeExampleDescription
Slash/reply helloDiscord’s native slash commands with autocomplete
Text!reply helloTraditional 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.

SlashText
/help!help

ping

Check bot responsiveness.

SlashText
/ping!ping

Ticket Management

new_thread

Create a new ticket for a user. Use when you need to initiate contact.

SlashText
/new_thread <user>!new_thread <user_id>

Parameters:

  • user - The Discord user to create a ticket for

close

Close the current ticket.

SlashText
/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 user
  • cancel - 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.

SlashText
/force_close!force_close

move_thread

Move a ticket to a different category.

SlashText
/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.

SlashText
/recover!recover

Messaging

reply

Send a message to the ticket user.

SlashText
/reply <content> [attachment] [anonymous]!reply <content>

Parameters:

  • content - Message text
  • attachment - Optional file attachment
  • anonymous - 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.

SlashText
/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.

SlashText
/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.

SlashText
/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.

SlashText
/remove_staff <user>!remove_staff <user_id>

Parameters:

  • user - Staff member to remove

take

Assign yourself to the ticket (claim ownership).

SlashText
/take!take

release

Release your assignment from the ticket.

SlashText
/release!release

Reminders

add_reminder

Set a reminder for the current ticket.

SlashText
/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.

SlashText
/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.

SlashText
/alert [cancel]!alert [cancel]

Parameters:

  • cancel - Remove an existing alert

Information

id

Display the ticket user’s Discord ID.

SlashText
/id!id

logs

View ticket history and logs.

SlashText
/logs!logs

status

View or change the bot’s operational status.

SlashText
/status [new_status]!status [new_status]

Snippets

Snippets are saved responses that can be quickly inserted.

snippet

Use a saved snippet.

SlashText
/snippet <key>!snippet <key>

Snippet management is done through the web panel or database directly.


Time Format Reference

For commands accepting time durations:

UnitExampleDescription
s30sSeconds
m15mMinutes
h2hHours
d1dDays

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

ModeServersUse Case
Single1Small communities, simple setup
Dual2Large 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

  1. Create a category for tickets (e.g., “Tickets”)
  2. Set category permissions:
    • @everyone: Deny View Channel
    • Staff roles: Allow View Channel
  3. 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

  1. Create or designate your staff server
  2. Invite the bot to both servers
  3. In the staff server, create a category for tickets
  4. 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

  1. Create a new staff server
  2. Invite the bot to the staff server
  3. Create a tickets category in the staff server
  4. 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>
    
  5. 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

  1. Update config.toml:
    [bot.mode]
    type = "single"
    guild_id = <your_server_id>
    
    [thread]
    inbox_category_id = <category_id_in_that_server>
    
  2. Restart the bot

Getting Server and Category IDs

  1. Enable Developer Mode in Discord:
    • User Settings > App Settings > Advanced > Developer Mode
  2. Right-click on a server icon > Copy Server ID
  3. 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_thread to 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 /reply or !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_leave is 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:

  1. User finds the bot in the server member list
  2. User sends a direct message
  3. Bot creates a ticket channel
  4. Bot sends welcome_message to 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_edit is 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_delete is 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:

OptionDescription
welcome_messageSent when ticket opens
close_messageSent when ticket closes
time_to_close_threadDefault scheduled close time
close_on_leaveAuto-close when user leaves server
embedded_messageDisplay messages as embeds
block_quoteUse 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

  1. Click Login on the panel homepage
  2. You are redirected to Discord’s OAuth2 authorization
  3. Authorize the application
  4. Discord redirects back to the panel
  5. 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

  1. Navigate to API Keys in the panel
  2. Click Create New Key
  3. Enter a descriptive name (e.g., “Website Contact Form”)
  4. Optionally set an expiration date
  5. 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:

  1. Use HTTPS - Run behind a reverse proxy with TLS
  2. Restrict access - Use firewall rules to limit who can reach the panel
  3. 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_url exactly matches Discord Developer Portal
  • Check for protocol (http vs https) and trailing slash differences

Client ID/Secret incorrect:

  • Regenerate the client secret in Discord Developer Portal
  • Update config.toml and 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

PathDescription
/app/config.tomlConfiguration file (required)
/app/dbDatabase directory (persistent storage)

Ports

PortDescription
3002Web panel and API

Environment Variables

VariableDescription
TZContainer 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.toml syntax
  • 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/db volume 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
  • 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:

  1. Reverse proxy with TLS termination (recommended)

    • Nginx, Caddy, Traefik
    • Automatic certificate renewal with Let’s Encrypt
  2. 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

ItemLocationFrequency
Databasedb.sqliteDaily
Configurationconfig.tomlOn 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:

  1. Close active tickets or notify staff
  2. Stop the bot
  3. Backup database
  4. Apply update
  5. Verify configuration
  6. Start the bot
  7. 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

  1. Check process status: systemctl status rustmail
  2. Check logs for errors
  3. Verify Discord API status
  4. Check network connectivity
  5. Verify token validity

Panel Inaccessible

  1. Check bot process is running
  2. Verify port 3002 is listening: netstat -tlnp | grep 3002
  3. Check reverse proxy configuration
  4. Verify SSL certificate validity
  5. Check firewall rules

Performance Issues

  1. Monitor CPU/memory usage
  2. Check database size
  3. Review ticket volume
  4. 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

OptionTypeRequiredDefaultDescription
tokenstringYes-Discord bot token from Developer Portal
statusstringYes-Bot’s activity status message
welcome_messagestringYes-Message sent to users when opening a ticket
close_messagestringYes-Message sent to users when ticket is closed

Typing Indicators

OptionTypeRequiredDefaultDescription
typing_proxy_from_userboolYes-Show typing indicator in ticket when user types
typing_proxy_from_staffboolYes-Show typing indicator in DM when staff types

Feature Toggles

OptionTypeRequiredDefaultDescription
enable_logsboolYes-Enable logging to a channel
enable_featuresboolYes-Enable feature request tracking
enable_panelboolYes-Enable web administration panel

Channel Configuration

OptionTypeRequiredDefaultDescription
logs_channel_idu64Conditional-Channel for bot logs. Required if enable_logs = true
features_channel_idu64Conditional-Channel for feature requests. Required if enable_features = true

OAuth2 (Panel)

Required when enable_panel = true.

OptionTypeRequiredDefaultDescription
client_idu64Conditional-Discord application client ID
client_secretstringConditional-Discord application client secret
redirect_urlstringConditional-OAuth2 callback URL (must match Discord config)

Network

OptionTypeRequiredDefaultDescription
ipstringNoAuto-detectedNetwork 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

OptionTypeRequiredDefaultDescription
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

OptionTypeRequiredDefaultDescription
timezonestringNoUTCTimezone 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
OptionTypeRequiredDescription
typestringYesMust be "single"
guild_idu64YesYour Discord server ID

Dual Server Mode

[bot.mode]
type = "dual"
community_guild_id = 123456789012345678
staff_guild_id = 987654321098765432
OptionTypeRequiredDescription
typestringYesMust be "dual"
community_guild_idu64YesServer where users are
staff_guild_idu64YesServer where tickets are created

Command Section

[command]
OptionTypeRequiredDefaultDescription
prefixstringYes"!"Prefix for text commands

Thread Section

[thread]

Required Settings

OptionTypeRequiredDefaultDescription
inbox_category_idu64Yes-Category where ticket channels are created

Message Display

OptionTypeRequiredDefaultDescription
embedded_messageboolYes-Display messages as Discord embeds
user_message_colorstringYes"5865f2"Hex color for user messages (without #)
staff_message_colorstringYes"57f287"Hex color for staff messages (without #)
system_message_colorstringYes"faa81a"Hex color for system messages (without #)
block_quoteboolYes-Use block quotes for message content

Ticket Behavior

OptionTypeRequiredDefaultDescription
time_to_close_threadu64Yes0Default minutes until auto-close (0 = disabled)
create_ticket_by_create_channelboolYes-Allow ticket creation by making a channel
close_on_leaveboolNofalseAuto-close when user leaves server
auto_archive_durationu16No10080Thread auto-archive time in minutes

Language Section

[language]
OptionTypeRequiredDefaultDescription
default_languagestringYes"en"Default language code
fallback_languagestringYes"en"Fallback when translation missing
supported_languages[string]Yes["en", "fr"]Available languages

Available Language Codes

CodeLanguage
enEnglish
frFrench
esSpanish
deGerman
itItalian
ptPortuguese
ruRussian
zhChinese
jaJapanese
koKorean

Notifications Section

[notifications]

Control feedback messages shown to staff.

OptionTypeRequiredDefaultDescription
show_success_on_editboolYestrueConfirm successful edits
show_partial_success_on_editboolYestrueNotify on partial edit success
show_failure_on_editboolYestrueNotify on edit failure
show_success_on_replyboolYestrueConfirm sent replies
show_success_on_deleteboolYestrueConfirm deletions
show_successboolNotrueGeneral success notifications
show_errorboolNotrueGeneral error notifications

Logs Section

[logs]

Control what actions are logged.

OptionTypeRequiredDefaultDescription
show_log_on_editboolYestrueLog message edits
show_log_on_deleteboolYestrueLog message deletions

Reminders Section

[reminders]
OptionTypeRequiredDefaultDescription
embed_colorstringYes"ffcc00"Hex color for reminder embeds (without #)

Error Handling Section

[error_handling]
OptionTypeRequiredDefaultDescription
show_detailed_errorsboolYestrueShow technical details in errors
log_errorsboolYestrueLog errors to console
send_error_embedsboolYestrueSend errors as embeds
auto_delete_error_messagesboolYesfalseAuto-delete error messages
error_message_ttlu64No-Seconds before error deletion
display_errorsboolNotrueShow 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:

HeaderValueDescription
X-API-Keyrustmail_...Your API key
Content-Typeapplication/jsonRequired 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 Discord
  • state - 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"
}
FieldTypeDescription
statusstringBot state: "running" or "stopped"
presencestringCurrent 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"
}
ValueDescription
onlineOnline status, shows configured activity
idleIdle/Away status
dndDo Not Disturb status
invisibleInvisible/Offline status
maintenanceMaintenance 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:

ParameterTypeDefaultDescription
idstring-Get a specific ticket by ID
pageint1Page number
page_sizeint50Items per page (max 200)
statusint1Filter: 1 = open, 0 = closed
category_idstring-Filter by category ID
sort_bystringcreated_atSort field: created_at, closed_at, user_name
sort_orderstringDESCSort 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"
}
FieldTypeRequiredDescription
discord_idstringYesDiscord user ID to create a ticket for
staff_discord_idstringNoStaff 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:

StatusError
400Invalid Discord ID format
403User is not a member of the community guild
404Discord user not found
409User 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
}
PermissionDescription
CreateTicketCan 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 panel
  • ManageBot - Can start/stop/restart the bot
  • ManageConfig - Can edit configuration
  • ManageTickets - Can manage tickets
  • ManageApiKeys - Can create/revoke API keys
  • ManagePermissions - Can grant/revoke permissions

Subject types:

  • User - Permission granted to a specific user
  • Role - 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

CodeMeaning
200Success
204Success (no content)
400Bad request (invalid parameters)
401Unauthorized (missing/invalid authentication)
403Forbidden (insufficient permissions)
404Not found
409Conflict (e.g., ticket already exists)
500Internal 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.

ColumnTypeDescription
idTEXTPrimary key (UUID)
user_idINTEGERDiscord user ID
user_nameTEXTUsername at ticket creation
channel_idTEXTDiscord channel ID
created_atDATETIMETicket creation timestamp
next_message_numberINTEGERCounter for message numbering
statusINTEGERTicket status (1=open, 0=closed)
user_leftBOOLEANWhether user left the server
closed_atDATETIMEClosure timestamp (nullable)
closed_byTEXTStaff who closed (nullable)
category_idTEXTCurrent category ID (nullable)
category_nameTEXTCurrent category name (nullable)
required_permissionsTEXTPermission requirements (nullable)

thread_messages

Stores all messages in tickets.

ColumnTypeDescription
idINTEGERPrimary key (auto-increment)
thread_idTEXTForeign key to threads
user_idINTEGERAuthor’s Discord ID
user_nameTEXTAuthor’s username
is_anonymousBOOLEANWhether sent anonymously
dm_message_idTEXTDiscord message ID in DM
inbox_message_idTEXTDiscord message ID in ticket channel
message_numberINTEGERSequential message number
created_atDATETIMEMessage timestamp
contentTEXTMessage content
thread_statusINTEGERThread status when sent

blocked_users

Stores blocked users who cannot create tickets.

ColumnTypeDescription
user_idTEXTPrimary key (Discord user ID)
user_nameTEXTUsername when blocked
blocked_byTEXTStaff who blocked
blocked_atDATETIMEBlock timestamp
expires_atDATETIMEBlock expiration

staff_alerts

Stores alert subscriptions for tickets.

ColumnTypeDescription
idINTEGERPrimary key
staff_user_idINTEGERStaff Discord ID
thread_user_idINTEGERTicket user Discord ID
created_atDATETIMEAlert creation time
usedBOOLEANWhether alert was triggered

reminders

Stores scheduled reminders.

ColumnTypeDescription
idINTEGERPrimary key
thread_idTEXTForeign key to threads
user_idBIGINTStaff Discord ID
channel_idBIGINTChannel Discord ID
guild_idBIGINTServer Discord ID
reminder_contentTEXTReminder message
trigger_timeINTEGERUnix timestamp to trigger
created_atINTEGERCreation Unix timestamp
completedBOOLEANWhether reminder fired

scheduled_closures

Stores scheduled ticket closures.

ColumnTypeDescription
idINTEGERPrimary key
thread_idTEXTForeign key to threads
scheduled_timeINTEGERUnix timestamp for closure
silentBOOLEANClose without notification
created_byTEXTStaff who scheduled

snippets

Stores saved response templates.

ColumnTypeDescription
idINTEGERPrimary key
keyTEXTUnique snippet identifier
contentTEXTSnippet text
created_byTEXTCreator Discord ID
created_atDATETIMECreation timestamp
updated_atDATETIMELast update timestamp

sessions_panel

Stores web panel sessions.

ColumnTypeDescription
session_idTEXTPrimary key (session token)
user_idTEXTDiscord user ID
access_tokenTEXTDiscord OAuth2 access token
refresh_tokenTEXTDiscord OAuth2 refresh token
expires_atINTEGERSession expiration Unix timestamp
avatar_hashTEXTUser’s avatar hash

api_keys

Stores API keys for external access.

ColumnTypeDescription
idINTEGERPrimary key
key_hashTEXTHashed API key (unique)
nameTEXTKey description
permissionsTEXTJSON array of permissions
created_atINTEGERCreation Unix timestamp
expires_atINTEGERExpiration timestamp (nullable)
last_used_atINTEGERLast usage timestamp (nullable)
is_activeINTEGERWhether key is active

panel_permissions

Stores granted panel permissions.

ColumnTypeDescription
idINTEGERPrimary key
subject_typeTEXT“user” or “role”
subject_idTEXTDiscord user/role ID
permissionTEXTPermission name
granted_byTEXTWho granted it
granted_atINTEGERGrant Unix timestamp

features_messages

Stores feature request tracking.

ColumnTypeDescription
idINTEGERPrimary key
message_idTEXTDiscord message ID
contentTEXTFeature description

thread_status

Stores thread status history.

ColumnTypeDescription
idINTEGERPrimary key
thread_idTEXTForeign key to threads
statusINTEGERStatus value
changed_atDATETIMEChange timestamp

Indexes

Performance indexes on frequently queried columns:

  • threads_id_key on threads(id)
  • thread_messages_id_key on thread_messages(id)
  • idx_api_keys_hash on api_keys(key_hash)
  • idx_api_keys_active on api_keys(is_active)
  • idx_snippets_key on snippets(key)
  • idx_panel_perms_subject on panel_permissions(subject_type, subject_id)
  • idx_panel_perms_permission on panel_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 client
  • sqlx - Async SQLite database
  • axum - HTTP server for panel/API
  • tokio - 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 framework
  • yew-router - Client-side routing
  • wasm-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:

EventHandlerPurpose
readyReadyHandlerInitialize bot state
message_createGuildMessagesHandlerProcess DMs and commands
interaction_createInteractionHandlerHandle slash commands
typing_startTypingProxyHandlerForward 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:

TaskModulePurpose
Remindersmodules/reminders.rsCheck and fire reminders
Scheduled closuresmodules/scheduled_closures.rsAuto-close tickets
Thread statusmodules/threads_status_updates.rsUpdate thread metadata
Features pollingmodules/features_polling.rsTrack 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:

  1. Build and test all crates
  2. Build panel WASM
  3. Create release binaries
  4. Build and push Docker image

Extension Points

Adding Commands

  1. Create module in rustmail/src/commands/
  2. Implement slash and text command handlers
  3. Register in commands/mod.rs

Adding API Endpoints

  1. Create handler in rustmail/src/api/handler/
  2. Add route in rustmail/src/api/routes/
  3. Apply middleware as needed

Adding Translations

  1. Add language to Language enum
  2. Implement translations
  3. Add to supported_languages config

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:

  1. Builds the panel with Trunk
  2. Outputs to rustmail/static/
  3. Runs the bot

To use it:

  1. Open the project in RustRover
  2. Select “Run rustmail” from the run configurations dropdown
  3. 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

  1. Fork the repository on GitHub
  2. Clone your fork locally
  3. Set up the development environment (see Building)
  4. 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 feature
  • fix: - Bug fix
  • docs: - Documentation changes
  • refactor: - Code refactoring
  • test: - Test additions/changes
  • chore: - 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 fmt for formatting
  • Address cargo clippy warnings
  • 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

  1. Create a module in rustmail/src/commands/
  2. Implement the command handler
  3. Add slash command definition
  4. Add text command parser
  5. Register in commands/mod.rs
  6. Add to documentation

New API Endpoints

  1. Create handler in rustmail/src/api/handler/
  2. Define route in rustmail/src/api/routes/
  3. Add authentication/authorization as needed
  4. Document in docs/reference/api.md

Database Changes

  1. Create migration in migrations/
  2. Update relevant structs in rustmail_types
  3. Add database functions
  4. 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

  1. Maintainers will review the PR
  2. Address feedback with additional commits
  3. 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

  1. Add variant to Language enum in rustmail/src/i18n/
  2. Create translation module
  3. Add JSON file for panel in rustmail_panel/src/i18n/translations/
  4. 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