// 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 . // // 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 . 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, }) }