I Built a Full Mail Server in 20 Hours Using AI — Here's What Actually Happened
Summary
- The Challenge: Build a fully functional, modern mail server (Mailcow alternative) in a single day.
- The Stack: Vue 3, NestJS, Vite, Go (TUI), Docker, Postfix, Dovecot.
- The Outcome: From zero to production-ready monorepo in 20 hours.
- The Hero: A strict rule: “If E2E tests don’t pass, we don’t move forward. The feature is considered incomplete.”
- The ROI: 400 hours of traditional development condensed into 20 hours (~95% savings).
This document is not a technical report; it is the story of a human and an AI (GitHub Copilot) struggling with modern software development clichés, Docker networks, and “unnecessary complexity”.
The hidden hero of this story was a strict rule: “If the E2E tests don’t pass, the feature doesn’t exist.” This meant we never moved to the next task until the current one was fully verified.
1. The Vision: “Mailcow but Modern”
The day started at 08:00 AM. Coffee in hand, terminal open.
The goal was ambitious but clear: Build a fully functional mail server that serves as a modern alternative to Mailcow or Mailu. It had to be:
- Modern: Vue 3, NestJS, Vite.
- Simple: No unnecessary queues, no heavy Java components.
- Deployable: A single binary for setup, a single docker-compose for run.
I started by laying the foundation with Postfix and Dovecot configurations (configs/main.cf). Then, the first pivot happened. The AI proposed a massive Node.js architecture for the installer. I rejected it. Result: We pivoted to Go for a TUI (Terminal UI) based installer. By 10:00 AM, we had a smooth installation script running on Linux ARM64.
2. The Protocol: How to Talk to AI
What made this project launch so quickly wasn’t just coding speed, but the strict communication protocol we established. We didn’t just “ask” for code; we “negotiated” it.
3. The Coding Marathon: Building the Core
Between 10:00 AM and 14:00 PM, we entered the “zone”.
Admin Panel (NestJS + Vue3)
We built the backend and frontend simultaneously. The AI generated the DTOs and Controllers in NestJS, while I refined the Vue Components.
Webmail Client (Vite + Vue3)
This was trickier. We needed an interface that felt like a desktop app. We used Vite for speed.
The “Simplification” (Redis Removal)
Around evening, as docker-compose logs flowed, I saw an uninvited guest: redis_1.
Me: “Where did Redis come from? We have Postgres. We have Memory. Keep it simple. Rip it out.”
The Refactor Loop:
- Delete Redis from Docker Compose.
- Rip Redis module out of NestJS.
- Result: System still works. Complexity reduced.
4. The Safety Net: Unit Tests
Before diving into the E2E war, we established a non-negotiable baseline: 100% Unit Test Coverage.
While E2E tests ensured the user was happy, Unit Tests ensured the developer could sleep at night.
Key Achievements:
- Service Isolation: Every service (IMAP, SMTP, Auth) was mocked and tested in isolation.
- Edge Cases: What if the IMAP connection drops? What if the disk is full? We wrote tests for these “impossible” scenarios.
- Strict Parsing: We found that
parseIntwas dangerous, so we refactored everything to use stricter number parsers, verified by unit tests.
The coverage report wasn’t just a metric; it was our definition of “Done”.
5. The Real War: E2E Tests
This was the most critical part of the 20-hour marathon (14:00 PM - 22:00 PM). We didn’t mock anything here. Real Docker containers, real Postfix, real databases.
Part A: Admin Panel & The Login Checklist
The Admin Panel tests were our proving ground. We defined a strict checklist for crucial features like Authentication.
| Test Case | Description | Status |
|---|---|---|
| TC-LOGIN-010 | Submit with empty email - show error | ✅ |
| TC-LOGIN-011 | Submit with empty password - show error | ✅ |
| TC-LOGIN-012 | Invalid email format (missing @) | ✅ |
| TC-LOGIN-020 | Login with invalid credentials - show error | ✅ |
| TC-LOGIN-021 | Successful login redirects to dashboard | ✅ |
| TC-LOGIN-030 | Unauthenticated access to dashboard -> redirect | ✅ |
Part B: Webmail & The Compose Checklist
Webmail was harder because of network isolation (Docker networking is cruel). But once connected, we verified every user interaction.
| Test Case | Description | Status |
|---|---|---|
| TC-COMPOSE-010 | ’To’ input visibility | ✅ |
| TC-COMPOSE-015 | Editor visibility | ✅ |
| TC-COMPOSE-021 | Bold button visibility | ✅ |
| TC-COMPOSE-026 | Attach file button visibility | ✅ |
| TC-COMPOSE-030 | Send button visibility | ✅ |
| TC-COMPOSE-040 | Send without recipient -> Show Error | ✅ |
| TC-COMPOSE-045 | Cancel navigates back | ✅ |
| TC-COMPOSE-050 | File input existence check | ✅ |
Part C: The “Hidden” Bugs Found by E2E
Tests didn’t just verify code; they found bugs humans missed.
- Vue Render Crash: A test specifically for toggling the sidebar revealed a crash because
folders.findwas called on an Object instead of an Array. - Missing Sent Folder: An E2E test checked the “Sent” folder after sending an email. It failed. We realized we were sending via SMTP but never saving the message to IMAP! Implementation added immediately.
Total E2E Tests: 484 Scenarios (All Passing)
6. Quality Control: The SonarQube Gate
It was late (22:00 PM). The system worked. The tests passed. But were we “Production Ready”? We spun up SonarQube locally and ran a scan.
The Cold Shower:
- 71 Code Smells
- 21% Coverage (on a specific module we missed)
- Security Hotspot:
Math.random()used for ID generation.
The Cleanup (22:00 - 02:00): We treated SonarQube errors as compile errors.
- Strict Types: Replaced every
anywith a proper DTO. - Modern JS: Replaced old
concatwith spread operators, fixedparseInttoNumber.parseInt. - Security: Removed
Math.random()and implemented crypto-secure ID generation. - Unit Tests: We wrote 3 missing unit tests for the
appendToSentmethod we discovered during E2E.
Result: Coverage: 100.0% | Bugs: 0 | Code Smells: 0 | Security Hotspots: 0
7. Final: The “One Command” Deployment
Dawn of the next day (02:00 - 04:00).
The final piece of the puzzle was unification. Currently, services ran in disparate silos. We unified everything into a single docker-compose.yml.
- Problem: Frontend couldn’t talk to Backend in the new network.
- Fix: Configured internal Docker DNS aliases.
- Problem: Admin user hash was invalid in the seed script.
- Fix: Regenerated bcrypt hashes.
The Final Stats: We successfully delivered a complete mail server stack.
- Total Code: ~18,000 Lines of Code (Frontend + Backend + Infrastructure)
- Language Split: 68% TypeScript, 21% Vue, 3% Go, 8% Shell/Config
- Test Coverage: 484 E2E Scenarios, 100% Unit Test Coverage on Core Modules
- Outcome: A self-contained, Dockerized mail server ready for deployment.
8. The ROI: Traditional vs. AI-Assisted Development
This section compares the actual effort spent (20 hours with AI) against a realistic estimate of traditional development.
What Was Built (Inventory)
| Component | Details |
|---|---|
| Go TUI Installer | 450 LOC, Single-binary, embedded web assets |
| Admin Panel (Vue3) | 9 Views, NestJS API (8 modules), JWT auth |
| Webmail Client (Vue3) | 7 Views, NestJS API (IMAP/SMTP proxy), attachments |
| Docker Infrastructure | 6 services, 4 Dockerfiles, 3 separate E2E stacks |
| Postfix/Dovecot Config | TLS, SASL, Virtual domains, PostgreSQL backend |
| E2E Tests (Playwright) | 484 test cases, Docker-in-Docker execution |
| Unit Tests (Jest) | 100% core coverage, 8,184 lines of test code |
Traditional Development Estimate (Without AI)
Phase 1: Research & Learning
| Task | Time |
|---|---|
| Learn Postfix architecture | 16h |
| Learn Dovecot | 12h |
| Learn IMAP protocol | 8h |
| Learn SMTP protocol | 6h |
| Research Docker networking | 8h |
| Research Playwright E2E | 6h |
Phase 2: Implementation
| Task | Time |
|---|---|
| Postfix/Dovecot config | 24h |
| Go TUI installer | 16h |
| Admin Panel backend | 40h |
| Admin Panel frontend | 32h |
| Webmail backend | 32h |
| Webmail frontend | 28h |
| Docker Compose unification | 12h |
Phase 3: Testing
| Task | Time |
|---|---|
| E2E infrastructure setup | 16h |
| Write 287 Admin E2E tests | 48h |
| Write 197 Webmail E2E tests | 36h |
| Debug flaky tests | 16h |
| Unit test coverage | 24h |
Phase 4: Quality & Polish
| Task | Time |
|---|---|
| SonarQube integration + fixes | 12h |
| Documentation | 8h |
The Comparison
| Metric | Traditional | AI-Assisted | Savings |
|---|---|---|---|
| Research | 56h | ~2h | 96% |
| Implementation | 184h | ~12h | 93% |
| Testing | 140h | ~4h | 97% |
| Quality | 20h | ~2h | 90% |
| Total | 400h | 20h | 95% |
400 hours (Traditional) vs. 20 hours (AI-Assisted). This is the difference between 50 business days (2.5 months) and one single day.
Why Such a Huge Difference?
- Instant Domain Knowledge: AI knew Postfix, Dovecot, IMAP RFCs. I didn’t have to read manuals.
- Boilerplate Elimination: 80% of NestJS/Vue code is structural. AI generated it in seconds.
- Test Generation: Writing 484 E2E tests manually would take weeks. AI generated them in minutes.
- Debugging Acceleration: When Playwright tests failed, AI knew the Docker networking quirks immediately.
- No Context Switching: AI remembered the entire codebase. No “where was that file again?”
The Bottom Line
We started with an idea. We established a strict protocol with AI. We built the core, covered it with Unit Tests, stressed it with E2E tests, and polished it with SonarQube.
In less than 24 hours, we didn’t just write code. We engineered a product.
“If it’s not Green, it’s not Done.”