Skip to content

Building a Self-Hosted Email Server: Complete Overview

Google Workspace increased prices 17-22% in 2025. At $168/year per user, a 10-person team pays $1,680 annually. Self-hosting? Just $72/year for the server. That’s 95% cost savings.

But cost isn’t the only reason. With corporate surveillance, data mining for AI training, and the constant threat of account suspension, taking control of your email infrastructure has never been more compelling.

Over 22 days in December 2025 and January 2026, I built a complete email server from scratch in Go. This article chronicles the architecture, performance metrics, deployment strategies, and unique features that emerged from 87 commits.

Why Self-Host Email in 2026?

Cost Comparison

SolutionAnnual Cost (10 users)Per-User CostPrivacyControl
Google Workspace$1,680$168LowNone
Microsoft 365$1,500$150LowNone
ProtonMail Business$900$90HighLimited
Self-Hosted$72$7.20MaximumComplete

Beyond Cost: Privacy and Control

Modern email providers scan every message. Gmail uses email content for ad targeting and AI training. ProtonMail encrypts in transit but requires proprietary clients. Self-hosting gives you:

  • Zero corporate surveillance: Your emails never leave your server
  • Unlimited users: $72/year covers everyone, forever
  • No account suspension: You own your data, no one can revoke access
  • Standards compliance: Full IMAP/SMTP/CalDAV/CardDAV support
  • Custom features: Add whatever you need (we built unique features like Screener and Send Later)

Architecture Overview

The server follows a layered architecture with clear separation of concerns:

┌───────────────────────────────────────────────────────────────────────┐
│                             Mail Server                               │
├───────────┬───────────┬───────────┬───────────┬───────────┬───────────┤
│  SMTP(25) │  Sub(587) │ IMAP(993) │ DAV(8443) │Admin(8080)│ AutoDisc  │
├───────────┴───────────┴───────────┴───────────┴───────────┴───────────┤
│                           Security Layer                              │
│  ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐  │
│  │  Rate Limit  │ │  Greylisting │ │    Audit     │ │   TLS/ACME   │  │
│  └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘  │
├───────────────────────────────────────────────────────────────────────┤
│                        Authentication Layer                           │
│  ┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐    │
│  │     Argon2id      │ │    User Quotas    │ │    Multi-Domain   │    │
│  └───────────────────┘ └───────────────────┘ └───────────────────┘    │
├───────────────────────────────────────────────────────────────────────┤
│                           Delivery Engine                             │
│  ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐  │
│  │    Redis     │ │   Circuit    │ │   Retry w/   │ │     TLS      │  │
│  │    Queue     │ │   Breakers   │ │   Backoff    │ │   Fallback   │  │
│  └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘  │
├───────────────────────────────────────────────────────────────────────┤
│                            Storage Layer                              │
│  ┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐    │
│  │      SQLite       │ │      Maildir      │ │       DKIM        │    │
│  │    (metadata)     │ │     (emails)      │ │     (signing)     │    │
│  └───────────────────┘ └───────────────────┘ └───────────────────┘    │
├───────────────────────────────────────────────────────────────────────┤
│                            Observability                              │
│  ┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐    │
│  │    Prometheus     │ │      Audit        │ │      Health       │    │
│  │     Metrics       │ │       Logs        │ │      Checks       │    │
│  └───────────────────┘ └───────────────────┘ └───────────────────┘    │
└───────────────────────────────────────────────────────────────────────┘

Core Components

The codebase is organized into 21 internal packages:

internal/
├── admin/          # Web interface and HTTP handlers
├── api/            # RESTful API endpoints
├── audit/          # Security event logging
├── auth/           # Authentication with Argon2id
├── autodiscover/   # Outlook/Apple Mail auto-configuration
├── config/         # Configuration management
├── dav/            # CalDAV/CardDAV server
├── dns/            # DNS verification and validation
├── greylist/       # Greylisting for spam prevention
├── imap/           # IMAP server with IDLE support
├── logging/        # Structured logging
├── metrics/        # Prometheus metrics
├── queue/          # Redis-backed message queue
├── resilience/     # Circuit breakers and retry logic
├── security/       # TLS, rate limiting, encryption
├── setup/          # Setup wizard and preflight checks
├── sieve/          # Email filtering engine
├── smtp/           # SMTP server and delivery engine
├── storage/        # SQLite database and Maildir storage
├── validation/     # Input validation
└── features/       # Unique features (Screener, Send Later, etc.)

Tech Stack Rationale

Why Go?

Go emerged as the ideal choice for several reasons:

  1. Concurrency primitives: Goroutines make handling concurrent IMAP connections and SMTP sessions trivial
  2. Static binary: Single executable deployment—no runtime dependencies
  3. Performance: Compiled to native code with excellent memory management
  4. Standard library: Robust HTTP server, crypto, and database packages
  5. Ecosystem: Excellent IMAP/SMTP libraries from the emersion project

Storage Choices

# SQLite for metadata
database_path: /var/lib/mailserver/mail.db
# Lightweight, embedded, no external database server needed
# Perfect for user accounts, domains, DKIM keys

# Maildir for email storage
maildir_path: /var/lib/mailserver/maildir
# Standard format, easy to backup, works with standard tools
# File-based, one file per email, O(1) access

# Redis for message queue
redis:
  address: localhost:6379
  db: 0
# In-memory, persistent when configured
# Perfect for delivery retries and scheduled emails

Why SQLite over PostgreSQL/MySQL?

For an email server:

  • Zero admin overhead: No database server to monitor or patch
  • Atomic backups: Single file = simple snapshots
  • Sufficient scale: Handles hundreds of domains and thousands of users
  • Fast: In-memory caching makes reads instant

For the message queue, Redis provides:

  • Ordered operations: Critical for delivery retries
  • Expiration: Auto-cleanup of old queue entries
  • Atomic operations: Prevent race conditions

Performance Metrics

After comprehensive optimizations (commits e7b2314, 4beac86), the server achieves remarkable efficiency:

Memory Footprint

MetricBeforeAfterImprovement
Current Memory149.65 MB9.39 MB94% reduction
Peak Memory459.05 MB71.09 MB85% reduction
Resident Memory282 MB27.4 MB90% reduction

Current Usage (Post-Optimization)

MetricValue% of Limit
Current Memory (cgroup)9.39 MB1.8%
Peak Memory71.09 MB14%
Binary Size32 MB-

CPU Usage

MetricValue
Current CPU0.1%
Threads6
Load Average0.07 (1m), 0.03 (5m), 0.00 (15m)

Capacity Estimates

MetricCurrentEstimated Max
Concurrent IMAP6~500-1000
Emails/hour~1~2000-5000
Mailboxes8~500-1000

Assessment: The server runs at 10-15% capacity and can handle 10x growth without hardware changes.

Optimization Techniques Applied

  1. Memory leak fix (commit e7b2314): Fixed UserRateLimiter with periodic cleanup
  2. Pre-compiled regex: Avoided compilation on every request
  3. IMAP session pre-allocation: Reduced GC pressure from map growth
  4. String allocation fixes: Replaced unnecessary conversions
  5. Goroutine limits: Prevented unbounded spawn under load

Feature Matrix

Core Email Protocols

FeatureImplementationStatus
IMAP Servergo-imap/v2 with native IDLE support
IDLE Push NotificationsReal-time email delivery
POP3 SupportLegacy compatibility (removed in 7d093d3)
SMTP Servergo-smtp with delivery engine
SMTPS (465)SSL/TLS
Submission (587)STARTTLS
DKIM SigningPer-domain signing keys
SPF VerificationInbound authentication
DMARC VerificationPolicy enforcement
Relay HostSmart outbound delivery

Calendar & Contacts

FeatureImplementationStatus
CalDAV ServerFull WebDAV + CalDAV protocol
CardDAV ServervCard standard support
Apple CalendarTested and working
ThunderbirdFull compatibility

Security Features

FeatureImplementationStatus
TLS/ACMEAutomatic Let’s Encrypt certificates
Argon2idOWASP-recommended password hashing
GreylistingConfigurable delay and age
Rate LimitingPer-user limits with cleanup
Audit LoggingAll security events tracked
TLS FallbackGraceful degradation for misconfigured servers

Administration

FeatureImplementationStatus
Web Admin PanelHacker News-style UI
Prometheus Metrics/metrics endpoint
Health Endpoints/health and /health/detailed
Auto-discoveryOutlook and Apple Mail support
Setup WizardGuided installation
Preflight ChecksPre-deployment validation
Doctor CommandDiagnostic troubleshooting

Unique Features

FeatureImplementationCommit
ScreenerHey.com-style first-contact filteringdc3201e
Email AliasesDisposable/masked addresses with trackingdc3201e
Send LaterScheduled email deliverydc3201e, 6e487f7
Undo SendConfigurable 0-30s delaydc3201e
Email SnoozeHide and reappear laterdc3201e, e2859ef
VIP ContactsPriority sender managementdc3201e, cb873d7
Two-Factor AuthAdmin panel security7cf5afb

Development Timeline

The project evolved rapidly over 22 days, from December 19, 2025 to January 10, 2026:

Phase 1: Foundation (Dec 19-20, 2025)

  • Initial implementation of personal email server (Dec 19)
  • Add CLI entry point with serve, migrate, domain, and user commands (Dec 19)
  • Add CalDAV/CardDAV support, DKIM signing, and deployment files (Dec 19)
  • Add comprehensive tests and documentation (Dec 19)
  • Add detailed Linode + Cloudflare deployment guide (Dec 19)

Key accomplishments: Core IMAP/SMTP servers, CLI, basic CalDAV/CardDAV, deployment guides.

Phase 2: Production Readiness (Dec 20-21, 2025)

  • Fix IMAP SELECT crash by always setting status fields (Dec 20)
  • Fix IMAP crash when client requests BODYSTRUCTURE (Dec 20)
  • Add instant email notifications via IMAP IDLE (Dec 20)
  • Performance: instant email notifications with async delivery and O(1) file access (Dec 20)
  • Fix critical IDLE bug: remove run() goroutine stealing updates (Dec 20)
  • Upgrade to go-imap v2 for native IDLE support (Dec 20)
  • Add Sieve filtering, admin panel, and DNS tools (Dec 20)
  • Fix admin panel SQL queries for users table (Dec 20)
  • Wire up Sieve executor and admin server in serve command (Dec 20)

Key accomplishments: IMAP IDLE support, Sieve filtering, web admin panel, stability fixes.

Phase 3: Reliability & Auto-discovery (Dec 21-30, 2025)

  • Deep reliability audit: IMAP, Sieve, Queue, DNS, DAV, Delivery (Dec 21)
  • Major reliability improvements: Apple-quality production code (Dec 21)
  • Fix Apple mobileconfig: use SMTPS port 465 with SSL (Dec 21)
  • Add email client autodiscover for easy setup (Dec 21)
  • Add queue monitoring to admin UI and extend session duration (Dec 21)
  • Add security, reliability, and usability improvements (Dec 21)
  • Fix: Start DAV server for CalDAV/CardDAV support (Dec 30)
  • Improve CalDAV/CardDAV compatibility for Apple clients (Dec 30)
  • Improve DAV security and fix SMTP authentication (Dec 30)

Key accomplishments: Auto-discovery for Outlook/Apple Mail, CalDAV/CardDAV fixes, reliability audit.

Phase 4: Security & Monitoring (Dec 31, 2025 - Jan 2, 2026)

  • Add metrics, audit logging, greylisting, and fix TLS delivery fallback (Dec 31, 2025)
  • Clarify encryption and privacy claims (Jan 2, 2026)
  • Use /tmp for internal files (Jan 2, 2026)

Key accomplishments: Prometheus metrics, audit logging, greylisting, TLS delivery fallback.

Phase 5: Multi-Domain & DKIM (Jan 4-8, 2026)

  • Add bind_address config option for multi-homed servers (Jan 4)
  • Add transactional email API with SendGrid-like functionality (Jan 5)
  • Major server improvements: security, reliability, and logging (Jan 5)
  • Add database migration for DKIM key management (Jan 7)
  • Add DKIM key storage abstraction layer (Jan 7)
  • Add CLI commands for DKIM management and backup/restore (Jan 7)
  • Add DKIM management to admin dashboard domains page (Jan 7)
  • Add system management page with backup/restore to admin (Jan 7)
  • Add DNS verification feature for domains (Jan 8)
  • Add domain setup wizard with DNS verification flow (Jan 8)

Key accomplishments: Full multi-domain support, DKIM key management, DNS verification wizard, transactional email API.

Phase 6: UI Improvements & 2FA (Jan 9-10, 2026)

  • Fix migration 006 to record version in schema_migrations (Jan 9)
  • Add two-factor authentication for admin panel (Jan 9)
  • Fix QR code generation and DNS wizard CSRF issues (Jan 9)
  • Simplify domain add form to single step (Jan 9)
  • Fix autodiscover to support multiple domains dynamically (Jan 9)
  • Fix multi-domain support throughout the codebase (Jan 9)
  • Add server performance report with live metrics (Jan 9)
  • Performance optimizations for memory and CPU (Jan 9)
  • Refactor admin UI to HN style with shared CSS file (Jan 9)
  • Add breadcrumbs navigation to all admin templates (Jan 9)
  • Optimize UI and fix critical security vulnerabilities (Jan 9)
  • Add admin dashboard improvements: phases 2-5 (Jan 9)
  • Improve mobile responsiveness in admin interface (Jan 10)

Key accomplishments: 2FA security, mobile responsive UI, performance optimizations, HN-style admin design.

Phase 7: Unique Features (Jan 10-12, 2026)

  • Add unique features: Screener, Aliases, Send Later, Undo Send, Snooze (Jan 12)
  • Refactor features to use admin dashboard pattern (Jan 12)
  • Initialize features store and add navigation link (Jan 12)
  • Add comprehensive tests for unique features (Jan 12)
  • Make all feature cards clickable (Jan 12)
  • Standardize features UI to match admin design system (Jan 12)
  • Add Send Later and Snooze features UI (Jan 12)
  • Start feature scheduler for Send Later, Snooze, Undo Send (Jan 12)
  • Add MessageMover for snooze wake-ups (Jan 12)
  • Use delivery queue for scheduled email sending (Jan 12)
  • Add email sender for scheduled email delivery (Jan 12)
  • Add VIP checking and smart zone detection (Jan 12)

Key accomplishments: Hey.com-style Screener, email aliases, Send Later, Undo Send, Snooze, VIP contacts.

Code Examples

IMAP Server with IDLE Support

The IMAP server uses the go-imap/v2 library with native IDLE support:

// @filename: main.go
// internal/imap/server.go
func (s *Server) handleIDLE(c *server.Conn, cmd *imap.Command) error {
    // Create IDLE handler
    handler := &idle.Handler{
        NewCallback: func() {
            // Send EXISTS update when new mail arrives
            c.SendUpdates(&imap.StatusUpdate{
                Mailbox: c.Mailbox(),
                Updates: []imap.StatusItem{imap.StatusMessages},
            })
        },
    }

    // Start IDLE
    return handler.Handle(cmd)
}

This implementation provides instant push notifications when new emails arrive, eliminating the need for polling.

DKIM Signing

DKIM signing is performed per-domain with automatically managed keys:

// @filename: main.go
// internal/smtp/dkim.go
func (b *Backend) SignMessage(msg *message.Message, domain string) error {
    // Get DKIM key for domain
    key, err := b.dkimStore.GetKey(domain)
    if err != nil {
        return err
    }

    // Create DKIM signer
    signer, err := dkim.NewSigner(b.hostname, "mail", domain, key)
    if err != nil {
        return err
    }

    // Add DKIM-Signature header
    return signer.Sign(msg)
}

Delivery Engine with Retry Logic

The delivery engine uses exponential backoff with per-domain circuit breakers:

// @filename: main.go
// internal/smtp/delivery/worker.go
func (w *Worker) deliver(msg *queue.Message) error {
    // Check circuit breaker
    if w.circuit.IsOpen(msg.ToDomain) {
        return fmt.Errorf("circuit breaker open for %s", msg.ToDomain)
    }

    // Attempt delivery
    err := w.smtpClient.Deliver(msg)
    if err != nil {
        // Record failure
        w.circuit.RecordFailure(msg.ToDomain)

        // Calculate backoff
        delay := w.calculateBackoff(msg.Attempts)

        // Requeue with delay
        return w.queue.Requeue(msg.ID, delay)
    }

    // Success - close circuit
    w.circuit.RecordSuccess(msg.ToDomain)
    return nil
}

func (w *Worker) calculateBackoff(attempt int) time.Duration {
    // Exponential backoff: 5m, 15m, 30m, 1h, 2h, 4h, 8h, 16h, 24h (capped)
    backoffs := []time.Duration{
        5 * time.Minute,
        15 * time.Minute,
        30 * time.Minute,
        1 * time.Hour,
        2 * time.Hour,
        4 * time.Hour,
        8 * time.Hour,
        16 * time.Hour,
        24 * time.Hour,
    }

    idx := min(attempt, len(backoffs)-1)
    // Add ±10% jitter
    jitter := time.Duration(float64(backoffs[idx]) * 0.1 * (rand.Float64()*2 - 1))
    return backoffs[idx] + jitter
}

Greylisting Implementation

Greylisting rejects first-time senders temporarily:

// @filename: main.go
// internal/greylist/store.go
func (s *Store) ShouldGreylist(ip, from, to string) (bool, time.Duration, error) {
    key := s.buildKey(ip, from, to)

    // Check if we've seen this triplet before
    entry, err := s.get(key)
    if err != nil {
        if err == ErrNotFound {
            // First time - greylist
            return true, s.minDelay, nil
        }
        return false, 0, err
    }

    // Check if we've passed the minimum delay
    if time.Since(entry.SeenAt) < s.minDelay {
        // Too soon - still greylisting
        return true, s.minDelay - time.Since(entry.SeenAt), nil
    }

    // Passed delay - remember and allow
    entry.SeenAt = time.Now()
    entry.ExpiresAt = time.Now().Add(s.maxAge)
    s.set(key, entry)

    return false, 0, nil
}

Deployment Options

Docker Deployment

# docker-compose.yml
version: '3.8'
services:
  mailserver:
    build: .
    ports:
      - '25:25'
      - '587:587'
      - '465:465'
      - '143:143'
      - '993:993'
      - '8443:8443'
      - '8080:8080'
    volumes:
      - ./config:/etc/mailserver
      - maildata:/var/lib/mailserver
    depends_on:
      - redis
    environment:
      - TZ=UTC

  redis:
    image: redis:7-alpine
    volumes:
      - redisdata:/data
    restart: unless-stopped

volumes:
  maildata:
  redisdata:

Systemd Deployment (Production)

# @filename: .env
# /etc/systemd/system/mailserver.service
[Unit]
Description=Mail Server
After=network.target redis.service
Requires=redis.service

[Service]
Type=simple
User=mailserver
Group=mailserver
ExecStart=/usr/local/bin/mailserver serve --config /etc/mailserver/config.yaml
Restart=always
RestartSec=5s
StandardOutput=journal
StandardError=journal

# Security hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/mailserver

[Install]
WantedBy=multi-user.target

Enable and start:

# @filename: script.sh
sudo systemctl daemon-reload
sudo systemctl enable mailserver
sudo systemctl start mailserver
sudo journalctl -u mailserver -f

Bare Metal Deployment

For maximum performance and minimal overhead:

# @filename: script.sh
# 1. Build from source
git clone https://github.com/fenilsonani/email-server.git
cd email-server
go build -o mailserver ./cmd/mailserver

# 2. Copy to system location
sudo cp mailserver /usr/local/bin/
sudo chmod +x /usr/local/bin/mailserver

# 3. Create directories
sudo mkdir -p /etc/mailserver /var/lib/mailserver/maildir
sudo chown mailserver:mailserver /var/lib/mailserver

# 4. Initialize
sudo -u mailserver mailserver setup
sudo -u mailserver mailserver migrate

Quick Start Wizard

The built-in setup wizard handles the entire initial configuration:

# @filename: script.sh
# Run preflight checks
./mailserver preflight

# Interactive setup wizard
./mailserver setup

# Diagnose any issues
./mailserver doctor

The wizard checks:

  • Port availability (25, 587, 465, 143, 993, 8080, 8443)
  • DNS configuration (MX, TXT, SRV records)
  • Redis connectivity
  • Directory permissions
  • TLS certificate status

Monitoring and Observability

Prometheus Metrics

Metrics are exposed at /metrics on the admin port:

curl http://localhost:8080/metrics

Available metrics:

MetricTypeDescription
mailserver_messages_received_totalCounterTotal inbound messages
mailserver_messages_sent_totalCounterSuccessful deliveries
mailserver_messages_rejected_totalCounterRejected messages (by reason)
mailserver_messages_bounced_totalCounterBounced messages
mailserver_delivery_duration_secondsHistogramDelivery latency
mailserver_queue_depthGaugeCurrent queue size
mailserver_auth_attempts_totalCounterAuth attempts (by result/protocol)
mailserver_quota_exceeded_totalCounterQuota rejections
mailserver_active_connectionsGaugeActive connections (by protocol)

Health Endpoints

# @filename: script.sh
# Basic health check
curl http://localhost:8080/health
# {"status":"ok"}

# Detailed health with component status
curl http://localhost:8080/health/detailed
# {"status":"ok","components":{"database":"ok","redis":"ok","storage":"ok"}}

Grafana Dashboard

Import the included dashboard from deploy/grafana-dashboard.json for visualization of all metrics.

Unique Features Deep Dive

The Screener

Inspired by Hey.com, the Screener holds emails from first-time senders until you approve them:

// @filename: main.go
// internal/features/screener.go
func (s *Screener) CheckMessage(from, to string) (Action, error) {
    // Check if sender is approved
    approved, err := s.isApproved(from, to)
    if err != nil {
        return Reject, err
    }

    if approved {
        return Accept, nil
    }

    // First time - move to Screener mailbox
    return Screen, nil
}

Benefits:

  • Zero spam from unknown senders
  • One-click approval for legitimate contacts
  • Automatic whitelist for approved senders

Email Aliases

Create disposable or masked email addresses with usage tracking:

// @filename: main.go
// internal/features/aliases.go
func (a *AliasManager) CreateAlias(user, domain string) (string, error) {
    // Generate random alias
    alias := fmt.Sprintf("alias-%s@%s", generateToken(), domain)

    // Store mapping
    err := a.store.Set(alias, user)
    if err != nil {
        return "", err
    }

    return alias, nil
}

func (a *AliasManager) DeliverToRealRecipient(alias string) (string, error) {
    return a.store.Get(alias)
}

Use cases:

  • Sign up for services without exposing your real email
  • Track which company leaked your email
  • Disable compromised aliases instantly

Send Later

Schedule emails for future delivery:

// @filename: main.go
// internal/features/scheduler.go
func (s *Scheduler) ScheduleEmail(msg *Email, sendAt time.Time) error {
    // Calculate delay
    delay := time.Until(sendAt)

    // Add to delivery queue with scheduled time
    return s.queue.AddWithDelay(msg.ID, delay)
}

Benefits:

  • Work across time zones without waking people up
  • Batch send emails at optimal times
  • Never forget to send important emails

Undo Send

Configurable delay before sending:

features:
  undo_send:
    enabled: true
    delay_seconds: 10 # 0-30 seconds

Email Snooze

Hide emails and have them reappear later:

// @filename: main.go
// internal/features/snooze.go
func (s *Snoozer) SnoozeEmail(msgID, mailbox string, until time.Time) error {
    // Move to Snoozed mailbox
    err := s.moveMail(msgID, "Snoozed")
    if err != nil {
        return err
    }

    // Schedule wake-up
    return s.scheduler.Schedule(func() {
        s.moveMail(msgID, mailbox)
    }, until)
}

VIP Contacts

Priority handling for important senders:

// @filename: main.go
// internal/features/vip.go
func (v *VIPManager) IsVIP(user, sender string) bool {
    // Check VIP list
    return v.store.Has(user, sender)
}

Security Considerations

Encryption

  • In transit: TLS enforced via ACME (Let’s Encrypt)
  • At rest: Unencrypted in Maildir (standard)
  • Recommendation: Use full-disk encryption (LUKS, FileVault)

Password Security

// @filename: handlers.go
// internal/auth/password.go
func HashPassword(password string) (string, error) {
    // Argon2id with OWASP parameters
    params := argon2.DefaultParams()
    params.Time = 3
    params.Memory = 64 * 1024  // 64 MB
    params.Threads = 4
    params.KeyLen = 32
    params.SaltLen = 16

    return argon2.CreateHash(password, params)
}

Rate Limiting

Per-user rate limits with automatic cleanup:

// @filename: main.go
// internal/security/ratelimit.go
func (r *RateLimiter) Check(userID, action string) (bool, time.Duration) {
    key := fmt.Sprintf("%s:%s", userID, action)

    count, err := r.redis.Get(key)
    if err != nil && err != redis.Nil {
        return false, 0
    }

    if count >= r.limits[action] {
        // Rate limited - return reset time
        ttl, _ := r.redis.TTL(key)
        return false, ttl
    }

    // Increment counter
    r.redis.Incr(key)
    r.redis.Expire(key, r.window)

    return true, 0
}

Firewall Recommendations

# @filename: script.sh
# Only open required ports
sudo ufw allow 25/tcp    # SMTP
sudo ufw allow 587/tcp   # Submission
sudo ufw allow 465/tcp   # SMTPS
sudo ufw allow 143/tcp   # IMAP
sudo ufw allow 993/tcp   # IMAPS
sudo ufw allow 8443/tcp  # CalDAV/CardDAV

# Restrict admin panel to localhost or VPN
sudo ufw deny 8080/tcp

Troubleshooting

Preflight Checks

./mailserver preflight

This checks:

  • Port availability
  • DNS configuration
  • Redis connectivity
  • Directory permissions
  • TLS certificate status

Doctor Command

./mailserver doctor

Diagnoses common issues:

  • Service connectivity
  • DNS record validation
  • Deliverability testing
  • Storage health

Common Issues

Connection refused:

sudo ss -tlnp | grep mailserver
sudo ufw status

TLS certificate issues:

ls -la /var/lib/mailserver/acme/
openssl s_client -connect mail.yourdomain.com:993 -servername mail.yourdomain.com

Email not delivered:

# @filename: script.sh
dig MX yourdomain.com
dig TXT yourdomain.com
dig TXT mail._domainkey.yourdomain.com
dig TXT _dmarc.yourdomain.com

Conclusion

Building a self-hosted email server in 2026 is not only feasible—it’s practical. With 87 commits over 22 days, we achieved:

  • 95% cost savings compared to Google Workspace
  • 9.4 MB memory footprint running at 10-15% capacity
  • Full standards compliance with IMAP/SMTP/CalDAV/CardDAV
  • Production-grade reliability with circuit breakers and retry logic
  • Unique features like Screener, Send Later, and Email Aliases

The architecture leverages Go’s strengths: concurrency, static binaries, and excellent libraries. The tech stack—Go, SQLite, Redis, Maildir—provides simplicity without sacrificing power.

For individuals and small teams seeking email independence, self-hosting is no longer a complex endeavor. It’s a viable, cost-effective alternative that puts you in complete control of your communications.

Further Reading

email go self-hosted devops
Share:

Continue Reading

IMAP IDLE Implementation: From Crashes to Production

A deep dive into implementing real-time email notifications using IMAP IDLE, chronicling three crashes, library bugs, and the journey to production-grade instant email delivery with Go. Learn about goroutine race conditions, go-imap v1 vs v2, and O(1) file access optimizations.

Read article
GoIMAPIDLE

Debugging IMAP Crashes: The Nil Pointer Nightmare

A deep dive into debugging three critical IMAP server crashes caused by nil pointer dereferences. Learn how we tracked down and fixed SELECT response crashes, BODYSTRUCTURE panics, and capability advertising issues in production.

Read article
GoIMAPDebugging

Quantum Computing for Developers: A Practical Guide to the Future of Computing

A comprehensive introduction to quantum computing for classical developers. Learn the fundamentals of qubits, quantum gates, and quantum algorithms. Explore practical implementations using Qiskit and Cirq, understand quantum machine learning basics, and discover how to get started with quantum simulators in the NISQ era.

Read article
GoBackendConcurrency