happyDeliver/internal/api/handlers.go

215 lines
5.6 KiB
Go

// This file is part of the happyDeliver (R) project.
// Copyright (c) 2025 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package api
import (
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
openapi_types "github.com/oapi-codegen/runtime/types"
"git.happydns.org/happyDeliver/internal/config"
"git.happydns.org/happyDeliver/internal/storage"
)
// APIHandler implements the ServerInterface for handling API requests
type APIHandler struct {
storage storage.Storage
config *config.Config
startTime time.Time
}
// NewAPIHandler creates a new API handler
func NewAPIHandler(store storage.Storage, cfg *config.Config) *APIHandler {
return &APIHandler{
storage: store,
config: cfg,
startTime: time.Now(),
}
}
// CreateTest creates a new deliverability test
// (POST /test)
func (h *APIHandler) CreateTest(c *gin.Context) {
// Generate a unique test ID
testID := uuid.New()
// Generate test email address
email := fmt.Sprintf("%s%s@%s",
h.config.Email.TestAddressPrefix,
testID.String(),
h.config.Email.Domain,
)
// Create test in database
test, err := h.storage.CreateTest(testID)
if err != nil {
c.JSON(http.StatusInternalServerError, Error{
Error: "internal_error",
Message: "Failed to create test",
Details: stringPtr(err.Error()),
})
return
}
// Return response
c.JSON(http.StatusCreated, TestResponse{
Id: test.ID,
Email: openapi_types.Email(email),
Status: TestResponseStatusPending,
Message: stringPtr("Send your test email to the address above"),
})
}
// GetTest retrieves test metadata
// (GET /test/{id})
func (h *APIHandler) GetTest(c *gin.Context, id openapi_types.UUID) {
test, err := h.storage.GetTest(id)
if err != nil {
if err == storage.ErrNotFound {
c.JSON(http.StatusNotFound, Error{
Error: "not_found",
Message: "Test not found",
})
return
}
c.JSON(http.StatusInternalServerError, Error{
Error: "internal_error",
Message: "Failed to retrieve test",
Details: stringPtr(err.Error()),
})
return
}
// Convert storage status to API status
var apiStatus TestStatus
switch test.Status {
case storage.StatusPending:
apiStatus = TestStatusPending
case storage.StatusReceived:
apiStatus = TestStatusReceived
case storage.StatusAnalyzed:
apiStatus = TestStatusAnalyzed
case storage.StatusFailed:
apiStatus = TestStatusFailed
default:
apiStatus = TestStatusPending
}
// Generate test email address
email := fmt.Sprintf("%s%s@%s",
h.config.Email.TestAddressPrefix,
test.ID.String(),
h.config.Email.Domain,
)
c.JSON(http.StatusOK, Test{
Id: test.ID,
Email: openapi_types.Email(email),
Status: apiStatus,
CreatedAt: test.CreatedAt,
UpdatedAt: &test.UpdatedAt,
})
}
// GetReport retrieves the detailed analysis report
// (GET /report/{id})
func (h *APIHandler) GetReport(c *gin.Context, id openapi_types.UUID) {
reportJSON, _, err := h.storage.GetReport(id)
if err != nil {
if err == storage.ErrNotFound {
c.JSON(http.StatusNotFound, Error{
Error: "not_found",
Message: "Report not found",
})
return
}
c.JSON(http.StatusInternalServerError, Error{
Error: "internal_error",
Message: "Failed to retrieve report",
Details: stringPtr(err.Error()),
})
return
}
// Return raw JSON directly
c.Data(http.StatusOK, "application/json", reportJSON)
}
// GetRawEmail retrieves the raw annotated email
// (GET /report/{id}/raw)
func (h *APIHandler) GetRawEmail(c *gin.Context, id openapi_types.UUID) {
_, rawEmail, err := h.storage.GetReport(id)
if err != nil {
if err == storage.ErrNotFound {
c.JSON(http.StatusNotFound, Error{
Error: "not_found",
Message: "Email not found",
})
return
}
c.JSON(http.StatusInternalServerError, Error{
Error: "internal_error",
Message: "Failed to retrieve raw email",
Details: stringPtr(err.Error()),
})
return
}
c.Data(http.StatusOK, "text/plain", rawEmail)
}
// GetStatus retrieves service health status
// (GET /status)
func (h *APIHandler) GetStatus(c *gin.Context) {
// Calculate uptime
uptime := int(time.Since(h.startTime).Seconds())
// Check database connectivity
dbStatus := StatusComponentsDatabaseUp
if _, err := h.storage.GetTest(uuid.New()); err != nil && err != storage.ErrNotFound {
dbStatus = StatusComponentsDatabaseDown
}
// Determine overall status
overallStatus := Healthy
if dbStatus == StatusComponentsDatabaseDown {
overallStatus = Unhealthy
}
mtaStatus := StatusComponentsMtaUp
c.JSON(http.StatusOK, Status{
Status: overallStatus,
Version: "0.1.0-dev",
Components: &struct {
Database *StatusComponentsDatabase `json:"database,omitempty"`
Mta *StatusComponentsMta `json:"mta,omitempty"`
}{
Database: &dbStatus,
Mta: &mtaStatus,
},
Uptime: &uptime,
})
}