feat: mail service

This commit is contained in:
Ilia Denisov
2026-04-17 18:39:16 +02:00
committed by GitHub
parent 23ffcb7535
commit 5b7593e6f6
183 changed files with 31215 additions and 248 deletions
+625
View File
@@ -0,0 +1,625 @@
// 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
}