Skip to main content

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' zerov4 security middleware — bot/brute-force protection, session hijacking detection, and request filtering are meant to be handled by zerov4. Don't run brsxmail bare, in production, exposed to the internet, with sensitive data; use it behind zerov4 as 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.json doesn'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.html file 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.html there — 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 zerov4 in front of the brsxmail FastAPI app and let it handle bot/brute-force protection, session hijacking detection, and suspicious request filtering.
  • brsxmail focuses 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_config is called internally by mail.run()), and config is saved to disk.
  • The server is now started by zerov4, not uvicorn directly; brute-force/bot/session-hijacking protection comes from the zerov4 layer.
  • brsxmail's own /register, /login, /send, etc. endpoints keep working the same way, just now sitting behind the zerov4 filter.

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)

Source distribution for brsxmail 0.1.0
File Size Uploaded
brsxmail-0.1.0.tar.gz 17.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for brsxmail 0.1.0
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page