// Package acceptgenericdelivery implements durable asynchronous acceptance of // generic delivery commands consumed from Redis Streams. package acceptgenericdelivery import ( "context" "encoding/base64" "errors" "fmt" "log/slog" "strings" "time" "galaxy/mail/internal/api/streamcommand" "galaxy/mail/internal/domain/attempt" "galaxy/mail/internal/domain/common" deliverydomain "galaxy/mail/internal/domain/delivery" "galaxy/mail/internal/domain/idempotency" "galaxy/mail/internal/logging" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" oteltrace "go.opentelemetry.io/otel/trace" ) var ( // ErrConflict reports that the idempotency scope already belongs to a // different normalized generic request. ErrConflict = errors.New("accept generic delivery conflict") // ErrServiceUnavailable reports that durable generic acceptance could not // be completed or recovered safely. ErrServiceUnavailable = errors.New("accept generic delivery service unavailable") ) const tracerName = "galaxy/mail/acceptgenericdelivery" // Outcome identifies the coarse generic-delivery acceptance outcome. type Outcome string const ( // OutcomeAccepted reports that the command was durably accepted into the // internal delivery pipeline. OutcomeAccepted Outcome = "accepted" // OutcomeDuplicate reports that the command matched an already accepted // idempotent request and therefore became a no-op replay. OutcomeDuplicate Outcome = "duplicate" ) // IsKnown reports whether outcome belongs to the supported generic-acceptance // outcome surface. func (outcome Outcome) IsKnown() bool { switch outcome { case OutcomeAccepted, OutcomeDuplicate: return true default: return false } } // Result stores the coarse generic-delivery acceptance outcome. type Result struct { // Outcome stores the stable generic-acceptance result. Outcome Outcome } // Validate reports whether result contains a supported generic-acceptance // outcome. func (result Result) Validate() error { if !result.Outcome.IsKnown() { return fmt.Errorf("accept generic delivery outcome %q is unsupported", result.Outcome) } return nil } // AttachmentPayload stores one durably persisted raw attachment payload owned // by a generic delivery. type AttachmentPayload struct { // Filename stores the user-facing attachment filename. Filename string // ContentType stores the MIME media type used for SMTP body construction. ContentType string // ContentBase64 stores the exact accepted inline base64 payload. ContentBase64 string // SizeBytes stores the decoded attachment size in bytes. SizeBytes int64 } // Validate reports whether payload contains a complete attachment body. func (payload AttachmentPayload) Validate() error { metadata := common.AttachmentMetadata{ Filename: payload.Filename, ContentType: payload.ContentType, SizeBytes: payload.SizeBytes, } if err := metadata.Validate(); err != nil { return err } decoded, err := base64.StdEncoding.DecodeString(payload.ContentBase64) if err != nil { return fmt.Errorf("attachment content_base64 must be valid base64: %w", err) } if int64(len(decoded)) != payload.SizeBytes { return fmt.Errorf( "attachment size bytes must match decoded content size: got %d, want %d", payload.SizeBytes, len(decoded), ) } return nil } // DeliveryPayload stores the raw attachment payloads that must survive stream // offset advancement. type DeliveryPayload struct { // DeliveryID identifies the owning accepted delivery. DeliveryID common.DeliveryID // Attachments stores the raw inline attachment payloads. Attachments []AttachmentPayload } // Validate reports whether payload contains a complete attachment bundle. func (payload DeliveryPayload) Validate() error { if err := payload.DeliveryID.Validate(); err != nil { return fmt.Errorf("delivery payload delivery id: %w", err) } if len(payload.Attachments) == 0 { return fmt.Errorf("delivery payload attachments must not be empty") } for index, attachment := range payload.Attachments { if err := attachment.Validate(); err != nil { return fmt.Errorf("delivery payload attachments[%d]: %w", index, err) } } return nil } // CreateAcceptanceInput stores the durable write set required for one // generic-delivery acceptance attempt. type CreateAcceptanceInput struct { // Delivery stores the accepted delivery record. Delivery deliverydomain.Delivery // FirstAttempt stores the first scheduled attempt. FirstAttempt attempt.Attempt // DeliveryPayload stores the optional raw attachment payload bundle. DeliveryPayload *DeliveryPayload // Idempotency stores the idempotency reservation bound to Delivery. Idempotency idempotency.Record } // Validate reports whether input contains a consistent durable write set. func (input CreateAcceptanceInput) Validate() error { if err := input.Delivery.Validate(); err != nil { return fmt.Errorf("delivery: %w", err) } if err := input.FirstAttempt.Validate(); err != nil { return fmt.Errorf("first attempt: %w", err) } if input.FirstAttempt.DeliveryID != input.Delivery.DeliveryID { return errors.New("first attempt delivery id must match delivery id") } if input.FirstAttempt.Status != attempt.StatusScheduled { return fmt.Errorf("first attempt status must be %q", attempt.StatusScheduled) } if err := input.Idempotency.Validate(); err != nil { return fmt.Errorf("idempotency: %w", err) } if input.Idempotency.DeliveryID != input.Delivery.DeliveryID { return errors.New("idempotency delivery id must match delivery id") } if input.Idempotency.Source != input.Delivery.Source { return errors.New("idempotency source must match delivery source") } if input.Idempotency.IdempotencyKey != input.Delivery.IdempotencyKey { return errors.New("idempotency key must match delivery idempotency key") } if input.DeliveryPayload != nil { if err := input.DeliveryPayload.Validate(); err != nil { return fmt.Errorf("delivery payload: %w", err) } if input.DeliveryPayload.DeliveryID != input.Delivery.DeliveryID { return errors.New("delivery payload delivery id must match delivery id") } } return nil } // Store describes the durable storage required by the generic-delivery use // case. type Store interface { // CreateAcceptance stores the complete durable write set for one generic // acceptance attempt. Implementations must wrap ErrConflict when the write // set races with an already accepted idempotency scope or delivery key. CreateAcceptance(context.Context, CreateAcceptanceInput) error // GetIdempotency loads the idempotency reservation for one generic-delivery // scope. GetIdempotency(context.Context, deliverydomain.Source, common.IdempotencyKey) (idempotency.Record, bool, error) // GetDelivery loads one accepted delivery by its identifier. GetDelivery(context.Context, common.DeliveryID) (deliverydomain.Delivery, bool, error) } // Clock provides the current wall-clock time. type Clock interface { // Now returns the current time. Now() time.Time } // Telemetry records low-cardinality generic-delivery outcomes. type Telemetry interface { // RecordGenericDeliveryOutcome records one coarse generic-acceptance // outcome. RecordGenericDeliveryOutcome(context.Context, string) // RecordAcceptedGenericDelivery records one newly accepted generic // delivery. RecordAcceptedGenericDelivery(context.Context) // RecordDeliveryStatusTransition records one durable delivery status // transition. RecordDeliveryStatusTransition(context.Context, string, string) } // Config stores the dependencies and policy used by Service. type Config struct { // Store owns the durable accepted state. Store Store // Clock provides wall-clock timestamps. Clock Clock // Telemetry records low-cardinality acceptance outcomes. Telemetry Telemetry // TracerProvider constructs the application span recorder used by the // generic acceptance flow. TracerProvider oteltrace.TracerProvider // Logger writes structured generic acceptance logs. Logger *slog.Logger // IdempotencyTTL stores how long accepted idempotency scopes remain valid. IdempotencyTTL time.Duration } // Service durably accepts generic asynchronous delivery commands. type Service struct { store Store clock Clock telemetry Telemetry tracerProvider oteltrace.TracerProvider logger *slog.Logger idempotencyTTL time.Duration } // New constructs Service from cfg. func New(cfg Config) (*Service, error) { switch { case cfg.Store == nil: return nil, errors.New("new accept generic delivery service: nil store") case cfg.Clock == nil: return nil, errors.New("new accept generic delivery service: nil clock") case cfg.IdempotencyTTL <= 0: return nil, errors.New("new accept generic delivery service: non-positive idempotency ttl") default: tracerProvider := cfg.TracerProvider if tracerProvider == nil { tracerProvider = otel.GetTracerProvider() } logger := cfg.Logger if logger == nil { logger = slog.Default() } return &Service{ store: cfg.Store, clock: cfg.Clock, telemetry: cfg.Telemetry, tracerProvider: tracerProvider, logger: logger.With("component", "accept_generic_delivery"), idempotencyTTL: cfg.IdempotencyTTL, }, nil } } // Execute accepts one normalized generic-delivery command. func (service *Service) Execute(ctx context.Context, command streamcommand.Command) (Result, error) { if ctx == nil { return Result{}, errors.New("accept generic delivery: nil context") } if service == nil { return Result{}, errors.New("accept generic delivery: nil service") } if err := command.Validate(); err != nil { return Result{}, fmt.Errorf("accept generic delivery: %w", err) } ctx, span := service.tracerProvider.Tracer(tracerName).Start(ctx, "mail.accept_generic_delivery") defer span.End() span.SetAttributes( attribute.String("mail.delivery_id", command.DeliveryID.String()), attribute.String("mail.source", string(command.Source)), attribute.String("mail.payload_mode", string(command.PayloadMode)), ) if strings.TrimSpace(command.TraceID) != "" { span.SetAttributes(attribute.String("mail.command_trace_id", command.TraceID)) } if !command.TemplateID.IsZero() { span.SetAttributes(attribute.String("mail.template_id", command.TemplateID.String())) } fingerprint, err := command.Fingerprint() if err != nil { return Result{}, fmt.Errorf("accept generic delivery: %w", err) } if result, handled, err := service.resolveReplay(ctx, command, fingerprint); handled { if err != nil { service.recordOutcome(ctx, replayOutcomeForError(err)) return Result{}, err } service.recordOutcome(ctx, string(result.Outcome)) return result, nil } createInput, result, err := service.buildCreateInput(command, fingerprint) if err != nil { return Result{}, fmt.Errorf("accept generic delivery: %w", err) } if err := service.store.CreateAcceptance(ctx, createInput); err != nil { if !errors.Is(err, ErrConflict) { service.recordOutcome(ctx, "service_unavailable") return Result{}, fmt.Errorf("%w: create acceptance: %v", ErrServiceUnavailable, err) } if replayResult, handled, replayErr := service.resolveReplay(ctx, command, fingerprint); handled { if replayErr != nil { service.recordOutcome(ctx, replayOutcomeForError(replayErr)) return Result{}, replayErr } service.recordOutcome(ctx, string(replayResult.Outcome)) return replayResult, nil } service.recordOutcome(ctx, "service_unavailable") return Result{}, fmt.Errorf("%w: create acceptance conflict without replay state", ErrServiceUnavailable) } service.recordOutcome(ctx, string(result.Outcome)) service.recordAcceptedDelivery(ctx) service.recordStatusTransition(ctx, createInput.Delivery) span.SetAttributes( attribute.String("mail.status", string(createInput.Delivery.Status)), attribute.String("mail.outcome", string(result.Outcome)), ) logArgs := logging.CommandAttrs(command) logArgs = append(logArgs, "status", string(createInput.Delivery.Status), "outcome", string(result.Outcome), "payload_mode", string(command.PayloadMode), ) logArgs = append(logArgs, logging.TraceAttrsFromContext(ctx)...) service.logger.Info("generic delivery accepted", logArgs...) return result, nil } func (service *Service) buildCreateInput(command streamcommand.Command, fingerprint string) (CreateAcceptanceInput, Result, error) { now := service.clock.Now().UTC().Truncate(time.Millisecond) deliveryRecord := deliverydomain.Delivery{ DeliveryID: command.DeliveryID, Source: command.Source, PayloadMode: command.PayloadMode, Envelope: command.Envelope, Attachments: attachmentMetadata(command.Attachments), IdempotencyKey: command.IdempotencyKey, Status: deliverydomain.StatusQueued, AttemptCount: 1, CreatedAt: now, UpdatedAt: now, } switch command.PayloadMode { case deliverydomain.PayloadModeRendered: deliveryRecord.Content = deliverydomain.Content{ Subject: command.Subject, TextBody: command.TextBody, HTMLBody: command.HTMLBody, } case deliverydomain.PayloadModeTemplate: deliveryRecord.TemplateID = command.TemplateID deliveryRecord.Locale = command.Locale deliveryRecord.TemplateVariables = cloneJSONObject(command.Variables) default: return CreateAcceptanceInput{}, Result{}, fmt.Errorf("build generic delivery record: unsupported payload mode %q", command.PayloadMode) } if err := deliveryRecord.Validate(); err != nil { return CreateAcceptanceInput{}, Result{}, fmt.Errorf("build generic delivery record: %w", err) } firstAttempt := attempt.Attempt{ DeliveryID: command.DeliveryID, AttemptNo: 1, ScheduledFor: now, Status: attempt.StatusScheduled, } if err := firstAttempt.Validate(); err != nil { return CreateAcceptanceInput{}, Result{}, fmt.Errorf("build generic first attempt: %w", err) } createInput := CreateAcceptanceInput{ Delivery: deliveryRecord, FirstAttempt: firstAttempt, Idempotency: idempotency.Record{ Source: command.Source, IdempotencyKey: command.IdempotencyKey, DeliveryID: command.DeliveryID, RequestFingerprint: fingerprint, CreatedAt: now, ExpiresAt: now.Add(service.idempotencyTTL), }, } if len(command.Attachments) > 0 { createInput.DeliveryPayload = &DeliveryPayload{ DeliveryID: command.DeliveryID, Attachments: attachmentPayloads(command.Attachments), } } if err := createInput.Validate(); err != nil { return CreateAcceptanceInput{}, Result{}, fmt.Errorf("build generic create input: %w", err) } result := Result{Outcome: OutcomeAccepted} if err := result.Validate(); err != nil { return CreateAcceptanceInput{}, Result{}, fmt.Errorf("build generic delivery result: %w", err) } return createInput, result, nil } func (service *Service) resolveReplay(ctx context.Context, command streamcommand.Command, fingerprint string) (Result, bool, error) { record, found, err := service.store.GetIdempotency(ctx, command.Source, command.IdempotencyKey) if err != nil { return Result{}, true, fmt.Errorf("%w: load idempotency: %v", ErrServiceUnavailable, err) } if !found { return Result{}, false, nil } if record.RequestFingerprint != fingerprint { return Result{}, true, fmt.Errorf("%w: request conflicts with current state", ErrConflict) } deliveryRecord, found, err := service.store.GetDelivery(ctx, record.DeliveryID) if err != nil { return Result{}, true, fmt.Errorf("%w: load delivery: %v", ErrServiceUnavailable, err) } if !found { return Result{}, true, fmt.Errorf("%w: delivery %q is missing for idempotency scope", ErrServiceUnavailable, record.DeliveryID) } if deliveryRecord.DeliveryID != command.DeliveryID { return Result{}, true, fmt.Errorf("%w: idempotency delivery %q mismatches command delivery %q", ErrServiceUnavailable, deliveryRecord.DeliveryID, command.DeliveryID) } return deriveReplayResult(deliveryRecord) } func deriveReplayResult(record deliverydomain.Delivery) (Result, bool, error) { switch record.Status { case deliverydomain.StatusAccepted, deliverydomain.StatusQueued, deliverydomain.StatusRendered, deliverydomain.StatusSending, deliverydomain.StatusSent, deliverydomain.StatusSuppressed, deliverydomain.StatusFailed, deliverydomain.StatusDeadLetter: return Result{Outcome: OutcomeDuplicate}, true, nil default: return Result{}, true, fmt.Errorf("%w: unsupported replay delivery status %q", ErrServiceUnavailable, record.Status) } } func (service *Service) recordAcceptedDelivery(ctx context.Context) { if service == nil || service.telemetry == nil { return } service.telemetry.RecordAcceptedGenericDelivery(ctx) } func (service *Service) recordStatusTransition(ctx context.Context, record deliverydomain.Delivery) { if service == nil || service.telemetry == nil { return } service.telemetry.RecordDeliveryStatusTransition(ctx, string(record.Status), string(record.Source)) } func (service *Service) recordOutcome(ctx context.Context, outcome string) { if service == nil || service.telemetry == nil || strings.TrimSpace(outcome) == "" { return } service.telemetry.RecordGenericDeliveryOutcome(ctx, outcome) } func replayOutcomeForError(err error) string { switch { case errors.Is(err, ErrConflict): return "conflict" case errors.Is(err, ErrServiceUnavailable): return "service_unavailable" default: return "" } } func attachmentMetadata(values []streamcommand.Attachment) []common.AttachmentMetadata { if values == nil { return nil } result := make([]common.AttachmentMetadata, len(values)) for index, value := range values { result[index] = common.AttachmentMetadata{ Filename: value.Filename, ContentType: value.ContentType, SizeBytes: value.SizeBytes, } } return result } func attachmentPayloads(values []streamcommand.Attachment) []AttachmentPayload { result := make([]AttachmentPayload, len(values)) for index, value := range values { result[index] = AttachmentPayload{ Filename: value.Filename, ContentType: value.ContentType, ContentBase64: value.ContentBase64, SizeBytes: value.SizeBytes, } } return result } func cloneJSONObject(value map[string]any) map[string]any { if value == nil { return nil } cloned := make(map[string]any, len(value)) for key, item := range value { cloned[key] = cloneJSONValue(item) } return cloned } func cloneJSONValue(value any) any { switch typed := value.(type) { case map[string]any: cloned := make(map[string]any, len(typed)) for key, item := range typed { cloned[key] = cloneJSONValue(item) } return cloned case []any: cloned := make([]any, len(typed)) for index, item := range typed { cloned[index] = cloneJSONValue(item) } return cloned default: return typed } }