package main import ( "context" "fmt" "log" "net/http" "os" "os/signal" "syscall" "time" "inboxer/src/internal/ai" "inboxer/src/internal/auth" "inboxer/src/internal/db" "inboxer/src/internal/web" "inboxer/src/internal/worker" "inboxer/src/pkg/config" "github.com/gorilla/mux" ) func main() { // Load configuration configPath, err := config.GetDefaultConfigPath() if err != nil { configPath = "bin/config.yaml" } cfg, err := config.LoadConfig(configPath) if err != nil { log.Fatalf("Failed to load configuration: %v", err) } // Initialize database database, err := db.NewDatabase(cfg.Database.Path, cfg.Server.SessionSecret) if err != nil { log.Fatalf("Failed to initialize database: %v", err) } defer database.Close() // Initialize SMTP sender (credentials from config.yaml) smtpConfig := auth.SMTPConfig{ Host: cfg.SMTP.Host, Port: cfg.SMTP.Port, Username: cfg.SMTP.Username, Password: cfg.SMTP.Password, From: cfg.SMTP.Username, // From address matches SMTP username } // Validate SMTP config (but don't fail if not set — user might configure later) if err := smtpConfig.ValidateConfig(); err != nil { log.Printf("Warning: SMTP configuration incomplete: %v", err) log.Println("OTP emails will not be sent until SMTP is configured in config.yaml") } smtpSender := auth.NewSMTPSender(smtpConfig) // Initialize session manager sessionManager := auth.NewSessionManager(cfg.Server.SessionSecret) // In development, allow non-HTTPS if os.Getenv("APP_ENV") == "development" { sessionManager.UpdateSessionOptions(false, 86400*7) } // Initialize OTP store (database-backed) otpStore := db.NewDatabaseOTPStore(database) // Initialize auth service authService := auth.NewAuthService(smtpSender, sessionManager, otpStore) // Initialize AI classifier deepSeekAPI := ai.NewDeepSeekAPI( cfg.AI.APIKey, cfg.AI.Model, cfg.AI.MaxTokens, cfg.AI.Temperature, ) var classifier worker.AIClassifier classifier, err = ai.NewClassifier(deepSeekAPI, cfg.AI.PromptFile) if err != nil { log.Printf("Warning: AI classifier initialization failed: %v", err) log.Println("Falling back to placeholder classifier (all emails -> Other)") classifier = worker.NewPlaceholderClassifier(cfg.Folders.Other) } // Create background worker (not started yet — handlers need a reference first) bgWorker := worker.NewWorker(database, cfg, classifier) // Initialize web handlers (needs worker for ProcessNow) handler, err := web.NewHandler(authService, database, cfg, bgWorker) if err != nil { log.Fatalf("Failed to initialize web handlers: %v", err) } // Setup router router := mux.NewRouter() handler.RegisterRoutes(router) // Add middleware router.Use(loggingMiddleware) router.Use(authMiddleware(authService)) // Create HTTP server server := &http.Server{ Addr: fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port), Handler: router, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, IdleTimeout: 60 * time.Second, } // Start background worker (now that everything is wired up) bgWorker.Start() // Start server in goroutine go func() { log.Printf("Starting server on %s", server.Addr) log.Printf("Access the application at http://localhost:%d", cfg.Server.Port) if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("Server error: %v", err) } }() // Wait for interrupt signal stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt, syscall.SIGTERM) <-stop log.Println("Shutting down...") // Stop background worker first bgWorker.Stop() // Give server time to shutdown gracefully ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if err := server.Shutdown(ctx); err != nil { log.Printf("Server shutdown error: %v", err) } log.Println("Server stopped") } // loggingMiddleware logs HTTP requests func loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() next.ServeHTTP(w, r) log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start)) }) } // authMiddleware checks authentication for protected routes func authMiddleware(authService *auth.AuthService) mux.MiddlewareFunc { protectedPaths := map[string]bool{ "/dashboard": true, "/settings": true, "/logout": true, "/toggle-test-mode": true, "/test-connection": true, "/process-now": true, } return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Check if path is protected if protectedPaths[r.URL.Path] { // Check if user is logged in if !authService.GetSessionManager().IsLoggedIn(r) { http.Redirect(w, r, "/login", http.StatusSeeOther) return } } next.ServeHTTP(w, r) }) } }