dms-gui
Graphical user interface for managing Docker-Mailserver
10K+
A graphical user interface for managing DMS (Docker-Mailserver) and other non-gui mailservers like Poste.io. This gui aims to manage all aspects of DMS including: email accounts, aliases, xapian indexes, DNS entries, and other stuff.
It relies on a generic REST API written in python, that you have to mount in DMS compose.
| dms | dms-gui | x86_64 | aarch64 | details |
|---|---|---|---|---|
| v15.1.0 | v1.5 | ✔️ | ✔️ | dovecot 2.3 |
| v16? | ❌ | ❌ | ❌ | dovecot 2.4 |
Simply, by executing
systemanddoveadmcommands inside DMS, through a python REST API.
Python REST API script and its loader are generated from dms-gui, and then mounted as a single volume in DMS compose, along with the exposed port. You don't need to alter
user-patches.shat all. The REST API script is conveniently placed in a folder that is mouted in DMS:./config/dms-gui/.
REST API Access security is handled with a key generated from dms-gui itself. The key is sent in query header of the http calls. Since the DMS REST API port is exposed on the docker network only, no one else has access to it.
Sure, it's in the
/backend/env.jsfile.
Top notch: best practice for React has been followed: CORS same-domain Strict + HTTPonly cookies + backend verification of credentials, zero trust of the frontend.
Two 32 bits secrets are generated when container starts: one for generateToken (valid 1h) and the other for refreshToken (valid 7 days). Refresh tokens are saved in the db for each user and invalidated when container restarts, since the secrets have changed.
Yes, the container relies on node-cron and restarts daily at 11PM to regenerate new secret keys. You can alter the schedule with the environment variable
DMSGUI_CRON.
Standard practice: passwords are stored in a local sqlite3 db as separate salt and hash. We only force users to use 8+ characters.
No, their credentials are set in the HTTPonly cookie and the backend only relies on its values to determine what's allowed.
No, sql commands are stored in a backend dictionary, and no frontend module can send sql commands directly. SQL is variabilized in the backend and cannot be injected. Routes are also protected following React best practices. The separation between frontend and backend is complete and interfaced with an API, just like Electron.js.
| user / Access | password | Profile | Dashboard | Accounts | Aliases | Logins | Settings | Backups | Imports |
|---|---|---|---|---|---|---|---|---|---|
| admins | dms-gui | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| users | dms-gui | ✔️ | ✔️ | partial | partial | ❌ | ❌ | partial | ❌ |
| linked users | DMS | ✔️ | partial | ❌ | partial | ❌ | ❌ | partial | ❌ |
Yes, users can change both their dms-gui password in their profile, and each of the mailboxes they control under Accounts. Logon password in dms-gui is saved in the database. Mailbox-linked users can only change the mailbox password, and their logon is handled by DMS dovecot directly.
Not yet, but it's coming.
No. This project has none of the React or 3rd party affected components like react-server-dom-turbopack, and is not even of the React versions affected. As I understand it, turbopack is another memory unsafe web bundler written in Rust, yet again.
As long as the default admin user (admin / password=changeme) exist, you are greeted with this message:

There you can change your dms-gui / DMS Dovecot password. Users managers of multiple mailboxes cannot change individual mailboxes yet.

Logins are 3 types:
| type | perks | details |
|---|---|---|
| admins | Administrator | Can even demote itself |
| users | Can manage multiple mailboxes | Not admin, cannot change managed mailboxes, Authentication by dms-gui |
| linked users | Can change their mailbox password | Authentication provided by DMS Dovecot |

Mailbox selection list comes from DMS directly. Password will be saved in both dms-gui and Dovecot in DMS, but Authentication for linked mailbox users is provided by DMS.

Mailbox users are automatically created, based off the scan of DMS dovecot server. The mechanic does not check if mailboxes have been deleted, it only pulls the current list and update the local db.

Also called "emails", as per the DMS setup command to create new email boxes, I prefer calling them mailboxes. They are Accounts, that can receive/store/send emails.
Accounts are automatically discovered and pulled from the local database for its speed. You can refresh the data manually with a simple click.
Creating accounts from here currently calls the DMS setup via docker.sock, but soon will rely on dovecot 2.4 API calls instead. Passwords entered are also stored in the local db.

Currently relying on DMS setup and a direct read of the postfix-regexp.cffile.

Multiple sections to save UI settings, DMS REST API access, and show some internals + DMS environment values.

dms-gui internals come from Node environment, and DMS values come from a mox of the env command and parsing dkim and dovecot configuration.
Some environment values like FTS (Full Text Search) will enable some options on the Accounts page (reindex for instance).

A dumb dashboard, but now you can click the cards and navigate to the section selected.

The application consists of two parts:
You have nothing to install, this is an all-included docker image that provides a GUI for DMS.
If you want to develop/pull requests and test, see README.docker.md and each README under the subfolders backend and frontend.
./config/dms-gui/ will host dms-gui.sqlite3 and its environment config file. This is a subfolder of the DMS config repository, for convenience; use any folder you want and update all the mounting points accordingly.
Rename ./config/dms-gui/.dms-gui.env.example as ./config/dms-gui/.dms-gui.env and update for your own environment:
###############################################################################
## dms-gui Configuration: all is handled by React.
## Only the defaults used in dms-gui will be mentionned here.
###############################################################################
## JWT_SECRET = secret for salting the cookies, regenerated during container start, before starting node
## JWT_SECRET_REFRESH = secret for salting the refresh cookies, regenerated during container start, before starting node
## Those keys cannot be defined anywhere else then during container start, and are secret as the name suggests
## docker/start.sh creates them
###############################################################################
## Optional: Dev Environment
# PORT_FRONTEND=3001 # ONLY set that up if you can serve the frontend locally and not inside the container
PORT_BACKEND=3000
BACKEND_PROXY_URL=http://localhost:${PORT_BACKEND}
FRONTEND_PROXY_URL=http://localhost:${PORT_FRONTEND}
UPSTREAM_NGINX_PRODUTION=try_files $$uri $$uri/ /index.html;
UPSTREAM_NGINX_DEVELOPMENT=proxy_pass ${FRONTEND_PROXY_URL}; proxy_http_version 1.1; proxy_set_header Upgrade $$http_upgrade; proxy_set_header Connection "upgrade";
## how long before rotation of the secrets:
ACCESS_TOKEN_EXPIRY=1h
REFRESH_TOKEN_EXPIRY=1d
## encryption options:
## IV_LEN is the length of the unique Initialization Vector (IV) = random salt used for encryption and hashing
IV_LEN=16
## HASH_LEN is the length of the hashed keys for passwords
HASH_LEN=64
#############################################################################
## AES_SECRET = encrypted data secret key, that one is set in the environment as well but must never change or you won;t be able to read your encrypted data anymore
## generate it once and for all with node or openssl:
## openssl rand -hex 32
## node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
AES_SECRET=replaceme
#############################################################################
## encrypted data algorithm
AES_ALGO=aes-256-cbc
## AES_HASH is the used to hash the secret key
AES_HASH=sha512
## utility paths for internal database
DMSGUI_CONFIG_PATH=/app/config
DATABASE=${DMSGUI_CONFIG_PATH}/dms-gui.sqlite3
## Override the daily restart of the container, with this simple trick: default is 11PM
## The container must restart regularly to regenerate the secret keys. Security first.
## ┌─────────────── second (optional)
## │ ┌───────────── minute
## │ │ ┌─────────── hour
## │ │ │ ┌──────── day of month 1-31
## │ │ │ │ ┌────── month 1-12
## │ │ │ │ │ ┌──── day of week 1-7
## │ │ │ │ │ │
## │ │ │ │ │ │
## * * * * * *
DMSGUI_CRON="* 1 23 * * *"
# enable fronted maps and debugging; define in compose is a better choice
# DEBUG=true
# make this a demo server; define in compose is a better choice
# isDEMO=true
# reset database during each restart; define in compose is a better choice
# DATABASE_RESET=true
# disable colors in backend logs, some terminals can't handle them
# LOG_COLORS=false
All is optional, as they will be superseeded by the ones defined and saved within dms-gui:
DEBUG: Node.js environment: (*production or development)
ACCESS_TOKEN_EXPIRY: lifetime of the generated HTTPonly token (1h)
REFRESH_TOKEN_EXPIRY: lifetime of the generated HTTPonly refresh token (1d)
DMSGUI_CRON: crontab format for daily restarts ("* 1 23 * * *")
LOG_COLORS: set false to disable colors in backend logs (*true)
isDEMO: set false to disable colors in backend logs (*false)
The ones you should never alter unless you want to develop:
PORT_FRONTEND: Internal port for the Node.js server (*3001)
API_URL: defaults to /api
BACKEND_PROXY_URL: defaults to http://localhost:3000
ENV_MODE: Node.js environment: (*production or development)
DMS_API_HOST: 0.0.0.0 // dms-gui's Python API injected into mailserver will use any addressDMS_API_PORT: 8888 // default port for the dms-gui's Python API injected into mailserverDMS_API_KEY: uuid // or whatever you like, must match the one in dms-gui (created there or vice-versa)DMS_API_SIZE: 1024 // max payload size the mailserver API will acceptLOG_LEVEL: info // *info, debug, warn, error: value should be set in your mailserver.envThe application supports multiple languages throught i18n.js:
Languages can be switched using the language selector in the top navigation bar.
There are two ways to deploy using Docker:
Sample extract from docker-compose.yml, rename dms to the actual name of your docker-Mailserver container!
---
services:
dms:
<your dms compose here>
...
environment:
# DMS_API_HOST: # defaults to 0.0.0.0
DMS_API_PORT: 8888 # defaults to 8888, must match what you se in dms-gui/Settings
DMS_API_KEY: uuid-random # key generated by you or dms-gui
# DMS_API_SIZE: 1024 # defaults to 1024
expose:
- "8888" # local python REST API, must match what you se in dms-gui/Settings
volumes:
...
# 1. you MUST create a subfolder "dms-gui" under your DMS config folder and mount in in the dms-gui section as "/app/config/"
# 2. AFTER you create and inject the API key under dms-gui Settings page, enable the API by uncommenting the line below and restart DMS
# 2. DO NOT enable the mount below until you created and saved the API in dms-gui, as the file is read only and cannot be created otherwise
- ./config/dms-gui/rest-api.conf:/etc/supervisor/conf.d/rest-api.conf:ro
networks:
frontend: # same network as dms-gui
gui:
container_name: dms-gui
hostname: dms-gui
image: audioscavenger/dms-gui:latest
restart: unless-stopped
depends_on:
- dms
# Use this environment file or the environment section, or both:
# Note: the file is placed under DMS own config folder;
# if using another one you will need to mount both the api.conf and api.py files in DMS
env_file: ./config/dms-gui/.dms-gui.env
- TZ=${TZ:-UTC}
- ENV_MODE=${ENV_MODE:-production}
- isDEMO=${isDEMO:-false} # DEMO mode
- DATABASE_RESET=${DATABASE_RESET:-false} # to reset the database each time; used for DEMO
- DEBUG=${DEBUG:-false} # enable debugging
expose:
- 80 # frontend via internal nginx
- 3001 # frontend: webpack-dev-server if you can get it to run inside the container... good luck
- 3000 # backend and docs
volumes:
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
# we are mounted under DMS own config folder:
- ./config/dms-gui/:/app/config/
networks:
frontend: # same network as DMS
# use the network of your choice, or default internal;
# DMS and dms-gui must be on the same network to see each others
networks:
frontend:
external: true
name: frontend
Note: Replace dms with the name of your docker-mailserver container.
Note: Replace frontend with the name of the external network your proxy also uses
We recommend SWAG reverse proxy for its simplicity: swag.
Security Headers: parameters like X-Frame-Options, Content-Security-Policy, and X-Content-Type-Options are handled by proxy.conf and resolver.conf, or other files within swag.
Sample proxy configuration:
server {
listen 443 ssl;
listen 443 quic;
listen [::]:443 ssl;
listen [::]:443 quic;
server_name dms.*;
# swagger API docs
location /docs {
# enable the next two lines for http auth
auth_basic "Restricted";
auth_basic_user_file /config/nginx/.htpasswd;
include /config/nginx/proxy.conf;
include /config/nginx/resolver.conf;
## FORCES Lax mode on all cookies flowing from this upstream target: not working for some reason
# proxy_cookie_flags ~ secure httponly samesite=strict;
## Appends standard attributes onto the root path cookie string:
## add: secure, httponly, samesite=strict, samesite=lax, samesite=none
## remove: nosecure, nohttponly, nosamesite
proxy_cookie_path / "/; Secure; HttpOnly; SameSite=strict";
set $upstream_app dms-gui;
set $upstream_port 3;
set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
}
location / {
# enable the next two lines for http auth (use you own)
# auth_basic "Restricted";
# auth_basic_user_file /config/nginx/.htpasswd;
include /config/nginx/proxy.conf;
include /config/nginx/resolver.conf;
## FORCES Lax mode on all cookies flowing from this upstream target: not working for some reason
# proxy_cookie_flags ~ secure httponly samesite=strict;
## Appends standard attributes onto the root path cookie string:
## add: secure, httponly, samesite=strict, samesite=lax, samesite=none
## remove: nosecure, nohttponly, nosamesite
proxy_cookie_path / "/; Secure; HttpOnly; SameSite=strict";
set $upstream_app dms-gui;
set $upstream_port 80;
set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
}
}
You can and should add a form of authentication at the proxy level, unless you totally trust React AuthContext and its implementation (which I don't).
Untested, sample below is missing lots of variables, and I don't care since you are supposed to use compose.
docker run -d \
--name dms-gui \
--env-file ./config/dms-gui/.dms-gui.env \
-p 127.0.0.1:80:80 \
-p 127.0.0.1:3001:3001 \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v /etc/timezone:/etc/timezone:ro \
-v /etc/localtime:/etc/localtime:ro \
-v./config/dms-gui/:/app/config/ \
audioscavenger/dms-gui:latest
Note: Replace dms with the name of your docker-mailserver container.
For detailed Docker setup instructions, please refer to:
Subject to heavily change over time, please use https://dms.domain.com/docs for current list.
GET /api/status - Server status
GET /api/infos - Server environment
GET /api/settings - Get settings
GET /api/configs - Get all config names in config table
GET /api/roles - Get a login's roles
POST /api/envs - Get DMS environment
POST /api/settings - Save settings
GET /api/logins - Get login
POST /api/logins - Add login
PATCH /api/logins - Update a login
DELETE /api/logins - delete login
POST /api/loginUser - login user true/false
POST /api/logout - logout
PUT /api/doveadm - send doveadm commands
GET /api/accounts - List email accounts
POST /api/accounts - Add a new account
DELETE /api/accounts - Delete an account
PATCH /api/accounts - Update account password
GET /api/aliases - List aliases
POST /api/aliases - Add a new alias
DELETE /api/aliases - Delete an alias
GET /api/domains - Get domains detected
POST /api/getCount - Get row count from a table
POST /api/initAPI - Create DMS API files and key
POST /api/kill - Reboot dms-gui
OAS description of all API endpoints is available at:

The REST API injected into DMS is generic: all it does is listen for POST requests, verify the KEY passed in the Authorization header, execute the system command passed in the body, and return the result in json format. You can use it free of charge in any other container having python3.
It's started as a deamon by simply mounting this supervisor service inside DMS, and shall be placed in a subfolder named dms-gui under the config folder of DMS. dms-gui creates both those files when you generate the API key in Settings, and only the supervisor conf shall be mounted in DMS compose:
./config/dms-gui/rest-api.conf:/etc/supervisor/conf.d/rest-api.conf:ro
The supervisor code is pretty generic:
[program:rest-api]
startsecs=1
stopwaitsecs=0
autostart=true
autorestart=true
stdout_logfile=/var/log/supervisor/%(program_name)s.log
stderr_logfile=/var/log/supervisor/%(program_name)s.log
command=/usr/bin/python3 /tmp/docker-mailserver/dms-gui/rest-api.py
This REST API logs what it does in logs/supervisor/rest-api.log like any other supervisor service, and I have found that PYTHONUNBUFFERED=1 will not print the messages in the docker log when run as a daemon.
Cannot be simpler then that, and super secure since the script also controls the maximum size of the payload received in the POST request. The API key is added manually as an environment variable in DMS compose.
Formatted logging with colors, that actually helps!

Absolutely unnecessary, but this project uses Prettier for consistent code formatting. Configuration is defined in the root .prettierrc.json file.
Formatting was automatically applied to staged files before each commit using Husky and lint-staged. This ensured that all committed code adheres to the defined style guide. I gave up using this as VScode does a fantastic job.
AGPL-3.0-only
Content type
Image
Digest
sha256:82aa81bf6…
Size
71.1 MB
Last updated
20 days ago
docker pull audioscavenger/dms-gui:v1.6.5