Files
notifier/api/rest/types.go
T
igodwin 71b02758d7 Fix HTML content type handling with case-insensitive validation
- Normalize content_type to lowercase before processing to handle case-insensitive input (e.g., "HTML", "Html")
- Add validation in Validate() to ensure only valid content types are accepted
- Return descriptive error message if invalid content type is provided
- Fix condition in ToNotification() to properly detect and default content type
- Update SMTP notifier auto-detection to work correctly when content type is not explicitly set

Issues fixed:
1. Dynamically detecting content type now works correctly (was always defaulting to "text")
2. Client can now specify content type in request as "HTML", "html", or "Html" - all work
3. Invalid content types are rejected with clear error messages
4. Auto-detection still works if neither explicit type nor valid HTML markers are found

Example scenarios:
- No content_type field: auto-detects based on body (checks for <, <html, <!DOCTYPE, <p>, <div>, <br>)
- content_type: "html": sends as HTML with multipart/alternative
- content_type: "HTML": normalized to "html", sends as HTML
- content_type: "invalid": returns validation error
- content_type: "text": explicitly sends as plain text, skips auto-detection

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-31 01:49:38 -07:00

183 lines
6.6 KiB
Go

package rest
import (
"fmt"
"strings"
"time"
"github.com/google/uuid"
"github.com/igodwin/notifier/internal/domain"
)
// SendNotificationRequest is the REST API request for sending a notification
type SendNotificationRequest struct {
Type string `json:"type"`
Account string `json:"account,omitempty"` // Optional account name for multi-account configs
Priority int `json:"priority,omitempty"`
Subject string `json:"subject"`
Body string `json:"body"`
ContentType string `json:"content_type,omitempty"` // "text" or "html" - auto-detected if not specified
Recipients []string `json:"recipients"`
CC []string `json:"cc,omitempty"` // Carbon copy recipients (email only)
BCC []string `json:"bcc,omitempty"` // Blind carbon copy recipients (email only)
Metadata map[string]interface{} `json:"metadata,omitempty"`
ScheduledFor *time.Time `json:"scheduled_for,omitempty"`
MaxRetries int `json:"max_retries,omitempty"`
}
// Validate validates the request
func (r *SendNotificationRequest) Validate() error {
if r.Type == "" {
return fmt.Errorf("type is required")
}
// For email, allow BCC-only (at least one recipient in To, CC, or BCC)
// For other types, require Recipients
totalRecipients := len(r.Recipients) + len(r.CC) + len(r.BCC)
if totalRecipients == 0 {
return fmt.Errorf("at least one recipient is required (recipients, cc, or bcc)")
}
if r.Body == "" {
return fmt.Errorf("body is required")
}
// Validate content type if specified (must be "text" or "html", case-insensitive)
if r.ContentType != "" {
contentTypeLower := strings.ToLower(r.ContentType)
if contentTypeLower != "text" && contentTypeLower != "html" {
return fmt.Errorf("invalid content_type: must be 'text' or 'html' (got %q)", r.ContentType)
}
}
return nil
}
// ToNotification converts the request to a domain notification
func (r *SendNotificationRequest) ToNotification() *domain.Notification {
maxRetries := r.MaxRetries
if maxRetries == 0 {
maxRetries = 3 // Default
}
// Convert content type, defaulting to text
// Normalize to lowercase to handle case-insensitive input (e.g., "HTML" -> "html")
contentType := domain.ContentType(strings.ToLower(r.ContentType))
if contentType == "" || contentType != domain.ContentTypeHTML {
contentType = domain.ContentTypeText
}
return &domain.Notification{
ID: uuid.New().String(),
Type: domain.NotificationType(r.Type),
Account: r.Account,
Priority: domain.Priority(r.Priority),
Status: domain.StatusPending,
Subject: r.Subject,
Body: r.Body,
ContentType: contentType,
Recipients: r.Recipients,
CC: r.CC,
BCC: r.BCC,
Metadata: r.Metadata,
CreatedAt: time.Now(),
ScheduledFor: r.ScheduledFor,
MaxRetries: maxRetries,
RetryCount: 0,
}
}
// SendNotificationResponse is the REST API response for sending a notification
type SendNotificationResponse struct {
Result NotificationResult `json:"result"`
}
// SendBatchNotificationsRequest is the REST API request for sending multiple notifications
type SendBatchNotificationsRequest struct {
Notifications []SendNotificationRequest `json:"notifications"`
}
// SendBatchNotificationsResponse is the REST API response for sending multiple notifications
type SendBatchNotificationsResponse struct {
Results []NotificationResult `json:"results"`
}
// Notification represents a notification in the REST API
type Notification struct {
ID string `json:"id"`
Type string `json:"type"`
Account string `json:"account,omitempty"`
Priority int `json:"priority"`
Status string `json:"status"`
Subject string `json:"subject"`
Body string `json:"body"`
ContentType string `json:"content_type,omitempty"`
Recipients []string `json:"recipients"`
CC []string `json:"cc,omitempty"`
BCC []string `json:"bcc,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
CreatedAt time.Time `json:"created_at"`
ScheduledFor *time.Time `json:"scheduled_for,omitempty"`
SentAt *time.Time `json:"sent_at,omitempty"`
RetryCount int `json:"retry_count"`
MaxRetries int `json:"max_retries"`
LastError string `json:"last_error,omitempty"`
}
// NotificationFromDomain converts a domain notification to API format
func NotificationFromDomain(n *domain.Notification) Notification {
return Notification{
ID: n.ID,
Type: string(n.Type),
Account: n.Account,
Priority: int(n.Priority),
Status: string(n.Status),
Subject: n.Subject,
Body: n.Body,
ContentType: string(n.ContentType),
Recipients: n.Recipients,
CC: n.CC,
BCC: n.BCC,
Metadata: n.Metadata,
CreatedAt: n.CreatedAt,
ScheduledFor: n.ScheduledFor,
SentAt: n.SentAt,
RetryCount: n.RetryCount,
MaxRetries: n.MaxRetries,
LastError: n.LastError,
}
}
// NotificationResult represents the result of a notification operation
type NotificationResult struct {
NotificationID string `json:"notification_id"`
Success bool `json:"success"`
Message string `json:"message,omitempty"`
Error string `json:"error,omitempty"`
SentAt time.Time `json:"sent_at"`
ProviderResponse map[string]interface{} `json:"provider_response,omitempty"`
}
// NotificationResultFromDomain converts a domain result to API format
func NotificationResultFromDomain(r *domain.NotificationResult) NotificationResult {
return NotificationResult{
NotificationID: r.NotificationID,
Success: r.Success,
Message: r.Message,
Error: r.Error,
SentAt: r.SentAt,
ProviderResponse: r.ProviderResponse,
}
}
// ListNotificationsResponse is the REST API response for listing notifications
type ListNotificationsResponse struct {
Notifications []Notification `json:"notifications"`
Total int64 `json:"total"`
}
// RetryNotificationResponse is the REST API response for retrying a notification
type RetryNotificationResponse struct {
Result NotificationResult `json:"result"`
}