Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Parth-420/Zapmail/llms.txt

Use this file to discover all available pages before exploring further.

Zapmail’s SMTP server is a custom implementation written in Go that handles incoming email connections and stores them in PostgreSQL. The server implements the core SMTP protocol commands needed for receiving emails.

Server initialization

The SMTP server starts by connecting to the database and listening for incoming TCP connections:
// From backend/main.go:35-61
func main() {
	db := connectDB()
	// Start the cleanup job to purge emails older than 7 days.
	go startCleanupJob(db)
	port := os.Getenv("PORT")
	if port == "" {
		log.Fatal("PORT environment variable not set")
	}

	// Start listening for incoming SMTP 
	ln, err := net.Listen("tcp", ":"+port)
	if err != nil {
		log.Fatal("Error starting server:", err)
	}
	log.Println("Temporary Mail Service SMTP Server listening on port "+port)

	for {
		conn, err := ln.Accept()
		if err != nil {
			log.Println("Error accepting connection:", err)
			continue
		}
		go handleConnection(conn, db)
	}
}
Each incoming connection is handled in a separate goroutine, allowing the server to process multiple emails concurrently.

Email struct

The Email struct represents an email record in the system:
// From backend/main.go:20-26
type Email struct {
	ID         int      
	Username   string    
	Recipient  string   
	RawData    string    
	ReceivedAt time.Time 
}
FieldTypeDescription
IDintAuto-generated database ID
UsernamestringExtracted username from recipient (before @)
RecipientstringFull recipient email address
RawDatastringComplete raw email content including headers
ReceivedAttime.TimeTimestamp when the email was received

Connection handling

The handleConnection function manages the SMTP conversation with the client:
// From backend/main.go:65-72
func handleConnection(conn net.Conn, db *sql.DB) {
	defer conn.Close()
	reader := bufio.NewReader(conn)
	writer := bufio.NewWriter(conn)

	writer.WriteString("220 Welcome to Temporary Mail Service\r\n")
	writer.Flush()

	var rcptTo string 
	// ... command processing loop
}
The server uses bufio.Reader and bufio.Writer for efficient reading and writing of SMTP protocol messages.

Command processing loop

The server continuously reads and processes SMTP commands until the client disconnects or sends QUIT:
// From backend/main.go:76-90
for {
	line, err := reader.ReadString('\n')
	if err != nil {
		if err == io.EOF {
			log.Println("Client disconnected")
		} else {
			log.Println("Error reading command:", err)
		}
		return
	}

	input := strings.ToUpper(strings.TrimSpace(line))
	log.Printf("Received: %s", input)
	// ... switch statement for command handling
}

SMTP commands

The server implements the following SMTP commands:

HELO / EHLO

Initiates the SMTP conversation:
// From backend/main.go:103-104
case strings.HasPrefix(input, "HELO") || strings.HasPrefix(input, "EHLO"):
	writer.WriteString("250 Hello\r\n")

MAIL FROM

Specifies the sender’s email address:
// From backend/main.go:105-106
case strings.HasPrefix(input, "MAIL FROM:"):
	writer.WriteString("250 Sender OK\r\n")
The server accepts all senders without validation since it’s designed for receiving emails to any address.

RCPT TO

Specifies the recipient’s email address and stores it for later processing:
// From backend/main.go:107-110
case strings.HasPrefix(input, "RCPT TO:"):
	// Save the recipient address for later extraction.
	rcptTo = strings.TrimSpace(line[len("RCPT TO:"):])
	writer.WriteString("250 Recipient OK\r\n")

DATA

Receives the actual email content:
// From backend/main.go:111-150
case input == "DATA":
	writer.WriteString("354 End data with <CR><LF>.<CR><LF>\r\n")
	writer.Flush()

	var dataLines []string
	for {
		dataLine, err := reader.ReadString('\n')
		if err != nil {
			if err == io.EOF {
				break
			}
			log.Println("Error reading email data:", err)
			return
		}
		if strings.TrimSpace(dataLine) == "." {
			break
		}
		dataLines = append(dataLines, dataLine)
	}
	rawEmail := strings.Join(dataLines, "")
	log.Println("Received raw email data:")
	log.Println(rawEmail)

	username := extractUsername(rcptTo)
	
	emailRecord := Email{
		Username:   username,
		Recipient:  rcptTo,
		RawData:    rawEmail,
		ReceivedAt: time.Now(),
	}
	// Store the email in the database.
	if err := storeEmail(db, emailRecord); err != nil {
		log.Println("Error storing email:", err)
		writer.WriteString("550 Error storing email\r\n")
	} else {
		writer.WriteString("250 OK: Message accepted\r\n")
	}

QUIT

Ends the SMTP session:
// From backend/main.go:151-154
case input == "QUIT":
	writer.WriteString("221 Bye\r\n")
	writer.Flush()
	return

Utility commands

The server also supports these informational commands:
// From backend/main.go:94-102
case input == "VRFY":
	writer.WriteString("250 VRFY not supported\r\n")
case input == "NOOP":
	writer.WriteString("250 OK\r\n")
case input == "EXPN":
	writer.WriteString("250 EXPN not supported\r\n")
case input == "HELP":
	writer.WriteString("214-Commands supported:\r\n")
	writer.WriteString("214 HELO, EHLO, MAIL FROM, RCPT TO, DATA, NOOP, VRFY, HELP, EXPN, QUIT\r\n")

Username extraction

The server extracts the username from the recipient email address:
// From backend/main.go:162-170
func extractUsername(rcpt string) string {
	rcpt = strings.Trim(rcpt, "<>")
	parts := strings.Split(rcpt, "@")
	if len(parts) < 2 {
		return ""
	}
	return parts[0]
}
This function:
  1. Removes angle brackets from the email address
  2. Splits the address at the @ symbol
  3. Returns the username portion (before the @)

Email storage

Emails are stored in the database using a simple INSERT query:
// From backend/main.go:185-193
func storeEmail(db *sql.DB, email Email) error {
	query := `
        INSERT INTO emails (username, recipient, raw_data, received_at)
        VALUES ($1, $2, $3, $4)
    `
	_, err := db.Exec(query, email.Username, email.Recipient, email.RawData, email.ReceivedAt)
	return err
}
The ID field is auto-generated by PostgreSQL and doesn’t need to be specified in the INSERT statement.

Automatic cleanup

The server runs a background job to delete old emails every hour:
// From backend/main.go:195-208
func startCleanupJob(db *sql.DB) {
	ticker := time.NewTicker(1 * time.Hour)
	go func() {
		for range ticker.C {
			_, err := db.Exec(`DELETE FROM emails WHERE received_at < NOW() - INTERVAL '7 days'`)
			if err != nil {
				log.Println("Error cleaning up old emails:", err)
			} else {
				log.Println("Cleanup job: Old emails removed.")
			}
		}
	}()
}
This ensures:
  • User privacy by automatically deleting old emails
  • Database efficiency by preventing unlimited growth
  • Compliance with temporary email service expectations
You can adjust the cleanup frequency and retention period by modifying the time.NewTicker interval and the SQL INTERVAL value.

Error handling

The server handles errors at multiple levels: Connection errors:
if err != nil {
	log.Println("Error accepting connection:", err)
	continue  // Continue accepting other connections
}
Reading errors:
if err != nil {
	if err == io.EOF {
		log.Println("Client disconnected")
	} else {
		log.Println("Error reading command:", err)
	}
	return  // Close this connection
}
Storage errors:
if err := storeEmail(db, emailRecord); err != nil {
	log.Println("Error storing email:", err)
	writer.WriteString("550 Error storing email\r\n")
} else {
	writer.WriteString("250 OK: Message accepted\r\n")
}

Dependencies

The SMTP server uses these Go packages:
import (
	"bufio"
	"database/sql"
	"io"
	"log"
	"net"
	"os"
	"strings"
	"time"

	"github.com/joho/godotenv"
	_ "github.com/lib/pq" 
)
PackagePurpose
bufioBuffered I/O for efficient reading/writing
database/sqlDatabase interface
netTCP networking
github.com/joho/godotenvLoad environment variables from .env
github.com/lib/pqPostgreSQL driver

Performance characteristics

  • Concurrent connections: Each connection runs in its own goroutine
  • Memory efficiency: Uses buffered I/O to minimize allocations
  • Database pooling: Reuses database connections across goroutines
  • Non-blocking: Main listener never blocks, even if a connection fails
Go’s lightweight goroutines allow the server to handle thousands of concurrent SMTP connections with minimal overhead.

Build docs developers (and LLMs) love