feat: implement Smol Mail server in Rust with nix flake deployment

This commit is contained in:
randogoth 2026-09-26 11:52:22 +03:00
commit 71ad7e04c0
14 changed files with 2592 additions and 0 deletions

67
README.md Normal file
View file

@ -0,0 +1,67 @@
# 分身 bunshin
A Rust implementation of the [Smol Mail](https://code.randogoth.com/randogoth/smolmail) server: a minimalist, end-to-end encrypted mail protocol over a Noise-secured TCP connection. `bunshin` implements the server side only — receiving, storing and serving sealed mail — not the client.
The server never sees plaintext, sender identities or any private key. It learns only which mailbox an envelope is for, its size, and when it arrived.
The flake's main purpose is turnkey deployment on a NixOS host: import `nixosModules.default`, point it at a key, and `nixos-rebuild switch`.
## Deploying on NixOS
Add bunshin as a flake input and import the module:
```nix
{
inputs.bunshin.url = "https://code.randogoth.com/randogoth/bunshin";
outputs = { self, nixpkgs, bunshin, ... }: {
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
modules = [
bunshin.nixosModules.default
{
services.bunshin = {
enable = true;
keyFile = "/var/lib/bunshin/server.key"; # provisioned out of band, see below
openFirewall = true;
inviteTokenFile = "/run/secrets/bunshin-invite"; # or inviteToken directly
};
}
];
};
};
}
```
`services.bunshin` also takes `host`, `port`, `dataDir`, `maxEnvelope`, `quota`, `retentionDays`, `rateConnections` and `rateSends`; see `flake.nix` for defaults. The module renders a `systemd` unit that runs `bunshin serve` under `DynamicUser`; it does not generate a key.
Before the first deploy, generate the server's static key once (from a dev shell or `nix run`) and place it at the configured `keyFile`:
```
nix run "https://code.randogoth.com/randogoth/bunshin" -- keygen --key server.key
```
`keygen` writes the server's static X25519 key (used for the Noise handshake, distinct from any user's Ed25519 identity) and prints its public key in base32. Publish that public key through a trusted channel — clients pin it, and a mismatch aborts the handshake.
## Building
Requires a Rust toolchain (stable, edition 2021) and a C compiler — `rusqlite`'s `bundled` feature compiles SQLite from source rather than linking a system copy, so no separate SQLite install is needed.
```
cargo build --release
cargo test
```
The binary lands at `target/release/bunshin`. With Nix, skip the toolchain setup entirely: `nix build` produces the same binary at `./result/bin/bunshin`.
Running directly, outside the NixOS module:
```
bunshin keygen --key server.key
bunshin serve --key server.key --db mail.db --host 0.0.0.0 --port 1961
```
`serve` accepts `--max-envelope`, `--quota`, `--retention-days`, `--invite-token`, `--rate-connections` and `--rate-sends` to control size limits, per-mailbox quota, message retention, registration gating and abuse control. Run `bunshin serve --help` for defaults.
## Status
Implements SPEC.md version 1 in full: `AUTH`, `RESOLVE`, `SEND`, `FETCH`, `DELETE` and `REGISTER` (including invite tokens and key rotation chains). Verified end-to-end against the Python reference client over a live Noise connection, plus a raw-protocol test suite covering the rejection paths (auth failures, unknown users, malformed and oversized envelopes, rate limiting).