Experience email the way you want with Mail0 – the first open source email app that puts your privacy and safety first. Join the discord: https://mail0.link/discord
Email clients are closed-source, data-hungry, or too complex to self-host. Zero is an open-source Gmail alternative that puts users in control of their own email infrastructure, with AI agents for triage and a unified inbox for multiple providers.
What This Does
Zero is a full-stack email application with a React/Next.js frontend (apps/mail/) and a Node.js server (apps/server/). The frontend handles the UI — inbox, compose, settings, and an AI chat sidebar (components/create/ai-chat.tsx). The backend manages email connections, database persistence via Drizzle ORM, and the tRPC API layer (apps/server/src/trpc/).
The repo is a fork of the popular Mail-0/Zero project (10,784 stars). It uses pnpm as the package manager, Docker for local PostgreSQL, and GitHub Actions for CI/CD. The codebase is TypeScript throughout: 186 .ts files and 180 .tsx files.
How It Is Wired
The application has two entry points: the Next.js frontend (apps/mail/app/page.tsx) and the Node.js server (apps/server/src/main.ts). The frontend communicates with the backend through tRPC, with the router defined in apps/server/src/trpc/index.ts. That router imports 18 modules and delegates to route handlers in apps/server/src/routes/agent/.
The most-connected modules are apps/server/src/types.ts (31 modules import it) and apps/server/src/env.ts (27 importers). These are hub modules — high churn here ripples widely. Both participate in circular dependency chains, along with apps/server/src/lib/server-utils.ts (24 importers) and apps/server/src/trpc/trpc.ts (20 importers). The import graph shows 62 modules inside circular dependencies out of 370 analyzed — roughly 17% of the codebase.
The server owns the database effects: apps/server/src/db/index.ts initializes the connection, and apps/server/src/routes/agent/db/index.ts handles agent-related persistence. The apps/server/src/pipelines.ts file is flagged as oversized at 1,689 lines.
How To Use It
git clone https://github.com/moses-y/Zero
cd Zero
pnpm install
pnpm docker:db:up
pnpm nizzy env # set up environment variables
pnpm nizzy sync # sync env vars and types
pnpm db:push # initialize the database
pnpm dev # start the app at localhost:3000
Configuration lives in .env.example — you'll need Google OAuth credentials and PostgreSQL connection details. The .devcontainer/Dockerfile provides a containerized dev environment. Node.js v18+, pnpm v10+, and Docker v20+ are required.
Real-World Use
A team wanting full control over their email infrastructure would deploy Zero on their own hardware. Users connect Gmail, Outlook, or other IMAP providers through the settings page (apps/mail/app/(routes)/settings/connections/page.tsx). AI agents in the compose flow (components/create/ai-chat.tsx) draft and summarize messages. The unified inbox aggregates all providers into one view.
Code Health & Issues
Static analysis found 150 issues (88 high, 61 medium, 1 low) across 5 categories:
High — Import cycle members (31 instances): apps/server/src/env.ts, apps/server/src/lib/server-utils.ts, apps/server/src/trpc/trpc.ts all participate in circular imports. Breaking these requires extracting shared types or inverting dependencies.
High — Hub modules (10 instances): apps/server/src/types.ts has 31 dependents; changes there have a wide blast radius.
High — Oversized files (6 instances): apps/mail/components/icons/icons.tsx (1,689 lines) and apps/server/src/pipelines.ts are hard to hold in one head.
High — Deep nesting (12 instances): command-palette-context.tsx and email-composer.tsx hit indentation depth 8.
High — Duplicated code (101 files): 1,203 repeated 6-line blocks across icon components.
CI/CD findings: GitHub Actions are not pinned to commit SHAs (a tag-move supply-chain risk), CI never runs the test suite (5 test files exist, no test command in workflows), and GITHUB_TOKEN lacks least-privilege permissions. The test-to-source ratio is 0.019 — coverage is minimal. Three GIFs over 5MB bloat the repo (largest: 18.3MB).
The Bottom Line
Zero is a functional, feature-rich email client with a solid architecture at the app level, but the server core has real maintainability debt — circular imports and hub modules make changes risky. The CI pipeline needs hardening before this is production-ready. Good for teams who want a modern, self-hosted email UI and are willing to invest in refactoring the backend.
What the analyser found
Deployment readiness
7/7
✓Container image
✓CI pipeline
✓Lockfile committed
✓Test suite
✓README
✓License
✓No committed secrets
Composition
635 files
TypeScript186
TSX180
JSON87
SQL42
YAML13
Markdown12
ReactDocker
Architecture
Top-level areas of the codebase, sized by module count. Arrows show how many imports cross from one area into another.
Ranked by severity × confidence × production reach. Reach is the honest discriminator across a collection that is mostly other people's code: the same finding matters more in something that ships.
high2
medium8
low1
Pin third-party GitHub Actions to a commit SHAhigh3 occurrences
A tag can be moved, so the action running with your token and secrets is whatever its owner last pushed; this is how tj-actions/changed-files leaked secrets from thousands of repos.
Fix: Replace each @vN with the 40-character commit SHA, keep # vN as a comment, and let Dependabot bump the SHAs.
Make CI invoke the test suite it hashigh5 occurrences
.github/workflows
5 test files, no test command in the 4 workflows read
A green check that never executed an assertion is worse than no check, because reviewers trust it.
Fix: Add a test step to the existing workflow rather than a new workflow.
Declare least-privilege permissions for GITHUB_TOKENmedium
.github/workflows/ci.yml
1 workflow(s) declare no permissions
With no declaration the token inherits the repository default, so any injected step can push commits or mint releases from inside your own CI.
Fix: Add permissions: contents: read at the top of the workflow and widen per job only where needed.
Enable Dependabot or Renovatemedium
8 manifest(s), no update bot configured
Without a bot a published advisory sits unpatched until someone audits by hand, which across 1,322 repositories means never.
Fix: Commit .github/dependabot.yml covering the repo ecosystems plus github-actions.
Pin the container base image by digestmedium
.devcontainer/Dockerfile
ubuntu:latest (mutable tag)
An untagged or mutable base means today's build and last month's contain different libc and a different CVE set, with no record of which shipped.
Fix: Use image:tag@sha256:<digest> and enable Dependabot's docker ecosystem.
Gate pull requests on a dependency vulnerability scanmedium
.github/workflows
no dependency scan in CI
This is the one gate that would catch a known-vulnerable package before it reaches a build, and no repository in the sample had it.
Fix: Add dependency-review-action on pull_request, or osv-scanner on push and a schedule.
Move large binaries to Git LFS or out of the repositorymedium5 occurrences
apps/mail/public/onboarding/step2.gif
5 blobs over 5MB: step2.gif 18.3MB, step1.gif 18.2MB, step3.gif 9.9MB
One repository here carries twenty blobs over 5MB including a 10.9MB spreadsheet, so every clone and every CI checkout pays for data nobody diffs.
Fix: Track those extensions with LFS, or move datasets to object storage and fetch them in a setup step.
Set persist-credentials: false on checkoutmedium
.github/workflows/ci.yml
checkout keeps the token, then dependencies are installed
The token stays in .git/config for every later step, so a malicious postinstall script reads a pushable credential without one ever being passed to it.
Fix: Add with: persist-credentials: false, and pass an explicit token only to the step that pushes.
Review the install lifecycle script and disable scripts in CImedium
package.json
postinstall
Install scripts are the execution vector for every npm compromise from event-stream onward, and a postinstall that fetches a binary makes the build depend on a URL nobody reviews.
Fix: Move the work into an explicit build step, or set ignore-scripts in CI and run it by name.
Expand the test suite; it cannot cover this codebasemedium7 occurrences
7 test files against 368 source files (ratio 0.019)
A reviewer reads "has tests" and assumes a safety net that covers two files out of eighty-five, which is how untested paths ship behind a green badge.
Fix: Add tests for the highest fan-in modules first.
Set timeout-minutes on the workflow jobslow2 occurrences
.github/workflows/close-conflicted-prs.yml
2 workflow(s) declare no job timeout
A wedged step runs to the six-hour platform default, which on a two-hourly schedule means three runs overlap behind it.
Fix: Add timeout-minutes with a realistic bound to each job.
Checked deterministically against the repository tree and a bounded set of its files: committed credentials, unpinned actions and base images, missing lockfiles and update bots, workflows that discard failures, published advisories against the declared dependencies, runtime configuration, licensing and notebook reproducibility. No language model is involved in this section.