package internalhttp import ( "encoding/base64" "errors" "fmt" "net/http" "strconv" "strings" "time" "galaxy/mail/internal/domain/attempt" "galaxy/mail/internal/domain/common" deliverydomain "galaxy/mail/internal/domain/delivery" "galaxy/mail/internal/service/listdeliveries" ) const ( // ErrorCodeDeliveryNotFound identifies a missing trusted delivery lookup // target. ErrorCodeDeliveryNotFound = "delivery_not_found" // ErrorCodeResendNotAllowed identifies resend requests against non-terminal // deliveries. ErrorCodeResendNotAllowed = "resend_not_allowed" deliveryIDPathValue = "delivery_id" ) // DeliveryListQuery stores the raw trusted query-string values accepted by the // operator delivery-list route before normalization. type DeliveryListQuery struct { // Recipient stores the optional recipient filter covering `to`, `cc`, and // `bcc`. Recipient string // Status stores the optional delivery-status filter. Status string // Source stores the optional delivery-source filter. Source string // TemplateID stores the optional template-family filter. TemplateID string // IdempotencyKey stores the optional idempotency-key filter. IdempotencyKey string // FromCreatedAtMS stores the optional inclusive lower creation-time bound. FromCreatedAtMS string // ToCreatedAtMS stores the optional inclusive upper creation-time bound. ToCreatedAtMS string // Limit stores the optional page size. Limit string // Cursor stores the optional opaque continuation cursor. Cursor string } // DeliverySummaryResponse stores one brief operator-facing delivery record. type DeliverySummaryResponse struct { DeliveryID string `json:"delivery_id"` ResendParentDeliveryID string `json:"resend_parent_delivery_id,omitempty"` Source string `json:"source"` PayloadMode string `json:"payload_mode"` TemplateID string `json:"template_id,omitempty"` To []string `json:"to"` Cc []string `json:"cc"` Bcc []string `json:"bcc"` ReplyTo []string `json:"reply_to"` Locale string `json:"locale,omitempty"` LocaleFallbackUsed bool `json:"locale_fallback_used"` IdempotencyKey string `json:"idempotency_key"` Status string `json:"status"` AttemptCount int `json:"attempt_count"` LastAttemptStatus string `json:"last_attempt_status,omitempty"` ProviderSummary string `json:"provider_summary,omitempty"` CreatedAtMS int64 `json:"created_at_ms"` UpdatedAtMS int64 `json:"updated_at_ms"` SentAtMS *int64 `json:"sent_at_ms,omitempty"` SuppressedAtMS *int64 `json:"suppressed_at_ms,omitempty"` FailedAtMS *int64 `json:"failed_at_ms,omitempty"` DeadLetteredAtMS *int64 `json:"dead_lettered_at_ms,omitempty"` } // DeliveryListResponse stores one deterministic page of brief delivery // summaries. type DeliveryListResponse struct { Items []DeliverySummaryResponse `json:"items"` NextCursor string `json:"next_cursor,omitempty"` } // AttachmentResponse stores one durable attachment audit record. type AttachmentResponse struct { Filename string `json:"filename"` ContentType string `json:"content_type"` SizeBytes int64 `json:"size_bytes"` } // DeadLetterResponse stores one operator-visible dead-letter entry. type DeadLetterResponse struct { DeliveryID string `json:"delivery_id"` FinalAttemptNo int `json:"final_attempt_no"` FailureClassification string `json:"failure_classification"` ProviderSummary string `json:"provider_summary,omitempty"` CreatedAtMS int64 `json:"created_at_ms"` RecoveryHint string `json:"recovery_hint,omitempty"` } // DeliveryDetailResponse stores one full operator-facing delivery view. type DeliveryDetailResponse struct { DeliveryID string `json:"delivery_id"` ResendParentDeliveryID string `json:"resend_parent_delivery_id,omitempty"` Source string `json:"source"` PayloadMode string `json:"payload_mode"` TemplateID string `json:"template_id,omitempty"` TemplateVariables map[string]any `json:"template_variables,omitempty"` To []string `json:"to"` Cc []string `json:"cc"` Bcc []string `json:"bcc"` ReplyTo []string `json:"reply_to"` Subject string `json:"subject,omitempty"` TextBody string `json:"text_body,omitempty"` HTMLBody string `json:"html_body,omitempty"` Attachments []AttachmentResponse `json:"attachments"` Locale string `json:"locale,omitempty"` LocaleFallbackUsed bool `json:"locale_fallback_used"` IdempotencyKey string `json:"idempotency_key"` Status string `json:"status"` AttemptCount int `json:"attempt_count"` LastAttemptStatus string `json:"last_attempt_status,omitempty"` ProviderSummary string `json:"provider_summary,omitempty"` CreatedAtMS int64 `json:"created_at_ms"` UpdatedAtMS int64 `json:"updated_at_ms"` SentAtMS *int64 `json:"sent_at_ms,omitempty"` SuppressedAtMS *int64 `json:"suppressed_at_ms,omitempty"` FailedAtMS *int64 `json:"failed_at_ms,omitempty"` DeadLetteredAtMS *int64 `json:"dead_lettered_at_ms,omitempty"` DeadLetter *DeadLetterResponse `json:"dead_letter,omitempty"` } // AttemptResponse stores one operator-facing delivery-attempt record. type AttemptResponse struct { DeliveryID string `json:"delivery_id"` AttemptNo int `json:"attempt_no"` ScheduledForMS int64 `json:"scheduled_for_ms"` StartedAtMS *int64 `json:"started_at_ms,omitempty"` FinishedAtMS *int64 `json:"finished_at_ms,omitempty"` Status string `json:"status"` ProviderClassification string `json:"provider_classification,omitempty"` ProviderSummary string `json:"provider_summary,omitempty"` } // DeliveryAttemptsResponse stores the attempt history of one accepted // delivery. type DeliveryAttemptsResponse struct { Items []AttemptResponse `json:"items"` } // DeliveryResendResponse stores the identifier of the clone delivery created // by one resend request. type DeliveryResendResponse struct { DeliveryID string `json:"delivery_id"` } // DecodeDeliveryListInput validates one trusted operator delivery-list // request and returns the normalized list input. func DecodeDeliveryListInput(request *http.Request) (listdeliveries.Input, error) { if request == nil { return listdeliveries.Input{}, errors.New("delivery list request must not be nil") } query, err := decodeDeliveryListQuery(request) if err != nil { return listdeliveries.Input{}, err } input, err := query.Normalize() if err != nil { return listdeliveries.Input{}, err } return input, nil } // DecodeDeliveryIDFromPath validates one trusted path delivery identifier. func DecodeDeliveryIDFromPath(request *http.Request) (common.DeliveryID, error) { if request == nil { return "", errors.New("delivery lookup request must not be nil") } return parseDeliveryID(request.PathValue(deliveryIDPathValue)) } // EncodeDeliveryListCursor encodes cursor into the frozen opaque base64url // format `created_at_ms:delivery_id`. func EncodeDeliveryListCursor(cursor listdeliveries.Cursor) (string, error) { if err := cursor.Validate(); err != nil { return "", fmt.Errorf("encode delivery list cursor: %w", err) } payload := fmt.Sprintf("%d:%s", cursor.CreatedAt.UTC().UnixMilli(), cursor.DeliveryID.String()) return base64.RawURLEncoding.EncodeToString([]byte(payload)), nil } // DecodeDeliveryListCursor decodes raw from the frozen opaque cursor format. func DecodeDeliveryListCursor(raw string) (listdeliveries.Cursor, error) { if strings.TrimSpace(raw) == "" { return listdeliveries.Cursor{}, errors.New("cursor must not be empty") } if strings.TrimSpace(raw) != raw { return listdeliveries.Cursor{}, errors.New("cursor must not contain surrounding whitespace") } payload, err := base64.RawURLEncoding.DecodeString(raw) if err != nil { return listdeliveries.Cursor{}, fmt.Errorf("decode cursor: %w", err) } createdAtRaw, deliveryIDRaw, ok := strings.Cut(string(payload), ":") if !ok { return listdeliveries.Cursor{}, errors.New("decode cursor: invalid cursor payload") } createdAtMS, err := strconv.ParseInt(createdAtRaw, 10, 64) if err != nil { return listdeliveries.Cursor{}, fmt.Errorf("decode cursor created_at_ms: %w", err) } cursor := listdeliveries.Cursor{ CreatedAt: time.UnixMilli(createdAtMS).UTC(), DeliveryID: common.DeliveryID(deliveryIDRaw), } if err := cursor.Validate(); err != nil { return listdeliveries.Cursor{}, fmt.Errorf("decode cursor: %w", err) } return cursor, nil } // Normalize converts the raw trusted query-string shape into the operator list // input consumed by the service layer. func (query DeliveryListQuery) Normalize() (listdeliveries.Input, error) { var input listdeliveries.Input recipient, err := parseOptionalEmail(query.Recipient, "recipient") if err != nil { return listdeliveries.Input{}, err } status, err := parseOptionalStatus(query.Status) if err != nil { return listdeliveries.Input{}, err } source, err := parseOptionalSource(query.Source) if err != nil { return listdeliveries.Input{}, err } templateID, err := parseOptionalTemplateID(query.TemplateID) if err != nil { return listdeliveries.Input{}, err } idempotencyKey, err := parseOptionalIdempotencyKey(query.IdempotencyKey) if err != nil { return listdeliveries.Input{}, err } fromCreatedAt, err := parseOptionalUnixMilli(query.FromCreatedAtMS, "from_created_at_ms") if err != nil { return listdeliveries.Input{}, err } toCreatedAt, err := parseOptionalUnixMilli(query.ToCreatedAtMS, "to_created_at_ms") if err != nil { return listdeliveries.Input{}, err } limit, err := parseOptionalLimit(query.Limit) if err != nil { return listdeliveries.Input{}, err } cursor, err := parseOptionalCursor(query.Cursor) if err != nil { return listdeliveries.Input{}, err } input = listdeliveries.Input{ Limit: limit, Cursor: cursor, Filters: listdeliveries.Filters{ Recipient: recipient, Status: status, Source: source, TemplateID: templateID, IdempotencyKey: idempotencyKey, FromCreatedAt: fromCreatedAt, ToCreatedAt: toCreatedAt, }, } if err := input.Validate(); err != nil { return listdeliveries.Input{}, err } return input, nil } func decodeDeliveryListQuery(request *http.Request) (DeliveryListQuery, error) { values := request.URL.Query() recipient, err := singleQueryValue(values, "recipient") if err != nil { return DeliveryListQuery{}, err } status, err := singleQueryValue(values, "status") if err != nil { return DeliveryListQuery{}, err } source, err := singleQueryValue(values, "source") if err != nil { return DeliveryListQuery{}, err } templateID, err := singleQueryValue(values, "template_id") if err != nil { return DeliveryListQuery{}, err } idempotencyKey, err := singleQueryValue(values, "idempotency_key") if err != nil { return DeliveryListQuery{}, err } fromCreatedAtMS, err := singleQueryValue(values, "from_created_at_ms") if err != nil { return DeliveryListQuery{}, err } toCreatedAtMS, err := singleQueryValue(values, "to_created_at_ms") if err != nil { return DeliveryListQuery{}, err } limit, err := singleQueryValue(values, "limit") if err != nil { return DeliveryListQuery{}, err } cursor, err := singleQueryValue(values, "cursor") if err != nil { return DeliveryListQuery{}, err } return DeliveryListQuery{ Recipient: recipient, Status: status, Source: source, TemplateID: templateID, IdempotencyKey: idempotencyKey, FromCreatedAtMS: fromCreatedAtMS, ToCreatedAtMS: toCreatedAtMS, Limit: limit, Cursor: cursor, }, nil } func singleQueryValue(values map[string][]string, key string) (string, error) { rawValues := values[key] switch len(rawValues) { case 0: return "", nil case 1: return rawValues[0], nil default: return "", fmt.Errorf("query parameter %q must appear at most once", key) } } func parseDeliveryID(raw string) (common.DeliveryID, error) { deliveryID := common.DeliveryID(raw) if err := deliveryID.Validate(); err != nil { return "", fmt.Errorf("delivery id: %w", err) } return deliveryID, nil } func parseOptionalEmail(raw string, name string) (common.Email, error) { if raw == "" { return "", nil } email := common.Email(strings.TrimSpace(raw)) if err := email.Validate(); err != nil { return "", fmt.Errorf("%s: %w", name, err) } return email, nil } func parseOptionalStatus(raw string) (deliverydomain.Status, error) { if raw == "" { return "", nil } status := deliverydomain.Status(strings.TrimSpace(raw)) if !status.IsKnown() { return "", fmt.Errorf("status %q is unsupported", raw) } return status, nil } func parseOptionalSource(raw string) (deliverydomain.Source, error) { if raw == "" { return "", nil } source := deliverydomain.Source(strings.TrimSpace(raw)) if !source.IsKnown() { return "", fmt.Errorf("source %q is unsupported", raw) } return source, nil } func parseOptionalTemplateID(raw string) (common.TemplateID, error) { if raw == "" { return "", nil } templateID := common.TemplateID(strings.TrimSpace(raw)) if err := templateID.Validate(); err != nil { return "", fmt.Errorf("template id: %w", err) } return templateID, nil } func parseOptionalIdempotencyKey(raw string) (common.IdempotencyKey, error) { if raw == "" { return "", nil } key := common.IdempotencyKey(strings.TrimSpace(raw)) if err := key.Validate(); err != nil { return "", fmt.Errorf("idempotency key: %w", err) } return key, nil } func parseOptionalUnixMilli(raw string, name string) (*time.Time, error) { if raw == "" { return nil, nil } value, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64) if err != nil { return nil, fmt.Errorf("%s: %w", name, err) } timestamp := time.UnixMilli(value).UTC() if err := common.ValidateTimestamp(name, timestamp); err != nil { return nil, err } return ×tamp, nil } func parseOptionalLimit(raw string) (int, error) { if raw == "" { return 0, nil } value, err := strconv.Atoi(strings.TrimSpace(raw)) if err != nil { return 0, fmt.Errorf("limit: %w", err) } if value < 1 { return 0, errors.New("limit must be at least 1") } return value, nil } func parseOptionalCursor(raw string) (*listdeliveries.Cursor, error) { if raw == "" { return nil, nil } cursor, err := DecodeDeliveryListCursor(raw) if err != nil { return nil, err } return &cursor, nil } func summaryResponseFromDelivery(record deliverydomain.Delivery) DeliverySummaryResponse { return DeliverySummaryResponse{ DeliveryID: record.DeliveryID.String(), ResendParentDeliveryID: record.ResendParentDeliveryID.String(), Source: string(record.Source), PayloadMode: string(record.PayloadMode), TemplateID: record.TemplateID.String(), To: emailStrings(record.Envelope.To), Cc: emailStrings(record.Envelope.Cc), Bcc: emailStrings(record.Envelope.Bcc), ReplyTo: emailStrings(record.Envelope.ReplyTo), Locale: record.Locale.String(), LocaleFallbackUsed: record.LocaleFallbackUsed, IdempotencyKey: record.IdempotencyKey.String(), Status: string(record.Status), AttemptCount: record.AttemptCount, LastAttemptStatus: string(record.LastAttemptStatus), ProviderSummary: record.ProviderSummary, CreatedAtMS: record.CreatedAt.UTC().UnixMilli(), UpdatedAtMS: record.UpdatedAt.UTC().UnixMilli(), SentAtMS: unixMilliPtr(record.SentAt), SuppressedAtMS: unixMilliPtr(record.SuppressedAt), FailedAtMS: unixMilliPtr(record.FailedAt), DeadLetteredAtMS: unixMilliPtr(record.DeadLetteredAt), } } func detailResponseFromDelivery(record deliverydomain.Delivery, deadLetter *deliverydomain.DeadLetterEntry) DeliveryDetailResponse { response := DeliveryDetailResponse{ DeliveryID: record.DeliveryID.String(), ResendParentDeliveryID: record.ResendParentDeliveryID.String(), Source: string(record.Source), PayloadMode: string(record.PayloadMode), TemplateID: record.TemplateID.String(), TemplateVariables: cloneJSONObject(record.TemplateVariables), To: emailStrings(record.Envelope.To), Cc: emailStrings(record.Envelope.Cc), Bcc: emailStrings(record.Envelope.Bcc), ReplyTo: emailStrings(record.Envelope.ReplyTo), Subject: record.Content.Subject, TextBody: record.Content.TextBody, HTMLBody: record.Content.HTMLBody, Attachments: attachmentResponses(record.Attachments), Locale: record.Locale.String(), LocaleFallbackUsed: record.LocaleFallbackUsed, IdempotencyKey: record.IdempotencyKey.String(), Status: string(record.Status), AttemptCount: record.AttemptCount, LastAttemptStatus: string(record.LastAttemptStatus), ProviderSummary: record.ProviderSummary, CreatedAtMS: record.CreatedAt.UTC().UnixMilli(), UpdatedAtMS: record.UpdatedAt.UTC().UnixMilli(), SentAtMS: unixMilliPtr(record.SentAt), SuppressedAtMS: unixMilliPtr(record.SuppressedAt), FailedAtMS: unixMilliPtr(record.FailedAt), DeadLetteredAtMS: unixMilliPtr(record.DeadLetteredAt), } if deadLetter != nil { response.DeadLetter = &DeadLetterResponse{ DeliveryID: deadLetter.DeliveryID.String(), FinalAttemptNo: deadLetter.FinalAttemptNo, FailureClassification: deadLetter.FailureClassification, ProviderSummary: deadLetter.ProviderSummary, CreatedAtMS: deadLetter.CreatedAt.UTC().UnixMilli(), RecoveryHint: deadLetter.RecoveryHint, } } return response } func attemptResponseFromRecord(record attempt.Attempt) AttemptResponse { return AttemptResponse{ DeliveryID: record.DeliveryID.String(), AttemptNo: record.AttemptNo, ScheduledForMS: record.ScheduledFor.UTC().UnixMilli(), StartedAtMS: unixMilliPtr(record.StartedAt), FinishedAtMS: unixMilliPtr(record.FinishedAt), Status: string(record.Status), ProviderClassification: record.ProviderClassification, ProviderSummary: record.ProviderSummary, } } func attachmentResponses(attachments []common.AttachmentMetadata) []AttachmentResponse { if len(attachments) == 0 { return []AttachmentResponse{} } result := make([]AttachmentResponse, len(attachments)) for index, attachment := range attachments { result[index] = AttachmentResponse{ Filename: attachment.Filename, ContentType: attachment.ContentType, SizeBytes: attachment.SizeBytes, } } return result } func emailStrings(values []common.Email) []string { if len(values) == 0 { return []string{} } result := make([]string, len(values)) for index, value := range values { result[index] = value.String() } return result } func unixMilliPtr(value *time.Time) *int64 { if value == nil { return nil } encoded := value.UTC().UnixMilli() return &encoded } func cloneJSONObject(value map[string]any) map[string]any { if value == nil { return nil } cloned := make(map[string]any, len(value)) for key, entry := range value { cloned[key] = entry } return cloned }