fumi/README.md
2026-09-28 16:37:22 +03:00

4.5 KiB
Raw Blame History

fumi

A Rust client for Smol Mail: a minimalist, end-to-end encrypted mail protocol over a Noise-secured TCP connection. fumi derives an identity from one 32-byte master secret, seals and opens mail, and talks to a mailbox; it is the counterpart to bunshin, the server.

Status

Phase 1, the TCP carrier (SPEC.md 1.1), is implemented and interoperates in both directions with the reference client smolmail.py and the reference server smolmaild.py, and with bunshin. The Reticulum carrier of RNS.md (1.2) is not built yet: smol+rns:// addresses parse and fail with a message naming the rns feature, and the flake does not yet vendor microReticulum.

Addressing

alice@example.org[:1961]                                        short form, resolved via the server
smol://alice@example.org/mfrggzdfzt...                          self-certifying, 52-char base32 identity
smol+rns://alice@8f2c1d0a7b6e5f4c3d2a1b0e9f8c7d6a               short form over Reticulum (rns feature)
smol+rns://alice@8f2c1d0a7b6e5f4c3d2a1b0e9f8c7d6a/mfrggzdfzt... self-certifying over Reticulum

Usage

One master per store, selected by the global --key and --db flags; a second identity is a second pair of files.

fumi [--key identity.key] [--db fumi.db] [--timeout 30] <command>

# identity
fumi keygen [--force]                      create a master secret
fumi whoami                                address, public key, fingerprint, rotation index
fumi restore <address>                     recover local state from the master alone
fumi rotate                                advance the rotation index, sign and push the certificate

# servers and contacts
fumi trust <host> <key-b32> [--force]      pin a server's Noise static key
fumi register <address> [--invite TOKEN]   bind this identity to a username
fumi resolve <address>                     look up a contact's key, walk the chain, pin it
fumi import <smol-uri>                     add a contact from a self-certifying address
fumi contacts                              known keys and how each was learned

# accept tokens
fumi accept <address>                     issue a token, admit to the main tier, sync the set
fumi block <address>                       withdraw it, sync the set

# mail
fumi send <address> [--subject S] [--body TEXT | --file F | -] [--header K:V] [--reply-to ID] [--anonymous] [--no-pad]
fumi fetch [--keep] [--reset]             retrieve, verify, store, acknowledge
fumi delete <id>...                       remove from the server explicitly
fumi list [--sent] [--requests]
fumi read <id> [--sent]

Mail is stored sealed and opened on demand; there is no plaintext at rest. A first contact is learned by trust on first use and marked unverified when the server's key was not pinned; a key change is accepted only when a signed rotation chain leads from the key held to the one offered, and is surfaced rather than applied silently.

Build

nix build              # or: nix develop -c cargo build
nix develop -c cargo test

bunshin and the reference clients make a complete test rig: register against a local server, exchange mail in both directions, then rotate and fetch the mail that the superseded key still receives.

Modules

File Contents
src/crypto.rs base32, HKDF, HMAC, SHA-256, the Ed25519→X25519 map with SPEC.md §2's checks
src/address.rs parsing for all four address forms, username validation, fingerprints
src/account.rs master, seed_n derivation, accept-key and tokens, rotation certificates
src/message.rs envelope seal/open (§5.1–§5.3), message id (§5.4), frontmatter (§5.5)
src/transport.rs Transport trait, bind values, operation and status constants
src/tcp.rs Noise_NX initiator, len u32 || op u8 || body framing, static-key pinning
src/client.rs the six operations, AUTH, chain walking, token sync, fetch pipeline
src/store.rs SQLite: state, contacts, accepted, tokens, seen ids, inbox, sent

Unit tests pin every derivation against vectors generated from smolmail.py, including a full envelope reproduced byte for byte.

References