// Package api is OpenMail's agent-facing HTTP surface: the AgentMail-shaped v0 // REST API (see ARCHITECTURE.md §4). package api import ( "encoding/json" "net/http" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/karti-ai/openmail/internal/config" "github.com/karti-ai/openmail/internal/core" "github.com/karti-ai/openmail/internal/mail" ) type Server struct { cfg config.Config core *core.Service // nil when DATABASE_URL is unset backend mail.MailBackend // never nil (NullBackend by default) podID string // default pod, resolved at startup } func New(cfg config.Config, svc *core.Service, backend mail.MailBackend, podID string) *Server { return &Server{cfg: cfg, core: svc, backend: backend, podID: podID} } func (s *Server) Router() http.Handler { r := chi.NewRouter() r.Use(middleware.RequestID) r.Use(middleware.RealIP) r.Use(middleware.Recoverer) r.Get("/healthz", s.handleHealth) r.Group(func(r chi.Router) { r.Use(s.bearerAuth) r.Route("/v0/inboxes", func(r chi.Router) { r.Post("/", s.createInbox) r.Get("/", s.listInboxes) r.Get("/{id}", s.getInbox) r.Post("/{id}/ingest", s.ingest) // seed/inbound path (milestone 1) r.Post("/{id}/messages/send", s.sendMessage) r.Get("/{id}/messages", s.listMessages) r.Get("/{id}/messages/{msgID}", s.getMessage) r.Post("/{id}/messages/{msgID}/reply", s.replyMessage) r.Get("/{id}/threads", s.listThreads) r.Get("/{id}/threads/{threadID}", s.getThread) }) }) return r } func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) { status := map[string]any{"status": "ok", "backend": backendName(s.backend)} if s.core != nil { if err := s.core.Ping(r.Context()); err != nil { // /healthz is unauthenticated — don't leak DSN/host/internal details. status["status"] = "degraded" status["db"] = "error" writeJSON(w, http.StatusServiceUnavailable, status) return } status["db"] = "ok" } else { status["db"] = "not configured" } writeJSON(w, http.StatusOK, status) } func backendName(b mail.MailBackend) string { if _, ok := b.(mail.NullBackend); ok { return "null" } return "configured" } // requireCore guards handlers that need the database. Returns false (and writes // a 503) when the store is unconfigured. func (s *Server) requireCore(w http.ResponseWriter) bool { if s.core == nil { writeJSON(w, http.StatusServiceUnavailable, map[string]string{ "error": "db_unconfigured", "message": "DATABASE_URL not set", }) return false } return true } func writeJSON(w http.ResponseWriter, code int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(code) _ = json.NewEncoder(w).Encode(v) } func writeErr(w http.ResponseWriter, code int, errCode, msg string) { writeJSON(w, code, map[string]string{"error": errCode, "message": msg}) }