// Package main is the entry point for the IMC mail server admin application. // This single binary contains both the Go backend API and the embedded SvelteKit frontend. package main import ( "crypto/rand" // cryptographically secure random number generator "flag" // standard library for parsing command-line flags "fmt" // formatted I/O, used here for printing output "log" // logging package "net/http" // HTTP server and client "os" // OS-level operations like reading command-line args "strings" // string manipulation utilities "github.com/gin-gonic/gin" // web framework "github.com/imc/backend/internal/api" // HTTP API routing and handlers "github.com/imc/backend/internal/auth" // password hashing "github.com/imc/backend/internal/config" // configuration loading "github.com/imc/backend/internal/db" // database connection and operations ) // main is the entry point of the application. // It handles command-line flags, initializes the database, sets up the HTTP router, // and starts the web server. func main() { // Parse command-line flags. // Flags are optional arguments passed after the program name. // Example: ./imc --bind=0.0.0.0 --port=8080 // --bind: The IP address the server should listen on. // 0.0.0.0 means listen on all network interfaces (accessible from other machines). // 127.0.0.1 means listen only on localhost (accessible only from this machine). bind := flag.String("bind", "", "IP address to bind to (default: 0.0.0.0)") // --port: The TCP port the server should listen on. // Common ports: 80 (HTTP), 443 (HTTPS), 8080 (HTTP alternate). port := flag.String("port", "", "Port to listen on (default: 8080)") // --reset-admin-password: Generate a random password for the admin user and exit. // Useful for initial setup or recovering from a lost password. resetAdminPassword := flag.Bool("reset-admin-password", false, "Reset admin password to a random value and exit") // Customize the help output to show double-hyphen style (Unix convention). // By default, Go's flag package uses single hyphens. flag.Usage = func() { fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [options]\n\nOptions:\n", os.Args[0]) flag.PrintDefaults() } // Actually parse the flags from command line arguments. flag.Parse() // Load configuration from environment variables. // Environment variables take precedence over code defaults. // See config.Load() for all available environment variables. cfg := config.Load() if err := cfg.Validate(); err != nil { log.Fatalf("Invalid configuration: %v", err) } // Override configuration with command-line flags if they were provided. // Empty string means the flag was not set, so we keep the config value. if *bind != "" { cfg.Bind = *bind } if *port != "" { cfg.Port = *port } // Connect to the database (MariaDB/MySQL). // The database stores both ISPmail data (virtual_users, virtual_domains, etc.) // and IMC's own data (imc_users, imc_users2domains, etc.). database, err := db.Connect(cfg) if err != nil { log.Fatalf("Failed to connect to database: %v", err) // Fatal = print and exit } // Create database tables if they don't exist. // This only creates IMC's own tables, not the ISPmail tables. if err := database.InitSchema(); err != nil { log.Printf("Warning: Could not initialize schema: %v", err) // Non-fatal = continue } // Handle --reset-admin-password flag. // When this flag is set, we generate a random password, update the database, and exit. if *resetAdminPassword { password := generateRandomPassword(16) hash, err := auth.HashPassword(password) if err != nil { log.Fatalf("Failed to hash password: %v", err) } if err := database.UpsertAdminUser("admin", hash); err != nil { log.Fatalf("Failed to reset admin password: %v", err) } fmt.Printf("Admin password reset successfully.\n") fmt.Printf("Username: admin\n") fmt.Printf("Password: %s\n", password) return } // Start the web server. // From here on, we set up the HTTP router and handlers. // Get the embedded filesystem containing the frontend. // The frontend is compiled into the binary during build time. frontendFS := FrontendFileSystem() // Set Gin to release mode for production. // This disables debug logging and other development features. gin.SetMode(gin.ReleaseMode) // Create a new Gin engine (router). engine := gin.New() engine.Use(gin.Recovery()) // Register API routes from the router. apiRouter := api.New(database, cfg) apiRouter.RegisterRoutes(engine) // Set up SPA fallback for non-API routes. // This must be the last route to catch all unmatched paths. engine.NoRoute(func(c *gin.Context) { path := c.Request.URL.Path // If it's an API path, return 404. // The API router should have handled /api/* routes. if strings.HasPrefix(path, "/api/") { c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) return } // Serve SvelteKit hashed assets at /_app/* if strings.HasPrefix(path, "/_app/") { assetPath := "embed/_app/" + strings.TrimPrefix(path, "/_app/") serveGinStaticFile(c, frontendFS, assetPath) return } // Serve favicon if path == "/favicon.png" { serveGinStaticFile(c, frontendFS, "embed/favicon.png") return } // For all other paths, serve the SPA index.html. // This allows client-side routing (e.g., /domains/example.org). serveGinStaticFile(c, frontendFS, "embed/index.html") }) // Build the address string for binding: "IP:PORT" // Example: "0.0.0.0:8080" or "127.0.0.1:8080" addr := fmt.Sprintf("%s:%s", cfg.Bind, cfg.Port) // Log the startup message. log.Printf("Server starting on http://%s", addr) // Start the HTTP server. // ListenAndServe blocks until the server is stopped. // log.Fatal prints the error and exits if the server can't start. log.Fatal(http.ListenAndServe(addr, engine)) } // serveGinStaticFile serves a static file from the embedded filesystem. func serveGinStaticFile(c *gin.Context, fs http.FileSystem, path string) { file, err := fs.Open(path) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) return } defer file.Close() fi, err := file.Stat() if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) return } if fi.IsDir() { c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) return } contentType := mimeType(path) c.Header("Content-Type", contentType) http.ServeContent(c.Writer, c.Request, fi.Name(), fi.ModTime(), file) } // generateRandomPassword creates a cryptographically secure random password. func generateRandomPassword(length int) string { const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*" result := make([]byte, length) if _, err := rand.Read(result); err != nil { log.Fatalf("Failed to generate random password: %v", err) } for i := range result { result[i] = charset[int(result[i])%len(charset)] } return string(result) } // mimeType returns the MIME type for a file based on its extension. // MIME types tell the browser what kind of file it is. // This is important for browsers to correctly interpret files. func mimeType(path string) string { // Use switch statement with multiple conditions (Go's patterns work like regex). // Check file extension and return appropriate MIME type. switch { case strings.HasSuffix(path, ".html"): return "text/html; charset=utf-8" case strings.HasSuffix(path, ".css"): return "text/css" case strings.HasSuffix(path, ".js"): return "application/javascript" case strings.HasSuffix(path, ".json"): return "application/json" case strings.HasSuffix(path, ".png"): return "image/png" case strings.HasSuffix(path, ".jpg") || strings.HasSuffix(path, ".jpeg"): return "image/jpeg" case strings.HasSuffix(path, ".svg"): return "image/svg+xml" case strings.HasSuffix(path, ".ico"): return "image/x-icon" case strings.HasSuffix(path, ".woff"): return "font/woff" case strings.HasSuffix(path, ".woff2"): return "font/woff2" case strings.HasSuffix(path, ".ttf"): return "font/ttf" case strings.HasSuffix(path, ".eot"): return "application/vnd.ms-fontobject" default: // Fallback for unknown types - treat as plain text. return "text/plain" } }