Skip to main content
Back
Also available in: 🇹🇷 Türkçe

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:

  1. Delete Redis from Docker Compose.
  2. Rip Redis module out of NestJS.
  3. 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 parseInt was 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 CaseDescriptionStatus
TC-LOGIN-010Submit with empty email - show error
TC-LOGIN-011Submit with empty password - show error
TC-LOGIN-012Invalid email format (missing @)
TC-LOGIN-020Login with invalid credentials - show error
TC-LOGIN-021Successful login redirects to dashboard
TC-LOGIN-030Unauthenticated 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 CaseDescriptionStatus
TC-COMPOSE-010’To’ input visibility
TC-COMPOSE-015Editor visibility
TC-COMPOSE-021Bold button visibility
TC-COMPOSE-026Attach file button visibility
TC-COMPOSE-030Send button visibility
TC-COMPOSE-040Send without recipient -> Show Error
TC-COMPOSE-045Cancel navigates back
TC-COMPOSE-050File input existence check

Part C: The “Hidden” Bugs Found by E2E

Tests didn’t just verify code; they found bugs humans missed.

  1. Vue Render Crash: A test specifically for toggling the sidebar revealed a crash because folders.find was called on an Object instead of an Array.
  2. 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 any with a proper DTO.
  • Modern JS: Replaced old concat with spread operators, fixed parseInt to Number.parseInt.
  • Security: Removed Math.random() and implemented crypto-secure ID generation.
  • Unit Tests: We wrote 3 missing unit tests for the appendToSent method 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)

ComponentDetails
Go TUI Installer450 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 Infrastructure6 services, 4 Dockerfiles, 3 separate E2E stacks
Postfix/Dovecot ConfigTLS, 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)

56h

Phase 1: Research & Learning

TaskTime
Learn Postfix architecture16h
Learn Dovecot12h
Learn IMAP protocol8h
Learn SMTP protocol6h
Research Docker networking8h
Research Playwright E2E6h
184h

Phase 2: Implementation

TaskTime
Postfix/Dovecot config24h
Go TUI installer16h
Admin Panel backend40h
Admin Panel frontend32h
Webmail backend32h
Webmail frontend28h
Docker Compose unification12h
140h

Phase 3: Testing

TaskTime
E2E infrastructure setup16h
Write 287 Admin E2E tests48h
Write 197 Webmail E2E tests36h
Debug flaky tests16h
Unit test coverage24h
20h

Phase 4: Quality & Polish

TaskTime
SonarQube integration + fixes12h
Documentation8h

The Comparison

MetricTraditionalAI-AssistedSavings
Research56h~2h96%
Implementation184h~12h93%
Testing140h~4h97%
Quality20h~2h90%
Total400h20h95%

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?

  1. Instant Domain Knowledge: AI knew Postfix, Dovecot, IMAP RFCs. I didn’t have to read manuals.
  2. Boilerplate Elimination: 80% of NestJS/Vue code is structural. AI generated it in seconds.
  3. Test Generation: Writing 484 E2E tests manually would take weeks. AI generated them in minutes.
  4. Debugging Acceleration: When Playwright tests failed, AI knew the Docker networking quirks immediately.
  5. 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.”

Share this article

Suggested hashtags (click to copy):