9.2 KiB
Hanzo Mail
Fork of Mox. Mail server (SMTP, IMAP4, webmail, DKIM/SPF/DMARC, autoconfig, ACME). This is the foundation for replacing Google Workspace email at Hanzo.
Lineage
| Upstream | github.com/mjl-/mox |
| Upstream author | Mechiel Lukkien [email protected] |
| Upstream license | MIT |
| Fork point | 9bbad6af30ae1c429dde9cdc2d04130532e1fed1 (v0.0.15 + 55 commits, 2026-07-21) |
| This repo | github.com/hanzoai/mail |
| Module path | github.com/hanzoai/mail |
This is a real GitHub fork (isFork: true, parent mjl-/mox), matching how
hanzoai/git (← go-gitea/gitea) and hanzoai/deploy (← argoproj/argo-cd) were
done. Provenance is therefore visible in the GitHub UI as well as in NOTICE.
Full upstream history is preserved; git remote upstream points at mjl-/mox.
Licensing — what applies to what
Verified by reading the tree, not assumed. NOTICE is the authoritative
statement; mail licenses prints it from the running binary.
- Mox source + Hanzo's changes — MIT (
LICENSE.MIT), © 2021 Mechiel Lukkien, plus Hanzo AI, Inc. for modifications. Not relicensed. publicsuffix/public_suffix_list.txt— MPL-2.0 (LICENSE.MPLv2.0), Mozilla's Public Suffix List. This one data file is the only thing the MPL covers. Mox is not dual-licensed. Proof:mox-/licenses.golabelsLICENSE.MPLv2.0ashttps://publicsuffix.org - Public Suffix List Mozilla, separate from mox's own MIT, andREADME.mdsays the same. The tracked copy is not the published document byte-for-byte: thego:generateline keeps upstream's licence and version header and the ICANN section's rules, and drops the per-registry commentary and the private section, neither of whichParseListreads. Both forms parse to the sameList, so this is a smaller artifact, not a different one.vendor/**— third-party deps under their own licenses. Copies collected verbatim inlicenses/(42 files) bygenlicenses.sh, which readsvendor/. Includes BSD-3-Clause from The Go Authors (golang.org/x/...and the Go-derived forksmjl-/{adns,autocert,flate,sherpa}) and Apache-2.0 (Prometheus, protobuf). Thelicenses/tree is dependency licenses — it is not a license grant over Mox or Hanzo Mail.
Mox was chosen over Stalwart deliberately: Stalwart is AGPL-3.0-only plus a proprietary per-file SELv2 enterprise license. AGPL's network clause would force publishing our fork and the SELv2 files cannot be forked commercially. Mox is MIT and Go, which matches our stack and our licensing posture.
What was renamed
Only the Go module path, and only where it affects the build:
go.modmodule line.- 268
.gofiles — import pathsgithub.com/mjl-/mox/...→github.com/hanzoai/mail/.... - 5 scripts that
go installthe module and would otherwise build upstream instead of this fork:genwebsite.sh,docker-release.sh,apidiff.sh,test-upgrade.sh,Dockerfile.release. licenses.go— embedsNOTICE;mox-/licenses.goprints it first and labels the MIT section as a fork of mjl-/mox, so the binary states its own lineage.
What was NOT touched — and why
LICENSE.MIT,LICENSE.MPLv2.0,licenses/— preserved verbatim. Never edit these.vendor/— no dependency was added, removed, or bumped.apidiff/v0.0.9.txt,v0.0.10.txt,v0.0.11.txt,next.txt— historical records of upstream's API at upstream releases. Rewriting the paths in them would falsify a historical record. Leave them.website/*.md,develop.txt— upstream's own website prose. (develop.txtalso referencesmjl-/mox-website-files, a different repo — do not let a carelesssedcatch that prefix.) Notewebsite/website.gois Go source and did change: the rename also repointed its "Sources at github" and "feedback?" links athanzoai/mail, which is the right outcome for a fork.- The
moxcommand name, config keys, data directory layout, and themoxGo package name inmox-/. Renaming these is a separate, larger job that breaks on-disk config and data compatibility. The binary still printsusage: mox .... Deliberate: the module rename is what makes the fork independently buildable; the rest is branding with real breakage risk. webmail/webmail.ts/webmail.js— still link tomjl-/mox/issuesfor bug reports.webmail.jsis generated from the.ts, so this needs the frontend toolchain, not ased. Blocking before any deploy — otherwise our webmail sends Hanzo users to file bugs on upstream's tracker.
Remaining references to upstream — all intentional
grep -rc 'mjl-/mox' --exclude-dir={.git,vendor,licenses} . should return only
these. Anything else is an accident and should be reviewed:
| File | Count | Why |
|---|---|---|
apidiff/v0.0.9.txt, v0.0.10.txt, v0.0.11.txt, next.txt |
63 | Historical records of upstream's API. Falsifying them would be dishonest. |
website/index.md, install/index.md, features/index.md |
22 | Upstream website prose, not rebranded. |
README.md, LLM.md, NOTICE |
16 | Deliberate attribution. |
webmail/webmail.ts, webmail.js |
5 | Upstream issue-tracker links — the pre-deploy blocker above. |
mox-/licenses.go |
1 | The "fork of github.com/mjl-/mox" label in mail licenses. |
develop.txt |
1 | Points at mjl-/mox-website-files, a different repo. |
Verify the fork's exact delta at any time with git diff fork-point..main. The
fork-point tag is an annotated tag on upstream commit
9bbad6af30ae1c429dde9cdc2d04130532e1fed1. That diff must never show changes to
LICENSE.MIT, LICENSE.MPLv2.0, licenses/, or vendor/ — currently it shows
none.
Build and test
export PATH=$PATH:/usr/local/go/bin
export GOWORK=off GOFLAGS=-mod=vendor # a parent go.work in ~/work/hanzo shadows this repo
go build ./...
go test ./...
GOWORK=off is required — without it go build ./... fails with "directory
prefix . does not contain modules listed in go.work".
Integration tests are behind //go:build integration and need Docker; the
default go test ./... excludes them.
Test flakiness is pre-existing upstream, not caused by the fork. go test ./... runs up to 32 package binaries at once on this box; mox's server tests
use hard 1s deadlines (server_test.go:414 "server not done within 1s") and
starve. The failing set differs every run — baseline upstream (before any change)
failed TestQresyncUIDOnly/TestSubscribe; the renamed tree failed a different
set. Both pass in isolation. Use go test -p 1 ./... for a clean signal.
Standing it up — next steps
Nothing is deployed. No DNS points anywhere. Repo setup only.
- Mailbox storage. Per-account BoltDB index plus message files on disk:
<DataDir>/accounts/<name>/index.dband<DataDir>/accounts/<name>/msg/<shard>/<id>. This is a stateful, single-writer design — it is not a stateless service and does not shard across replicas. It needs a real PersistentVolume with backups (mail backup <destdir>andmail verifydata <data-dir>are built in), not an emptyDir. Decide the storage class and backup target before anything else; this is the main architectural constraint of the whole project. - Config and keys.
mail quickstart user@domaingeneratesmox.conf,domains.conf, and DKIM keys. DKIM private keys and the admin/account password hashes are secrets — they go in KMS (kms.hanzo.ai) and reach the cluster via KMSSecret CRDs. Never commit them. Passwords are hashed by mox (bcrypt/SCRAM credentials); do not add any plaintext path. - DKIM / SPF / DMARC / MX / DNS.
mail config dnsrecords <domain>prints every record to publish;mail config dnscheck <domain>verifies them. Publish in Cloudflare. MX records must not be proxied. Sequence matters: stand the server up and verify withdnscheckbefore moving MX off Google, or mail bounces. - Migration from Google. There is no built-in IMAP puller — verified, not
assumed. Two real options: (a) Google Takeout → mbox →
mail import mbox <account> <mailbox> <file>(also available as mbox/maildir.zip/.tgzupload in the web account UI); or (b) an external IMAP-to-IMAP sync tool run against Hanzo Mail's IMAP server. Pick one and pilot it on a single throwaway account before touching anyone's real mail. - CI/CD and image. Images build in CI only — never locally — and publish to
ghcr.io/hanzoai/mail. Notedocker-release.sh/Dockerfile.releasestill carry upstream's release flow and registry; repoint them (or replace with a.hanzo/workflowspipeline) as part of the deploy work, not before. - IAM. Mox has its own account/password store. Integrating Hanzo IAM (hanzo.id, OIDC) is a genuine design question, because IMAP and SMTP clients authenticate with SASL, not OIDC — expect app passwords or a SASL-to-IAM bridge. Design it before promising SSO.
Syncing upstream
git fetch upstream && git merge upstream/main. The module rename touches nearly
every .go file, so import-path conflicts are expected on merge; resolve by
keeping our path. Do not resolve a conflict by reverting go.mod.