// Command email-agent runs the Email Agent HTTP service. package main import ( "context" "flag" "fmt" "log/slog" "net/http" "os" "os/signal" "syscall" "time" "gitea.lab.audasmedia.com.au/sam/email_agent/internal/config" "gitea.lab.audasmedia.com.au/sam/email_agent/internal/server" "gitea.lab.audasmedia.com.au/sam/email_agent/internal/store" "gitea.lab.audasmedia.com.au/sam/email_agent/internal/version" ) func main() { var ( showVersion = flag.Bool("version", false, "print version information and exit") healthcheck = flag.Bool("healthcheck", false, "probe the local /health endpoint and exit") ) flag.Parse() if *showVersion { fmt.Printf("email-agent %s (commit %s)\n", version.Version, version.Commit) return } cfg, err := config.Load() if err != nil { fmt.Fprintf(os.Stderr, "config error: %v\n", err) os.Exit(1) } logger := newLogger(cfg.LogLevel) slog.SetDefault(logger) if *healthcheck { if err := probeHealth(cfg.Port); err != nil { fmt.Fprintf(os.Stderr, "healthcheck failed: %v\n", err) os.Exit(1) } return } if err := run(cfg, logger); err != nil { logger.Error("fatal", "error", err) os.Exit(1) } } func run(cfg *config.Config, logger *slog.Logger) error { logger.Info("starting email-agent", "version", version.Version, "commit", version.Commit, "env", cfg.Env, ) if cfg.SessionSecret == "" && cfg.Password == "" { logger.Warn("no EMAIL_AGENT_PASSWORD or EMAIL_AGENT_SESSION_SECRET set; login is disabled until one is configured") } ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() st, err := store.Open(ctx, cfg.DBPath) if err != nil { return fmt.Errorf("open store: %w", err) } defer func() { if cerr := st.Close(); cerr != nil { logger.Error("close store", "error", cerr) } }() schemaVersion, err := st.SchemaVersion(ctx) if err != nil { return fmt.Errorf("read schema version: %w", err) } logger.Info("database ready", "path", st.Path(), "schema_version", schemaVersion) srv, err := server.New(cfg, st, logger) if err != nil { return fmt.Errorf("build server: %w", err) } return srv.Run(ctx) } // probeHealth requests the local health endpoint. It exists so the container // healthcheck does not need an external HTTP client in the image. func probeHealth(port int) error { client := &http.Client{Timeout: 3 * time.Second} url := fmt.Sprintf("http://127.0.0.1:%d/health", port) resp, err := client.Get(url) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("unexpected status %d from %s", resp.StatusCode, url) } return nil } func newLogger(level string) *slog.Logger { var lvl slog.Level if err := lvl.UnmarshalText([]byte(level)); err != nil { lvl = slog.LevelInfo } handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: lvl}) return slog.New(handler) }