// Package stubprovider provides the deterministic local provider used by Mail // Service tests and local bootstrap flows. package stubprovider import ( "context" "errors" "fmt" "sync" "galaxy/mail/internal/domain/common" deliverydomain "galaxy/mail/internal/domain/delivery" "galaxy/mail/internal/ports" ) const providerName = "stub" // ScriptedOutcome stores one queued stub-provider result consumed by the next // Send call. type ScriptedOutcome struct { // Classification stores the stable provider result classification. Classification ports.Classification // Script stores the optional stable script label included in the redacted // provider summary. Script string // Details stores optional in-memory-only diagnostic fields associated with // the scripted result. Details map[string]string } // Validate reports whether outcome contains one supported queued stub result. func (outcome ScriptedOutcome) Validate() error { if !outcome.Classification.IsKnown() { return fmt.Errorf("stub scripted classification %q is unsupported", outcome.Classification) } if outcome.Script != "" { if _, err := ports.BuildSafeSummary(ports.SummaryFields{ Provider: providerName, Result: string(outcome.Classification), Script: outcome.Script, }); err != nil { return fmt.Errorf("stub scripted outcome: %w", err) } } for key, value := range outcome.Details { result := ports.Result{ Classification: outcome.Classification, Summary: "provider=stub result=accepted", Details: map[string]string{ key: value, }, } if err := result.Validate(); err != nil { return fmt.Errorf("stub scripted details: %w", err) } } return nil } // Provider stores one deterministic in-memory provider implementation. type Provider struct { mu sync.Mutex queue []ScriptedOutcome inputs []ports.Message closed bool } // New constructs the deterministic stub provider. func New(initial ...ScriptedOutcome) (*Provider, error) { provider := &Provider{} if err := provider.Enqueue(initial...); err != nil { return nil, fmt.Errorf("new stub provider: %w", err) } return provider, nil } // Send records message and returns the next scripted outcome, or a stable // accepted outcome when no script remains. func (provider *Provider) Send(ctx context.Context, message ports.Message) (ports.Result, error) { switch { case ctx == nil: return ports.Result{}, errors.New("send with stub provider: nil context") case provider == nil: return ports.Result{}, errors.New("send with stub provider: nil provider") } if err := message.Validate(); err != nil { return ports.Result{}, fmt.Errorf("send with stub provider: %w", err) } provider.mu.Lock() defer provider.mu.Unlock() if provider.closed { return ports.Result{}, errors.New("send with stub provider: provider is closed") } provider.inputs = append(provider.inputs, cloneMessage(message)) if len(provider.queue) == 0 { return scriptedResult(ScriptedOutcome{ Classification: ports.ClassificationAccepted, }) } next := provider.queue[0] provider.queue = provider.queue[1:] return scriptedResult(next) } // Close marks the provider as closed. Future Send calls fail fast. func (provider *Provider) Close() error { if provider == nil { return nil } provider.mu.Lock() defer provider.mu.Unlock() provider.closed = true return nil } // Enqueue appends scripted outcomes to the stub queue. func (provider *Provider) Enqueue(outcomes ...ScriptedOutcome) error { if provider == nil { return errors.New("enqueue stub provider outcomes: nil provider") } provider.mu.Lock() defer provider.mu.Unlock() for index, outcome := range outcomes { if err := outcome.Validate(); err != nil { return fmt.Errorf("enqueue stub provider outcomes[%d]: %w", index, err) } provider.queue = append(provider.queue, ScriptedOutcome{ Classification: outcome.Classification, Script: outcome.Script, Details: ports.CloneDetails(outcome.Details), }) } return nil } // Inputs returns a detached snapshot of the accepted Send inputs. func (provider *Provider) Inputs() []ports.Message { if provider == nil { return nil } provider.mu.Lock() defer provider.mu.Unlock() inputs := make([]ports.Message, len(provider.inputs)) for index, input := range provider.inputs { inputs[index] = cloneMessage(input) } return inputs } func scriptedResult(outcome ScriptedOutcome) (ports.Result, error) { summary, err := ports.BuildSafeSummary(ports.SummaryFields{ Provider: providerName, Result: string(outcome.Classification), Script: outcome.Script, }) if err != nil { return ports.Result{}, fmt.Errorf("build stub provider summary: %w", err) } result := ports.Result{ Classification: outcome.Classification, Summary: summary, Details: ports.CloneDetails(outcome.Details), } if err := result.Validate(); err != nil { return ports.Result{}, fmt.Errorf("build stub provider result: %w", err) } return result, nil } func cloneMessage(message ports.Message) ports.Message { cloned := ports.Message{ Envelope: deliverydomain.Envelope{ To: append([]common.Email(nil), message.Envelope.To...), Cc: append([]common.Email(nil), message.Envelope.Cc...), Bcc: append([]common.Email(nil), message.Envelope.Bcc...), ReplyTo: append([]common.Email(nil), message.Envelope.ReplyTo...), }, Content: message.Content, } if len(message.Attachments) > 0 { cloned.Attachments = make([]ports.Attachment, len(message.Attachments)) for index, attachment := range message.Attachments { content := make([]byte, len(attachment.Content)) copy(content, attachment.Content) cloned.Attachments[index] = ports.Attachment{ Metadata: attachment.Metadata, Content: content, } } } return cloned }