// Package core holds OpenMail's domain services: inboxes, messages, threads, // drafts, and the inbound ingest path (parse → thread → store). It is the layer // the API and MCP server call, and it implements mail.InboundSink so any // MailBackend can deliver into it. package core import ( "context" "errors" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "github.com/karti-ai/openmail/internal/store" ) // ErrNotFound is returned when a requested resource does not exist. var ErrNotFound = errors.New("core: not found") type Service struct { pool *pgxpool.Pool } func New(st *store.Store) *Service { return &Service{pool: st.Pool} } // Ping verifies the database connection (used by the health endpoint). func (s *Service) Ping(ctx context.Context) error { return s.pool.Ping(ctx) } // EnsureDefaultPod returns the id of the singleton "default" pod, creating it if // absent. Until DB-backed api_keys carry a pod_id, the API operates within this // one tenant. Idempotent. func (s *Service) EnsureDefaultPod(ctx context.Context) (string, error) { var id string err := s.pool.QueryRow(ctx, `SELECT id::text FROM pods WHERE name = 'default'`).Scan(&id) if err == nil { return id, nil } if !errors.Is(err, pgx.ErrNoRows) { return "", err } err = s.pool.QueryRow(ctx, `INSERT INTO pods (name) VALUES ('default') ON CONFLICT (name) DO NOTHING RETURNING id::text`).Scan(&id) if errors.Is(err, pgx.ErrNoRows) { // Lost a race; read the row the other writer created. err = s.pool.QueryRow(ctx, `SELECT id::text FROM pods WHERE name = 'default'`).Scan(&id) } return id, err } // nullStr maps an optional string to a value usable as a nullable SQL arg. func nullStr(s *string) any { if s == nil { return nil } return *s }