// Package delivery defines the logical delivery and dead-letter entities owned // directly by Mail Service. package delivery import ( "encoding/json" "fmt" "strings" "time" "galaxy/mail/internal/domain/attempt" "galaxy/mail/internal/domain/common" ) // Source identifies the trusted caller or workflow that created one delivery. type Source string const ( // SourceAuthSession reports deliveries accepted from Auth / Session Service. SourceAuthSession Source = "authsession" // SourceNotification reports deliveries accepted from Notification Service. SourceNotification Source = "notification" // SourceOperatorResend reports clone deliveries created by the operator // resend workflow. SourceOperatorResend Source = "operator_resend" ) // IsKnown reports whether Source belongs to the frozen v1 source vocabulary. func (source Source) IsKnown() bool { switch source { case SourceAuthSession, SourceNotification, SourceOperatorResend: return true default: return false } } // PayloadMode identifies whether the delivery carries pre-rendered content or // template-selection metadata. type PayloadMode string const ( // PayloadModeRendered reports that the delivery already stores final // rendered content. PayloadModeRendered PayloadMode = "rendered" // PayloadModeTemplate reports that final content is produced later from a // template and locale. PayloadModeTemplate PayloadMode = "template" ) // IsKnown reports whether PayloadMode is supported by the current domain // model. func (mode PayloadMode) IsKnown() bool { switch mode { case PayloadModeRendered, PayloadModeTemplate: return true default: return false } } // Status identifies the lifecycle state of one logical mail delivery. type Status string const ( // StatusAccepted reports that intake validation succeeded and a durable // delivery record exists. StatusAccepted Status = "accepted" // StatusQueued reports that the next attempt is durably scheduled. StatusQueued Status = "queued" // StatusRendered reports that template-mode content has been materialized. StatusRendered Status = "rendered" // StatusSending reports that one worker currently owns the active attempt. StatusSending Status = "sending" // StatusSent reports that the provider accepted the SMTP envelope. StatusSent Status = "sent" // StatusSuppressed reports that delivery was intentionally skipped as a // successful business outcome. StatusSuppressed Status = "suppressed" // StatusFailed reports that delivery ended in a terminal failure without a // dead-letter entry. StatusFailed Status = "failed" // StatusDeadLetter reports that delivery reached an operator-visible // dead-letter state. StatusDeadLetter Status = "dead_letter" ) // IsKnown reports whether Status belongs to the frozen v1 delivery lifecycle. func (status Status) IsKnown() bool { switch status { case StatusAccepted, StatusQueued, StatusRendered, StatusSending, StatusSent, StatusSuppressed, StatusFailed, StatusDeadLetter: return true default: return false } } // IsTerminal reports whether Status can no longer accept lifecycle // transitions. func (status Status) IsTerminal() bool { switch status { case StatusSent, StatusSuppressed, StatusFailed, StatusDeadLetter: return true default: return false } } // CanTransitionTo reports whether the current Status may move to next under // the frozen Stage 2 delivery lifecycle rules. func (status Status) CanTransitionTo(next Status) bool { switch status { case StatusAccepted: switch next { case StatusQueued, StatusSuppressed: return true } case StatusQueued: switch next { case StatusRendered, StatusSending, StatusFailed: return true } case StatusRendered: switch next { case StatusSending, StatusFailed: return true } case StatusSending: switch next { case StatusSent, StatusSuppressed, StatusQueued, StatusFailed, StatusDeadLetter: return true } } return false } // AllowsResend reports whether deliveries in Status may be cloned through the // trusted resend workflow. func (status Status) AllowsResend() bool { switch status { case StatusSent, StatusSuppressed, StatusFailed, StatusDeadLetter: return true default: return false } } // Envelope stores the SMTP-addressing fields of one logical delivery. type Envelope struct { // To stores the primary recipients. To []common.Email // Cc stores the carbon-copy recipients. Cc []common.Email // Bcc stores the blind-carbon-copy recipients. Bcc []common.Email // ReplyTo stores the reply-to addresses attached to the message headers. ReplyTo []common.Email } // Validate reports whether Envelope contains only valid addresses and at // least one effective recipient. func (envelope Envelope) Validate() error { recipientCount := 0 validateGroup := func(name string, values []common.Email) error { for index, value := range values { if err := value.Validate(); err != nil { return fmt.Errorf("%s[%d]: %w", name, index, err) } } return nil } if err := validateGroup("delivery envelope to", envelope.To); err != nil { return err } recipientCount += len(envelope.To) if err := validateGroup("delivery envelope cc", envelope.Cc); err != nil { return err } recipientCount += len(envelope.Cc) if err := validateGroup("delivery envelope bcc", envelope.Bcc); err != nil { return err } recipientCount += len(envelope.Bcc) if err := validateGroup("delivery envelope reply to", envelope.ReplyTo); err != nil { return err } if recipientCount == 0 { return fmt.Errorf("delivery envelope must contain at least one recipient") } return nil } // Content stores the materialized subject and body parts of one delivery. type Content struct { // Subject stores the final subject line. Subject string // TextBody stores the final plaintext body. TextBody string // HTMLBody stores the optional final HTML body. HTMLBody string } // ValidateMaterialized reports whether Content contains the minimum subject // and plaintext body required for a concrete outbound message. func (content Content) ValidateMaterialized() error { if content.Subject == "" { return fmt.Errorf("delivery content subject must not be empty") } if content.TextBody == "" { return fmt.Errorf("delivery content text body must not be empty") } return nil } // Delivery stores one durable logical mail delivery record. type Delivery struct { // DeliveryID identifies the delivery. DeliveryID common.DeliveryID // ResendParentDeliveryID identifies the original delivery when the current // record was created by the resend workflow. ResendParentDeliveryID common.DeliveryID // Source stores the frozen source vocabulary value. Source Source // PayloadMode stores whether the delivery uses pre-rendered content or // deferred template rendering. PayloadMode PayloadMode // TemplateID stores the template family used by template-mode deliveries. TemplateID common.TemplateID // Envelope stores the SMTP addressing information. Envelope Envelope // Content stores the final rendered subject and bodies when materialized. Content Content // Attachments stores long-lived attachment metadata only. Attachments []common.AttachmentMetadata // Locale stores the canonical locale used for template selection when // applicable. Locale common.Locale // LocaleFallbackUsed reports whether rendering fell back from the requested // locale to `en`. LocaleFallbackUsed bool // TemplateVariables stores the JSON object used for later template // rendering when PayloadMode is `template`. TemplateVariables map[string]any // IdempotencyKey stores the caller-owned deduplication key. IdempotencyKey common.IdempotencyKey // Status stores the current delivery lifecycle state. Status Status // AttemptCount stores how many attempts have been created for the delivery. AttemptCount int // LastAttemptStatus stores the latest recorded attempt outcome when one is // available. LastAttemptStatus attempt.Status // ProviderSummary stores redacted provider outcome details when available. ProviderSummary string // CreatedAt stores when the delivery was created. CreatedAt time.Time // UpdatedAt stores when the delivery was last mutated. UpdatedAt time.Time // SentAt stores when the delivery entered the sent terminal state. SentAt *time.Time // SuppressedAt stores when the delivery entered the suppressed terminal // state. SuppressedAt *time.Time // FailedAt stores when the delivery entered the failed terminal state. FailedAt *time.Time // DeadLetteredAt stores when the delivery entered the dead-letter terminal // state. DeadLetteredAt *time.Time } // Validate reports whether Delivery satisfies the frozen Stage 2 structural // and lifecycle invariants. func (record Delivery) Validate() error { if err := record.DeliveryID.Validate(); err != nil { return fmt.Errorf("delivery id: %w", err) } if !record.Source.IsKnown() { return fmt.Errorf("delivery source %q is unsupported", record.Source) } if !record.PayloadMode.IsKnown() { return fmt.Errorf("delivery payload mode %q is unsupported", record.PayloadMode) } if err := record.Envelope.Validate(); err != nil { return err } for index, attachment := range record.Attachments { if err := attachment.Validate(); err != nil { return fmt.Errorf("delivery attachments[%d]: %w", index, err) } } if err := record.IdempotencyKey.Validate(); err != nil { return fmt.Errorf("delivery idempotency key: %w", err) } if !record.Status.IsKnown() { return fmt.Errorf("delivery status %q is unsupported", record.Status) } if record.AttemptCount < 0 { return fmt.Errorf("delivery attempt count must not be negative") } if record.LastAttemptStatus != "" && !record.LastAttemptStatus.IsKnown() { return fmt.Errorf("delivery last attempt status %q is unsupported", record.LastAttemptStatus) } if err := validateOptionalToken("delivery provider summary", record.ProviderSummary); err != nil { return err } if err := common.ValidateTimestamp("delivery created at", record.CreatedAt); err != nil { return err } if err := common.ValidateTimestamp("delivery updated at", record.UpdatedAt); err != nil { return err } if record.UpdatedAt.Before(record.CreatedAt) { return fmt.Errorf("delivery updated at must not be before created at") } switch record.Source { case SourceOperatorResend: if err := record.ResendParentDeliveryID.Validate(); err != nil { return fmt.Errorf("delivery resend parent delivery id: %w", err) } if record.ResendParentDeliveryID == record.DeliveryID { return fmt.Errorf("delivery resend parent delivery id must differ from delivery id") } default: if !record.ResendParentDeliveryID.IsZero() { return fmt.Errorf("delivery resend parent delivery id must be empty unless source is %q", SourceOperatorResend) } } switch record.PayloadMode { case PayloadModeRendered: if !record.TemplateID.IsZero() { return fmt.Errorf("rendered delivery must not contain template id") } if !record.Locale.IsZero() { return fmt.Errorf("rendered delivery must not contain locale") } if record.LocaleFallbackUsed { return fmt.Errorf("rendered delivery must not mark locale fallback") } if len(record.TemplateVariables) != 0 { return fmt.Errorf("rendered delivery must not contain template variables") } if err := record.Content.ValidateMaterialized(); err != nil { return err } case PayloadModeTemplate: if err := record.TemplateID.Validate(); err != nil { return fmt.Errorf("delivery template id: %w", err) } if err := record.Locale.Validate(); err != nil { return fmt.Errorf("delivery locale: %w", err) } if err := validateJSONObject("delivery template variables", record.TemplateVariables); err != nil { return err } if record.Status == StatusRendered || record.Status == StatusSending || record.Status == StatusSent { if err := record.Content.ValidateMaterialized(); err != nil { return err } } } if record.Status == StatusRendered && record.PayloadMode != PayloadModeTemplate { return fmt.Errorf("delivery status %q requires payload mode %q", StatusRendered, PayloadModeTemplate) } if err := validateTerminalTimestamps(record); err != nil { return err } return nil } // DeadLetterEntry stores the operator-visible dead-letter record for one // delivery that exhausted normal automated handling. type DeadLetterEntry struct { // DeliveryID identifies the dead-lettered delivery. DeliveryID common.DeliveryID // FinalAttemptNo stores the last attempt number associated with the // dead-letter transition. FinalAttemptNo int // FailureClassification stores the final machine-readable failure class. FailureClassification string // ProviderSummary stores redacted provider outcome details when available. ProviderSummary string // CreatedAt stores when the dead-letter entry was created. CreatedAt time.Time // RecoveryHint stores an optional operator-facing recovery note. RecoveryHint string } // Validate reports whether DeadLetterEntry contains a complete dead-letter // record. func (entry DeadLetterEntry) Validate() error { if err := entry.DeliveryID.Validate(); err != nil { return fmt.Errorf("dead-letter delivery id: %w", err) } if entry.FinalAttemptNo < 1 { return fmt.Errorf("dead-letter final attempt number must be at least 1") } if err := validateToken("dead-letter failure classification", entry.FailureClassification); err != nil { return err } if err := validateOptionalToken("dead-letter provider summary", entry.ProviderSummary); err != nil { return err } if err := validateOptionalToken("dead-letter recovery hint", entry.RecoveryHint); err != nil { return err } if err := common.ValidateTimestamp("dead-letter created at", entry.CreatedAt); err != nil { return err } return nil } // ValidateFor reports whether entry is the required dead-letter record for // record. func (entry DeadLetterEntry) ValidateFor(record Delivery) error { if err := record.Validate(); err != nil { return err } if err := entry.Validate(); err != nil { return err } if record.Status != StatusDeadLetter { return fmt.Errorf("dead-letter entry requires delivery status %q", StatusDeadLetter) } if entry.DeliveryID != record.DeliveryID { return fmt.Errorf("dead-letter delivery id must match delivery id") } if record.AttemptCount < entry.FinalAttemptNo { return fmt.Errorf("dead-letter final attempt number must not exceed delivery attempt count") } if record.DeadLetteredAt == nil { return fmt.Errorf("dead-letter delivery must contain dead-lettered at") } if entry.CreatedAt.Before(*record.DeadLetteredAt) { return fmt.Errorf("dead-letter created at must not be before delivery dead-lettered at") } return nil } // ValidateDeadLetterState reports whether record and entry satisfy the frozen // rule that only dead-lettered deliveries may own a dead-letter entry. func ValidateDeadLetterState(record Delivery, entry *DeadLetterEntry) error { if err := record.Validate(); err != nil { return err } if record.Status == StatusDeadLetter { if entry == nil { return fmt.Errorf("dead-letter delivery requires dead-letter entry") } return entry.ValidateFor(record) } if entry != nil { return fmt.Errorf("dead-letter entry is not allowed for delivery status %q", record.Status) } return nil } func validateTerminalTimestamps(record Delivery) error { if record.SentAt != nil { if err := common.ValidateTimestamp("delivery sent at", *record.SentAt); err != nil { return err } if record.SentAt.Before(record.CreatedAt) { return fmt.Errorf("delivery sent at must not be before created at") } } if record.SuppressedAt != nil { if err := common.ValidateTimestamp("delivery suppressed at", *record.SuppressedAt); err != nil { return err } if record.SuppressedAt.Before(record.CreatedAt) { return fmt.Errorf("delivery suppressed at must not be before created at") } } if record.FailedAt != nil { if err := common.ValidateTimestamp("delivery failed at", *record.FailedAt); err != nil { return err } if record.FailedAt.Before(record.CreatedAt) { return fmt.Errorf("delivery failed at must not be before created at") } } if record.DeadLetteredAt != nil { if err := common.ValidateTimestamp("delivery dead-lettered at", *record.DeadLetteredAt); err != nil { return err } if record.DeadLetteredAt.Before(record.CreatedAt) { return fmt.Errorf("delivery dead-lettered at must not be before created at") } } switch record.Status { case StatusAccepted, StatusQueued, StatusRendered, StatusSending: if record.SentAt != nil || record.SuppressedAt != nil || record.FailedAt != nil || record.DeadLetteredAt != nil { return fmt.Errorf("non-terminal delivery must not contain terminal timestamp fields") } case StatusSent: if record.SentAt == nil { return fmt.Errorf("sent delivery must contain sent at") } if record.SuppressedAt != nil || record.FailedAt != nil || record.DeadLetteredAt != nil { return fmt.Errorf("sent delivery must not contain other terminal timestamp fields") } case StatusSuppressed: if record.SuppressedAt == nil { return fmt.Errorf("suppressed delivery must contain suppressed at") } if record.SentAt != nil || record.FailedAt != nil || record.DeadLetteredAt != nil { return fmt.Errorf("suppressed delivery must not contain other terminal timestamp fields") } case StatusFailed: if record.FailedAt == nil { return fmt.Errorf("failed delivery must contain failed at") } if record.SentAt != nil || record.SuppressedAt != nil || record.DeadLetteredAt != nil { return fmt.Errorf("failed delivery must not contain other terminal timestamp fields") } case StatusDeadLetter: if record.DeadLetteredAt == nil { return fmt.Errorf("dead-letter delivery must contain dead-lettered at") } if record.SentAt != nil || record.SuppressedAt != nil || record.FailedAt != nil { return fmt.Errorf("dead-letter delivery must not contain other terminal timestamp fields") } } return nil } func validateToken(name string, value string) error { switch { case strings.TrimSpace(value) == "": return fmt.Errorf("%s must not be empty", name) case strings.TrimSpace(value) != value: return fmt.Errorf("%s must not contain surrounding whitespace", name) default: return nil } } func validateOptionalToken(name string, value string) error { if value == "" { return nil } return validateToken(name, value) } func validateJSONObject(name string, value map[string]any) error { if value == nil { return fmt.Errorf("%s must not be nil", name) } if _, err := json.Marshal(value); err != nil { return fmt.Errorf("%s must be JSON-serializable: %w", name, err) } return nil }