TeaNode · Documentation · Source

Local development

How to get a working TeaNode build and a database to point it at. For what the pieces are and how they fit together, see AGENTS.md in the repository root. For the conventions your change has to follow, see docs/coding/coding-standards.md, and for the dashboard docs/coding/frontend-design.md.

Prerequisites

Build and test

make build          # build/teanode-server and build/teanode, the client
make web            # build the dashboard into internal/frontend/static
make                # format, build, test
make test           # tests; starts a PostgreSQL container automatically
make lint           # golangci-lint plus the local naming checks
make lint-ci        # only what CI runs

To run one package's tests:

go test -mod=vendor -v ./internal/util/dkim -run TestVerify

Dependencies are vendored. After changing go.mod, run go mod tidy && go mod vendor and commit the vendor/ change with it.

The short way

make dev

That starts PostgreSQL and MinIO, writes dev/.env if it is missing, sets the database up from it, generates a self-signed certificate, and runs the server. make dev-frontend runs the dashboard's own dev server beside it.

Everything below is the same thing by hand.

PostgreSQL

make dev-up starts one, and the tests start their own. A server you run entirely by hand needs one:

docker run --restart always --name teanode-postgres \
  --env POSTGRES_DB=teanode \
  --env POSTGRES_USER=teanode \
  --env POSTGRES_PASSWORD=teanode \
  --publish 127.0.0.1:5432:5432 \
  -d postgres

A shell on it:

docker exec -it teanode-postgres psql -U teanode teanode

To wipe the schema and let the server rebuild it from migrations:

docker exec -it teanode-postgres psql -U teanode teanode \
  -c 'DROP SCHEMA public CASCADE; CREATE SCHEMA public;'

Running a development server

Configuration lives in the database, and the environment says where that is. scripts/dev-config.bash writes a dev/.env that is safe on a laptop:

Those describe the server only on the first run against an empty database. After that the database holds the answer, and the server warns about any of them that disagree. To start over, make dev-clean.

set -a; . ./dev/.env; set +a
./build/teanode-server config init
./build/teanode-server tls self-signed
./build/teanode-server run --log-level DEBUG

With that environment in your shell, the client reaches that server over loopback with nothing else set up: teanode domain list, teanode dkim show example.com, teanode user list. From another shell, sign in instead with teanode auth login --url http://127.0.0.1:10081.

Send it a message with swaks:

swaks --to hello@example.com --from someone@webmail.example --server 127.0.0.1:10025

The sender has to be at a real domain that publishes mail servers and does not ask for failures to be refused: every reserved example domain now publishes a DMARC policy of reject, so a message "from" someone@example.net is turned away at the door, which is correct and unhelpful, and webmail.example above stands for a domain of your choosing. One with a policy of none — most large webmail providers — is scored and accepted. The server also refuses a sender whose domain has no MX record at all.

To receive into a mailbox rather than forward, point an alias of kind "mailbox" at yours on the domain's Aliases tab, and send to that address. A second local account's mailbox is the easiest way to see a message land in both places: send from one to the other with New message.

Optional services

Both are off by default. Turn them on in the dashboard, or in an exported configuration loaded back with teanode-server config import, only when you are working on that code path.

SpamAssassin

docker run --restart always --name spamassassin \
  --publish 127.0.0.1:783:783 \
  --env 'UPDATE_PERIOD=*/15 * * * *' \
  -d tiredofit/spamassassin

Score a message by hand, which is what internal/util/spamc does:

(echo -en 'SYMBOLS SPAMC/1.5\r\n\r\n'; cat message.eml) | nc -q0 localhost 783

SPAMD/1.1 0 EX_OK
Content-length: 154
Spam: False ; -0.2 / 5.0

DKIM_SIGNED,DKIM_VALID,FREEMAIL_FROM,HTML_MESSAGE,SPF_PASS,URIBL_BLOCKED

Set antispam.enabled: true and a domain's spamFilterScoreThreshold to use it. Mail scoring at or above the threshold is rejected.

ClamAV

docker run --restart always --name clamav \
  --publish 127.0.0.1:3310:3310 \
  --volume /var/lib/clamav \
  -d clamav/clamav

Check it is alive, and feed it the EICAR test string, which every scanner reports as a virus without being one:

echo 'nSTATS' | nc -q0 localhost 3310

(echo -en 'nINSTREAM\n\0\0\0\x44'; \
 echo -n 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*'; \
 echo -en '\0\0\0\0') | nc -q0 localhost 3310

Set antivirus.enabled: true to use it.

Binding the real mail ports without root

In production the compose file grants CAP_NET_BIND_SERVICE, so the server binds 25, 80, 443 and 587 as uid 65532 rather than as root. If you would rather run on high ports and redirect, this also works:

iptables  -t nat -A PREROUTING -i eth0 -p tcp --dport 25  -j REDIRECT --to-port 10025
iptables  -t nat -A PREROUTING -i eth0 -p tcp --dport 587 -j REDIRECT --to-port 10587
ip6tables -t nat -A PREROUTING -i eth0 -p tcp --dport 25  -j REDIRECT --to-port 10025
ip6tables -t nat -A PREROUTING -i eth0 -p tcp --dport 587 -j REDIRECT --to-port 10587

Documentation

Getting started

Running it

Security

Contributing

Every document is also plain markdown, listed in /llms.txt.