Internals

Developer Guide

Code map, build, tests, change paths, vendor fork.

Documentation index · UML and runtime

Prerequisites and build model

The Go version is declared in go.mod, currently 1.27.1. The server builds against Ceph Tentacle/librados with the build tag tentacle. The Dockerfile provides the matching Linux environment, runs go vet and produces the server and the load generator. The runtime runs as UID/GID 167:167; the external programs stay root-owned.

vendor is the reproducible dependency set of the container build. The go-sieve fork additionally lives in third_party/go-sieve; go.mod carries a local replace for it. Changes to the fork must be deliberately carried over into the vendor set. Changing only one of the two copies can make tests and the shipped image use different code.

Code map

Area Entry point Responsibility
Startup/configuration main.go, config.go Wiring, defaults, validation, lifecycle
Listener listen.go TCP, trust-bound PROXY, TLS
IMAP conn.go, wire.go Parser, states, command dispatch, response encoding
Mailbox synchronization selected.go, notify.go MODSEQ, snapshots, IDLE/NOTIFY
POP3 pop3.go Maildrop, lease, deletion markers
LMTP lmtpd.go Recipient resolution, buffering, per-recipient status
Delivery delivery Sieve actions, storing, rollback, redirect/vacation/notify; shared by LMTP and LDA
Admin API admin, contract, shardpostctl Listener, roles, audit, local delivery
Identity auth.go, SASL Request/result contract, Nauthilus, master user, OIDC
Store contract model.go Persistence-independent types, user, watch, lease
Persistence cephstore, user.go RADOS, account view, CAS, index operations
Test backend memstore.go Protocol tests without librados
Search fts, postings Tokenization and compact posting lists
Sieve sieve.go, ManageSieve Evaluation and script management
IMAPSieve engine.go, learning.go Mail events and durable learning jobs
External programs runner.go, learn.py Allowlist, resource limits, replaceable classifier
Operations logging, metrics, telemetry JSON logs, Prometheus, OTLP
Resources membudget, rookcreds Message budget and Ceph key rotation

Quick tests without Ceph

From the project directory:

go test -race ./internal/config ./internal/logging ./internal/auth ./internal/imapproto ./internal/pop3proto ./internal/sieve ./internal/imapsieve ./internal/extprograms ./internal/fts ./internal/postings

Some of these tests open local sockets and start test programs. An environment that blocks loopback or process execution is unsuitable for them. That has to be distinguished from an actual protocol bug.

make test runs the cgo-free race suite. The former writing cluster test is explicitly named make test-static-cluster and requires an isolated static test profile.

A single regression test:

go test -race ./internal/imapproto -run TestSessionLoggingAndSecretSafety -count=1

newTestServer in memstore_test.go creates a real TCP listener in front of the in-memory store. Additions should check observable protocol behavior, not just internal implementation details.

Full Linux build and tests

docker build --platform linux/amd64 --target build -t shardpost-build:dev .
docker run --rm \
  -v "$PWD/third_party:/src/third_party:ro" \
  shardpost-build:dev go test -tags tentacle ./...
docker build --platform linux/amd64 -t shardpost:dev .
docker run --rm -e LOG_LEVEL=info shardpost:dev config check

The fixture mount is required: one server regression test loads Pigeonhole test files from third_party, and the production Dockerfile does not copy that test data into its build stage. The Go packages are built from vendor. go test is not a production Ceph end-to-end test here.

When changing concurrency, additionally run the complete Linux suite with go test -p 1 -race -tags tentacle ./.... Record the scope and platform of the checks actually run in a release report.

An isolated real server start requires Ceph pools, cephx access, TLS and an authentication profile. Do not copy production secrets into shell history, example configuration or logs. The existing Kubernetes profiles are specific to that infrastructure; they are not a local one-command development system.

Typical changes

New IMAP command: add the parser/dispatch path in conn.go, the matching domain module, state and permission checks, response semantics, resource release and tests against the TCP test server. Only advertise a capability once its semantics hold up. Add the known command name for metrics and debug logs; never turn freely entered arguments into labels.

New store operation: change the contract in model.User, the in-memory implementation and the Ceph implementation together. Explain CAS, lease and crash behavior in the storage chapter. When several objects are involved, plan for a recovery strategy and tests at the abort boundaries.

New configuration option: add a typed field with mapstructure, env for suitable scalar values, a default and validation. Test YAML, environment precedence, invalid values and a secret-free error message. Prefer loading new secrets through file references. Keep file examples and the operational profile separate.

New Sieve extension: first read the fork contract and its AGENTS.md. Mind the loader and the runtime, the capability/require check, the action semantics and both protocol contexts. The fork has its own compiler and Pigeonhole tests. Afterwards check vendor parity and the image that was actually built.

New learning target: provide a constrained helper and configure it as a program. Input on stdin; no shell. Exit 0 means success, 75 a temporary error, other codes a permanent error. An already stored job references the program name; do not remove it without a transition.

Resources and logging

Every opened model.User view, watch, lease and memory reservation needs a matching teardown. Callback code must not keep using the RADOS IOContexts after they have been released. Do not write a response concurrently with the IMAP NOTIFY pusher; observe its cmdMu contract.

Logs use slog/JSON. For connections use logging.Session and logging.Begin; session_id, proto, remote and local establish the context. Info carries lifecycle and authentication result, debug carries known command names and durations, warn a degraded operation and error a failed operation. Never log whole authentication requests, mail bodies or script arguments. Details: log configuration.

Documentation and review

Before a change, read the affected contract and one real caller. After the change, update source links, diagrams, configuration description and regression test together. The diagram tooling keeps Mermaid sources and SVGs in sync. Release and test reports state the version, the platform, the checks actually run and the remaining gaps.

The current working directory has no Git metadata. No commit or tag evidence can therefore be derived here. In a later repository, source state, vendor state, tests and release digest belong in the same auditable approval process.

Rendered from docs/development/README.md in the ShardPost repository.