Files
internal-mail-manager/README.md
T
2026-03-30 14:42:49 +00:00

224 lines
6.5 KiB
Markdown

# Docker Mailserver User Management Panel
A small full-stack admin tool for managing Docker Mailserver accounts with Bun, ElysiaJS, and React.
## 1. Proposed stack
### Backend
- Bun
- ElysiaJS
- Bun SQLite
- Docker CLI through fixed argument arrays
- Zod for small, explicit validation
Why this backend:
- Bun keeps the runtime, package manager, and SQLite support simple.
- ElysiaJS is a good fit for a small internal API without extra framework weight.
- SQLite is enough for admin sessions and a small amount of panel metadata, and keeps deployment straightforward.
- Docker CLI integration stays close to Docker Mailserver's real operational model instead of hiding it behind unnecessary layers.
### Frontend
- React
- Vite
- Plain CSS
Why React:
- It is easy for most full-stack teams to read and maintain.
- This panel only needs basic local state and forms, so React works well without routers or global state libraries.
- The UI stays minimal while still being easy to grow later.
## 2. Folder structure
```text
apps/
api/
src/
auth/
docker/
lib/
mail/
users/
web/
src/
api/
components/
pages/
styles/
packages/
shared/
data/
app.db
infra/
nginx/
```
## 3. Backend implementation
The backend is intentionally flat:
- `apps/api/src/index.ts`
Defines the HTTP routes and keeps request flow obvious.
- `apps/api/src/auth/service.ts`
Handles admin login, bootstrap admin seeding, session cookies, and session lookup.
- `apps/api/src/docker/`
Wraps Docker CLI calls with fixed argument arrays only. No shell interpolation.
- `apps/api/src/users/service.ts`
Contains the mailbox user operations, pending mailbox invites, and the one-time link flow for password changes.
- `apps/api/src/lib/`
Holds config parsing, SQLite access, validation, HTTP helpers, and small security utilities.
### API design
- `GET /api/auth/session`
- `POST /api/auth/login`
- `POST /api/auth/logout`
- `GET /api/users`
- `POST /api/users`
- `DELETE /api/users/:email`
- `POST /api/users/:email/password-reset`
- `POST /api/password-resets/inspect`
- `POST /api/password-resets/consume`
## 4. Frontend implementation
The frontend is a small React app with three views:
- Login page
- Dashboard page
- Public reset-password page
The dashboard includes:
- mailbox listing
- simple search
- create user modal
- 48-hour setup link generation for new mailboxes
- temporary link generation for password changes on existing mailboxes
- delete action
- no direct password change from the admin panel
The create flow now generates a 48-hour setup link. The mailbox does not exist yet at that point. It is created only when the user opens the link and chooses a password.
## 5. Password reset flow
The reset flow is built to stay secure without becoming heavy:
1. For a new mailbox, the admin generates a 48-hour setup link.
2. The mailbox is not created yet.
3. The backend stores only the SHA-256 hash of the invite token.
4. The admin shares the one-time setup link manually with the user.
5. When the user chooses a password through the link, the backend creates the mailbox in Docker Mailserver.
6. For existing mailboxes, the same public page is reused for password reset links.
7. Every link is one-time and expires automatically.
## 6. Docker CLI integration strategy
The app talks to Docker Mailserver through `docker exec` with explicit argument arrays:
- `setup email list`
- `setup email add <email> <password>`
- `setup email update <email> <password>`
- `setup email del <email>`
- `sendmail -t -i`
Security choices here:
- No `sh -c`
- No user input inserted into shell strings
- Input is validated before command execution
- Docker commands have a timeout
- Errors are returned as API failures instead of leaking raw implementation detail to the browser
## 7. Security notes
- Admin authentication uses an Argon2id-hashed bootstrap account from environment variables.
- Panel login uses a single access key instead of an email/password pair.
- Access key login is protected by an intentionally aggressive in-memory rate limit with escalating lockouts.
- Sessions are server-side, opaque, and stored in an `HttpOnly` cookie with `SameSite=Strict`.
- Password reset tokens are random, one-time, time-limited, and stored only as hashes.
- New mailbox setup links are also one-time, time-limited, and stored only as hashes.
- The admin panel never changes mailbox passwords directly; it only generates one-time links.
- Admin-only routes require a valid session cookie.
- Validation is applied to logins, emails, passwords, and reset payloads.
- Allowed origins are restricted by `APP_ORIGINS`.
- Setup emails for new accounts are sent from the backend only. No secrets are exposed to the frontend.
- Docker Mailserver access is treated as privileged infrastructure access.
## 8. Run locally
### Prerequisites
- Bun installed
- Docker available locally
- A running Docker Mailserver container
- The API process must be allowed to access Docker
### Setup
1. Copy `.env.example` to `.env`.
2. Update at least:
- `HOSTED_DOMAINS`
- `MAILSERVER_CONTAINER`
- `APP_ADMIN_EMAIL`
- `APP_ADMIN_ACCESS_KEY`
- `MAILBOX_INVITE_TTL_HOURS=48`
3. Install dependencies:
```bash
bun install
```
4. Start the API:
```bash
bun run dev:api
```
5. In another terminal, start the frontend:
```bash
bun run dev:web
```
6. Open `http://localhost:5173`.
### Production-style local run
If you want the built frontend to be served by the API itself:
```bash
bun run build
bun run dev:api
```
Then open `http://localhost:3001`.
The API serves files from `apps/web/dist` by default, configurable with `WEB_DIST_PATH`.
### Docker permission note
If you see:
```text
permission denied while trying to connect to the docker API at unix:///var/run/docker.sock
```
the app itself is fine, but the API process cannot talk to Docker.
Common fixes:
- Run the API with a user that belongs to the `docker` group
- If the API runs in a container, mount `/var/run/docker.sock:/var/run/docker.sock`
- Make sure the Docker daemon is actually running
- Make sure `MAILSERVER_CONTAINER` matches the real Docker Mailserver container name
## Notes and assumptions
- This panel assumes Docker Mailserver is using the file-based account provisioner and supports the standard `setup email` commands.
- `APP_ADMIN_EMAIL` is now an internal admin identity for sessions and audit trails. The actual panel login uses `APP_ADMIN_ACCESS_KEY`.
- The app stores only panel-specific metadata in SQLite. Mailbox accounts themselves remain managed by Docker Mailserver.