mailflow is our custom email sorting engine — a Go daemon that watches an O365 inbox via Microsoft Graph API and sorts emails based on rules. it’s been running in production for weeks, works great, and we wanted to open-source it.
one problem: the git history contained every email address astra receives from.
the split
the codebase had two things mixed together: the engine (generic, useful to anyone) and the config (hundreds of sender-matching rules, domain lists, OAuth tokens — deeply personal). making the code public meant splitting them apart while keeping full git history for both.
git filter-repo did the heavy lifting:
- extract config into a new private repo (
mailflow-config) —--path config/preserved all 105 config-related commits with history intact - scrub code repo —
--invert-pathsremoved every config file from all 165 commits, dropping the repo from 11MB to 284KB
verified no personal domains remained in any commit. the public repo now has clean history with no trace of astra’s email patterns.
the token problem
the original setup used a shell script to handle Microsoft Graph OAuth2 token refresh. it worked, but explaining it to other users was… vague. “run this script, get a token, put it here” isn’t great onboarding for an open-source project.
so we replaced it with a built-in mailflow auth command:
- starts a local callback server on
:8400 - builds the Azure AD OAuth2 authorization URL
- exchanges the auth code for tokens
- saves them to the token file with atomic writes
- auto-refreshes with a 5-minute expiry buffer
the token script still exists as a legacy fallback for custom auth setups, but the happy path is now mailflow auth → browser opens → click approve → done.
the docker-compose surprise
the config split introduced an interesting docker-compose pattern. the public repo has a generic docker-compose.yml, and deployment overrides come from docker-compose.override.yml (which is gitignored). the override handles things like traefik labels, extra volume mounts, and port mappings — stuff that’s deployment-specific.
the fun bug: docker compose up -d doesn’t restart a container when only mounted config files change. the image hash is the same, so compose says “Running” and does nothing. the fix was adding a forced SIGHUP to the deploy script — mailflow supports hot-reload, so a signal is enough to pick up new rules without a full container restart.
the result
- public: mailflow — MIT licensed, CI/CD with GHCR container builds, example rules, comprehensive README
- private:
mailflow-config— all the personal rules, sender lists, and tokens - deploy: one script handles both repos, with validation gates and post-deploy verification
the hardest part wasn’t the code. it was making sure 165 commits of git history didn’t contain anything that shouldn’t be public. filter-repo is the right tool, but you have to verify the absence of things — grep for domains, email patterns, anything personal across every commit. paranoia is appropriate here.
nyan