diff --git a/api/openapi.yaml b/api/openapi.yaml
index 8852c42..e7ca45c 100644
--- a/api/openapi.yaml
+++ b/api/openapi.yaml
@@ -134,6 +134,41 @@ paths:
schema:
$ref: '#/components/schemas/Error'
+ /report/{id}/reanalyze:
+ post:
+ tags:
+ - reports
+ summary: Reanalyze email and regenerate report
+ description: Re-run the analysis on the stored raw email to regenerate the report with the latest analyzer version. This is useful after analyzer improvements or bug fixes.
+ operationId: reanalyzeReport
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ pattern: '^[a-z0-9-]+$'
+ description: Base32-encoded test ID (with hyphens)
+ responses:
+ '200':
+ description: Report regenerated successfully
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Report'
+ '404':
+ description: Email not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ '500':
+ description: Internal server error during reanalysis
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+
/status:
get:
tags:
diff --git a/internal/api/handlers.go b/internal/api/handlers.go
index b53c391..3b57747 100644
--- a/internal/api/handlers.go
+++ b/internal/api/handlers.go
@@ -35,18 +35,26 @@ import (
"git.happydns.org/happyDeliver/internal/utils"
)
+// EmailAnalyzer defines the interface for email analysis
+// This interface breaks the circular dependency with pkg/analyzer
+type EmailAnalyzer interface {
+ AnalyzeEmailBytes(rawEmail []byte, testID uuid.UUID) (reportJSON []byte, err error)
+}
+
// APIHandler implements the ServerInterface for handling API requests
type APIHandler struct {
storage storage.Storage
config *config.Config
+ analyzer EmailAnalyzer
startTime time.Time
}
// NewAPIHandler creates a new API handler
-func NewAPIHandler(store storage.Storage, cfg *config.Config) *APIHandler {
+func NewAPIHandler(store storage.Storage, cfg *config.Config, analyzer EmailAnalyzer) *APIHandler {
return &APIHandler{
storage: store,
config: cfg,
+ analyzer: analyzer,
startTime: time.Now(),
}
}
@@ -192,6 +200,63 @@ func (h *APIHandler) GetRawEmail(c *gin.Context, id string) {
c.Data(http.StatusOK, "text/plain", rawEmail)
}
+// ReanalyzeReport re-analyzes an existing email and regenerates the report
+// (POST /report/{id}/reanalyze)
+func (h *APIHandler) ReanalyzeReport(c *gin.Context, id string) {
+ // Convert base32 ID to UUID
+ testUUID, err := utils.Base32ToUUID(id)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, Error{
+ Error: "invalid_id",
+ Message: "Invalid test ID format",
+ Details: stringPtr(err.Error()),
+ })
+ return
+ }
+
+ // Retrieve the existing report (mainly to get the raw email)
+ _, rawEmail, err := h.storage.GetReport(testUUID)
+ 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 email",
+ Details: stringPtr(err.Error()),
+ })
+ return
+ }
+
+ // Re-analyze the email using the current analyzer
+ reportJSON, err := h.analyzer.AnalyzeEmailBytes(rawEmail, testUUID)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, Error{
+ Error: "analysis_error",
+ Message: "Failed to re-analyze email",
+ Details: stringPtr(err.Error()),
+ })
+ return
+ }
+
+ // Update the report in storage
+ if err := h.storage.UpdateReport(testUUID, reportJSON); err != nil {
+ c.JSON(http.StatusInternalServerError, Error{
+ Error: "internal_error",
+ Message: "Failed to update report",
+ Details: stringPtr(err.Error()),
+ })
+ return
+ }
+
+ // Return the updated report JSON directly
+ c.Data(http.StatusOK, "application/json", reportJSON)
+}
+
// GetStatus retrieves service health status
// (GET /status)
func (h *APIHandler) GetStatus(c *gin.Context) {
diff --git a/internal/app/server.go b/internal/app/server.go
index 332516b..0c70eef 100644
--- a/internal/app/server.go
+++ b/internal/app/server.go
@@ -32,6 +32,7 @@ import (
"git.happydns.org/happyDeliver/internal/config"
"git.happydns.org/happyDeliver/internal/lmtp"
"git.happydns.org/happyDeliver/internal/storage"
+ "git.happydns.org/happyDeliver/pkg/analyzer"
"git.happydns.org/happyDeliver/web"
)
@@ -63,8 +64,11 @@ func RunServer(cfg *config.Config) error {
}
}()
+ // Create analyzer adapter for API
+ analyzerAdapter := analyzer.NewAPIAdapter(cfg)
+
// Create API handler
- handler := api.NewAPIHandler(store, cfg)
+ handler := api.NewAPIHandler(store, cfg, analyzerAdapter)
// Set up Gin router
if os.Getenv("GIN_MODE") == "" {
diff --git a/internal/storage/storage.go b/internal/storage/storage.go
index 7c27279..d8a8cb4 100644
--- a/internal/storage/storage.go
+++ b/internal/storage/storage.go
@@ -43,6 +43,7 @@ type Storage interface {
CreateReport(testID uuid.UUID, rawEmail []byte, reportJSON []byte) (*Report, error)
GetReport(testID uuid.UUID) (reportJSON []byte, rawEmail []byte, err error)
ReportExists(testID uuid.UUID) (bool, error)
+ UpdateReport(testID uuid.UUID, reportJSON []byte) error
DeleteOldReports(olderThan time.Time) (int64, error)
// Close closes the database connection
@@ -117,6 +118,18 @@ func (s *DBStorage) GetReport(testID uuid.UUID) ([]byte, []byte, error) {
return dbReport.ReportJSON, dbReport.RawEmail, nil
}
+// UpdateReport updates the report JSON for an existing test ID
+func (s *DBStorage) UpdateReport(testID uuid.UUID, reportJSON []byte) error {
+ result := s.db.Model(&Report{}).Where("test_id = ?", testID).Update("report_json", reportJSON)
+ if result.Error != nil {
+ return fmt.Errorf("failed to update report: %w", result.Error)
+ }
+ if result.RowsAffected == 0 {
+ return ErrNotFound
+ }
+ return nil
+}
+
// DeleteOldReports deletes reports older than the specified time
func (s *DBStorage) DeleteOldReports(olderThan time.Time) (int64, error) {
result := s.db.Where("created_at < ?", olderThan).Delete(&Report{})
diff --git a/pkg/analyzer/analyzer.go b/pkg/analyzer/analyzer.go
index 3588280..dd082a5 100644
--- a/pkg/analyzer/analyzer.go
+++ b/pkg/analyzer/analyzer.go
@@ -23,6 +23,7 @@ package analyzer
import (
"bytes"
+ "encoding/json"
"fmt"
"github.com/google/uuid"
@@ -85,3 +86,32 @@ func (a *EmailAnalyzer) GetScoreSummaryText(result *AnalysisResult) string {
}
return a.generator.GetScoreSummaryText(result.Results)
}
+
+// APIAdapter adapts the EmailAnalyzer to work with the API package
+// This adapter implements the interface expected by the API handler
+type APIAdapter struct {
+ analyzer *EmailAnalyzer
+}
+
+// NewAPIAdapter creates a new API adapter for the email analyzer
+func NewAPIAdapter(cfg *config.Config) *APIAdapter {
+ return &APIAdapter{
+ analyzer: NewEmailAnalyzer(cfg),
+ }
+}
+
+// AnalyzeEmailBytes performs analysis and returns JSON bytes directly
+func (a *APIAdapter) AnalyzeEmailBytes(rawEmail []byte, testID uuid.UUID) ([]byte, error) {
+ result, err := a.analyzer.AnalyzeEmailBytes(rawEmail, testID)
+ if err != nil {
+ return nil, err
+ }
+
+ // Marshal report to JSON
+ reportJSON, err := json.Marshal(result.Report)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal report: %w", err)
+ }
+
+ return reportJSON, nil
+}
diff --git a/web/src/routes/test/[test]/+page.svelte b/web/src/routes/test/[test]/+page.svelte
index 7672fa8..db3e447 100644
--- a/web/src/routes/test/[test]/+page.svelte
+++ b/web/src/routes/test/[test]/+page.svelte
@@ -1,7 +1,7 @@
@@ -208,9 +227,22 @@
{/if}
-
+
+
Test Another Email