brsxmail
Closed-circuit / internal messaging system. FastAPI based, backend-ready. The user can provide their own HTML interface or use the built-in (simple, form-based) one.
⚠️ Security note: This package does not provide production-grade security on its own. Passwords are hashed with plain
sha256(no salt), sessions are kept in RAM (lost on restart, not shared across workers), and there's no rate limiting/CORS/brute-force protection. This package is designed to be used together with BRSX-Labs'zerov4security middleware — bot/brute-force protection, session hijacking detection, and request filtering are meant to be handled byzerov4. Don't runbrsxmailbare, in production, exposed to the internet, with sensitive data; use it behindzerov4as a layer, or on closed-circuit/internal networks.
Installation
pip install brsxmail
or clone this repo and:
pip install -e .
Usage
Quick start
python run.py
- On first run, if
brsxmail.config.jsondoesn't exist, an interactive setup wizard opens in the terminal: it asks for domain, port, host, storage type (json/sqlite), and data folder. You never have to write a config by hand. - On subsequent runs, since config already exists, the wizard isn't asked again — the server starts directly.
- To reset settings:
python run.py --reconfigure
Via CLI after pip install
brsxmail
brsxmail --reconfigure
The simplest usage
from brsxmail import mail
mail.run()
From code (advanced)
from brsxmail import create_app, get_or_create_config
import uvicorn
config = get_or_create_config()
app = create_app(config)
uvicorn.run(app, host=config["host"], port=config["port"])
Using your own HTML
The setup wizard asks you directly: "Which interface do you want to
use?" — the built-in default, or your own index.html. Your answer
is saved into brsxmail.config.json as use_custom_html: true/false;
the server doesn't silently decide on every startup, it follows this
setting.
- If you chose "I'll use my own index.html": you need to place an
index.htmlfile in the folder where you run the server. The server looks for it there on every request. If the file isn't there, it prints a clear warning to the terminal and falls back to the default interface (not silently). - If you chose "use the default": it won't look at the working
directory at all, even if there's an
index.htmlthere — it always uses the interface bundled with the package.
If you change your mind, run python run.py --reconfigure to re-run
the wizard and update your choice.
The server reads this file and fills in these placeholders for you:
{{LOGGED_IN}}→"true"/"false"{{USER}}→ the logged-in user's email (empty if not logged in){{DOMAIN}}→ the domain from config (e.g.@brsx.com)
Writing your own interface: the API contract
When writing your own index.html, the JS side needs to follow these
rules. For every endpoint, the request type (form-data or JSON), the
expected fields, and the shape of the response are documented below.
If you don't use these names and types exactly, the backend won't
recognize your requests.
General rule: All POST endpoints expect form-data, not a JSON
body. All responses come back as JSON. After login, the session is
kept via a cookie (session_id) — using credentials: "same-origin"
(or the browser default) in your fetch calls is enough; you don't
need to carry the cookie manually.
POST /register — Register
Request (form-data):
const form = new FormData();
form.append("email", "ali@your-domain.com");
form.append("password", "1234");
await fetch("/register", { method: "POST", body: form });
Response:
{ "ok": true }
or on error (400):
{ "error": "User already exists" }
POST /login — Login
Request (form-data), same fields as register: email, password.
Response (on success, the browser automatically gets the session_id
cookie):
{ "ok": true }
On error (401):
{ "error": "Invalid login" }
POST /logout — Logout
Request: no body needed, just a POST request.
Response:
{ "ok": true }
POST /send — Send a message
Must be logged in (cookie is sent automatically).
Request (form-data):
const form = new FormData();
form.append("receiver", "veli@your-domain.com");
form.append("content", "hello");
await fetch("/send", { method: "POST", body: form });
Response:
{ "ok": true }
Errors: 401 (not logged in) or 400 (recipient not found).
GET /inbox — Inbox
Request: no parameters, just GET.
Response — a list (array) of messages, each element shaped like:
[
{
"id": "71b57f2c-...",
"from": "ali@your-domain.com",
"to": "veli@your-domain.com",
"content": "hello",
"time": "26.07.2026 09:56",
"read": false
}
]
If not logged in, returns 401 with { "error": "..." } (not an array).
GET /message/{id} — Open a message
Put the message's id field in place of {id}: /message/71b57f2c-...
Response — a single message object (not an array), marked as
read: true:
{ "id": "...", "from": "...", "to": "...", "content": "...", "time": "...", "read": true }
404 if not found.
DELETE /message/{id} — Delete a message
await fetch(`/message/${msgId}`, { method: "DELETE" });
Response:
{ "ok": true }
404 if not found or not authorized.
GET /search?q=... — Search
Pass the query as the q parameter: /search?q=hello
Response — a message list (array), same shape as /inbox.
GET /unread-count — Unread count
Response:
{ "unread": 3 }
Summary table
| Method | Path | Body type | Fields | Returns |
|---|---|---|---|---|
| GET | / |
— | — | HTML |
| POST | /register |
form-data | email, password |
{ok} / {error} |
| POST | /login |
form-data | email, password |
{ok} / {error} |
| POST | /logout |
— | — | {ok} |
| POST | /send |
form-data | receiver, content |
{ok} / {error} |
| GET | /inbox |
— | — | message list |
| GET | /message/{id} |
— | — | single message object |
| DELETE | /message/{id} |
— | — | {ok} / {error} |
| GET | /search?q=... |
— | q (query param) |
message list |
| GET | /unread-count |
— | — | {unread: N} |
For a working example, check the bundled brsxmail/webui/index.html —
it has real, working JS examples of all these calls; use it as a
reference when writing your own interface.
Using it together with zerov4
brsxmail doesn't include a security layer on its own; instead it's
designed to be run behind BRSX-Labs' zerov4 (ZeroxArx) security
middleware as a layer in front of it. In practice that means:
- You put
zerov4in front of thebrsxmailFastAPI app and let it handle bot/brute-force protection, session hijacking detection, and suspicious request filtering. brsxmailfocuses only on the messaging logic (register, login, send, inbox, search, delete); it doesn't harden itself against authentication attacks on its own.
Setup
mail.run() starts and blocks on uvicorn by default — in that case
there's never a moment for zerov4 to wrap the app. Instead, use
blocking=False to just get the ready FastAPI app, and hand the
server off to zerov4:
# main.py (or whatever your app file is named)
from brsxmail import mail
from zerov4 import arx
app = mail.run(blocking=False) # only creates the app, doesn't start the server
arx.run(app) # zerov4 wraps the app and starts the server itself
When run this way:
- The setup wizard is still asked on first run (
get_or_create_configis called internally bymail.run()), and config is saved to disk. - The server is now started by
zerov4, notuvicorndirectly; brute-force/bot/session-hijacking protection comes from thezerov4layer. brsxmail's own/register,/login,/send, etc. endpoints keep working the same way, just now sitting behind thezerov4filter.
Using it on a closed-circuit / internal network (not exposed to the
internet, trusted user base) without zerov4, on its own, is also a
reasonable option; the zerov4 recommendation specifically applies to
internet-facing or sensitive-data deployments.
Storage
Default: JSON file based (data_dir/users.json, data_dir/messages.json).
If sqlite is chosen in the setup wizard, brsxmail.db is used in the
same data folder instead. Both backends implement the same interface,
so the endpoints work without knowing which backend was chosen.
Notes
- This package is a closed-circuit / internal system; it does not use a real SMTP/email protocol and does not send mail externally.
- Domain checking defaults to
@example.com, changeable in the setup wizard. - Passwords are hashed with unsalted
sha256, sessions are kept in RAM. See the "Security note" and "Using it together with zerov4" sections above.
Release files for brsxmail 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| brsxmail-0.1.0.tar.gz | 17.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| brsxmail-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 33.7 kB
Release files / brsxmail-0.1.0.tar.gz
| Download URL | brsxmail-0.1.0.tar.gz |
|---|---|
| Size | 17.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d96cb6e187dbe0357833997d98ad3b87d510811ac2f2cbc92f48da39b19214e4
|
|
BLAKE2b-256 checksum How to use checksums |
498eb44b69784ec5d780bad40f0bb603c72e2d8902b69be0d7b69ca5f91e397f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.10.0
|
Release files / brsxmail-0.1.0-py3-none-any.whl
| Download URL | brsxmail-0.1.0-py3-none-any.whl |
|---|---|
| Size | 16.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d29a261e7024854f460e8ccce442f11c944a848f853a2347e7bf34d9192d831e
|
|
BLAKE2b-256 checksum How to use checksums |
8a7007452aac5e601e55304312d57b8c385115a53ec2572a42ccda83446f4d6d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.10.0
|