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
+200
View File
@@ -0,0 +1,200 @@
// Package attempt defines the logical delivery-attempt entity owned by Mail
// Service.
package attempt
import (
"fmt"
"strings"
"time"
"galaxy/mail/internal/domain/common"
)
// Status identifies the lifecycle state of one concrete delivery attempt.
type Status string
const (
// StatusScheduled reports that the attempt is durably planned but has not
// started execution yet.
StatusScheduled Status = "scheduled"
// StatusInProgress reports that one worker currently owns the attempt.
StatusInProgress Status = "in_progress"
// StatusProviderAccepted reports that the provider accepted the SMTP
// envelope.
StatusProviderAccepted Status = "provider_accepted"
// StatusProviderRejected reports that the provider rejected the SMTP
// envelope.
StatusProviderRejected Status = "provider_rejected"
// StatusTransportFailed reports that the attempt failed before a stable
// provider accept or reject result was obtained.
StatusTransportFailed Status = "transport_failed"
// StatusTimedOut reports that the provider call exceeded the configured
// execution deadline.
StatusTimedOut Status = "timed_out"
// StatusRenderFailed reports that template rendering failed before any
// provider interaction was attempted.
StatusRenderFailed Status = "render_failed"
)
// IsKnown reports whether Status is supported by the current Mail Service
// attempt state machine.
func (status Status) IsKnown() bool {
switch status {
case StatusScheduled,
StatusInProgress,
StatusProviderAccepted,
StatusProviderRejected,
StatusTransportFailed,
StatusTimedOut,
StatusRenderFailed:
return true
default:
return false
}
}
// IsTerminal reports whether Status can no longer accept a lifecycle
// transition.
func (status Status) IsTerminal() bool {
switch status {
case StatusProviderAccepted,
StatusProviderRejected,
StatusTransportFailed,
StatusTimedOut,
StatusRenderFailed:
return true
default:
return false
}
}
// CanTransitionTo reports whether the current Status may move to next under
// the frozen Stage 2 attempt lifecycle rules.
func (status Status) CanTransitionTo(next Status) bool {
switch status {
case StatusScheduled:
switch next {
case StatusInProgress, StatusRenderFailed:
return true
}
case StatusInProgress:
switch next {
case StatusProviderAccepted, StatusProviderRejected, StatusTransportFailed, StatusTimedOut:
return true
}
}
return false
}
// Attempt stores one durable execution record for a delivery attempt.
type Attempt struct {
// DeliveryID identifies the owning logical delivery.
DeliveryID common.DeliveryID
// AttemptNo stores the monotonically increasing attempt sequence number.
AttemptNo int
// ScheduledFor stores when the attempt becomes due.
ScheduledFor time.Time
// StartedAt stores when a worker claimed the attempt for execution.
StartedAt *time.Time
// FinishedAt stores when the attempt reached a terminal outcome.
FinishedAt *time.Time
// Status stores the current attempt lifecycle state.
Status Status
// ProviderClassification stores provider-specific or adapter-specific
// result classification details when available.
ProviderClassification string
// ProviderSummary stores redacted provider outcome details when available.
ProviderSummary string
}
// Validate reports whether Attempt satisfies the frozen Stage 2 structural and
// lifecycle invariants.
func (record Attempt) Validate() error {
if err := record.DeliveryID.Validate(); err != nil {
return fmt.Errorf("attempt delivery id: %w", err)
}
if record.AttemptNo < 1 {
return fmt.Errorf("attempt number must be at least 1")
}
if err := common.ValidateTimestamp("attempt scheduled for", record.ScheduledFor); err != nil {
return err
}
if !record.Status.IsKnown() {
return fmt.Errorf("attempt status %q is unsupported", record.Status)
}
if err := validateOptionalToken("attempt provider classification", record.ProviderClassification); err != nil {
return err
}
if err := validateOptionalToken("attempt provider summary", record.ProviderSummary); err != nil {
return err
}
switch record.Status {
case StatusScheduled:
if record.StartedAt != nil {
return fmt.Errorf("scheduled attempt must not contain started at")
}
if record.FinishedAt != nil {
return fmt.Errorf("scheduled attempt must not contain finished at")
}
case StatusInProgress:
if record.StartedAt == nil {
return fmt.Errorf("in-progress attempt must contain started at")
}
if err := common.ValidateTimestamp("attempt started at", *record.StartedAt); err != nil {
return err
}
if record.StartedAt.Before(record.ScheduledFor) {
return fmt.Errorf("attempt started at must not be before scheduled for")
}
if record.FinishedAt != nil {
return fmt.Errorf("in-progress attempt must not contain finished at")
}
default:
if record.StartedAt == nil {
return fmt.Errorf("terminal attempt must contain started at")
}
if err := common.ValidateTimestamp("attempt started at", *record.StartedAt); err != nil {
return err
}
if record.StartedAt.Before(record.ScheduledFor) {
return fmt.Errorf("attempt started at must not be before scheduled for")
}
if record.FinishedAt == nil {
return fmt.Errorf("terminal attempt must contain finished at")
}
if err := common.ValidateTimestamp("attempt finished at", *record.FinishedAt); err != nil {
return err
}
if record.FinishedAt.Before(*record.StartedAt) {
return fmt.Errorf("attempt finished at must not be before started at")
}
}
return nil
}
func validateOptionalToken(name string, value string) error {
if value == "" {
return nil
}
if strings.TrimSpace(value) != value {
return fmt.Errorf("%s must not contain surrounding whitespace", name)
}
return nil
}
+168
View File
@@ -0,0 +1,168 @@
package attempt
import (
"testing"
"time"
"galaxy/mail/internal/domain/common"
"github.com/stretchr/testify/require"
)
func TestStatusCanTransitionTo(t *testing.T) {
t.Parallel()
tests := []struct {
name string
from Status
to Status
want bool
}{
{name: "scheduled to in progress", from: StatusScheduled, to: StatusInProgress, want: true},
{name: "scheduled to render failed", from: StatusScheduled, to: StatusRenderFailed, want: true},
{name: "scheduled to accepted", from: StatusScheduled, to: StatusProviderAccepted, want: false},
{name: "in progress to accepted", from: StatusInProgress, to: StatusProviderAccepted, want: true},
{name: "in progress to rejected", from: StatusInProgress, to: StatusProviderRejected, want: true},
{name: "in progress to transport failed", from: StatusInProgress, to: StatusTransportFailed, want: true},
{name: "in progress to timed out", from: StatusInProgress, to: StatusTimedOut, want: true},
{name: "accepted terminal", from: StatusProviderAccepted, to: StatusTimedOut, want: false},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
require.Equal(t, tt.want, tt.from.CanTransitionTo(tt.to))
})
}
}
func TestStatusIsTerminal(t *testing.T) {
t.Parallel()
require.False(t, StatusScheduled.IsTerminal())
require.False(t, StatusInProgress.IsTerminal())
require.True(t, StatusProviderAccepted.IsTerminal())
require.True(t, StatusProviderRejected.IsTerminal())
require.True(t, StatusTransportFailed.IsTerminal())
require.True(t, StatusTimedOut.IsTerminal())
require.True(t, StatusRenderFailed.IsTerminal())
}
func TestAttemptValidate(t *testing.T) {
t.Parallel()
scheduledFor := time.Unix(1_775_121_700, 0).UTC()
startedAt := scheduledFor.Add(time.Minute)
finishedAt := startedAt.Add(2 * time.Second)
tests := []struct {
name string
record Attempt
wantErr bool
}{
{
name: "valid scheduled",
record: Attempt{
DeliveryID: common.DeliveryID("delivery-123"),
AttemptNo: 1,
ScheduledFor: scheduledFor,
Status: StatusScheduled,
},
},
{
name: "valid in progress",
record: Attempt{
DeliveryID: common.DeliveryID("delivery-123"),
AttemptNo: 2,
ScheduledFor: scheduledFor,
StartedAt: &startedAt,
Status: StatusInProgress,
},
},
{
name: "valid terminal",
record: Attempt{
DeliveryID: common.DeliveryID("delivery-123"),
AttemptNo: 3,
ScheduledFor: scheduledFor,
StartedAt: &startedAt,
FinishedAt: &finishedAt,
Status: StatusProviderAccepted,
},
},
{
name: "valid render failed",
record: Attempt{
DeliveryID: common.DeliveryID("delivery-123"),
AttemptNo: 4,
ScheduledFor: scheduledFor,
StartedAt: &startedAt,
FinishedAt: &finishedAt,
Status: StatusRenderFailed,
ProviderClassification: "missing_required_variable",
ProviderSummary: "missing required variables: player.name",
},
},
{
name: "attempt number must be positive",
record: Attempt{
DeliveryID: common.DeliveryID("delivery-123"),
ScheduledFor: scheduledFor,
Status: StatusScheduled,
},
wantErr: true,
},
{
name: "in progress missing started at",
record: Attempt{
DeliveryID: common.DeliveryID("delivery-123"),
AttemptNo: 1,
ScheduledFor: scheduledFor,
Status: StatusInProgress,
},
wantErr: true,
},
{
name: "terminal missing finished at",
record: Attempt{
DeliveryID: common.DeliveryID("delivery-123"),
AttemptNo: 1,
ScheduledFor: scheduledFor,
StartedAt: &startedAt,
Status: StatusProviderRejected,
},
wantErr: true,
},
{
name: "finished before started",
record: Attempt{
DeliveryID: common.DeliveryID("delivery-123"),
AttemptNo: 1,
ScheduledFor: scheduledFor,
StartedAt: &startedAt,
FinishedAt: &scheduledFor,
Status: StatusTimedOut,
},
wantErr: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := tt.record.Validate()
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
})
}
}
+202
View File
@@ -0,0 +1,202 @@
// Package common defines shared value objects used across the Mail Service
// domain model.
package common
import (
"fmt"
"mime"
"net/mail"
"strings"
"time"
"golang.org/x/text/language"
)
// DeliveryID identifies one logical mail delivery accepted by Mail Service.
type DeliveryID string
// String returns DeliveryID as its stored identifier string.
func (id DeliveryID) String() string {
return string(id)
}
// IsZero reports whether DeliveryID does not contain a usable value.
func (id DeliveryID) IsZero() bool {
return strings.TrimSpace(string(id)) == ""
}
// Validate reports whether DeliveryID is non-empty and already normalized for
// domain use.
func (id DeliveryID) Validate() error {
return validateToken("delivery id", string(id))
}
// TemplateID identifies one template family owned by the filesystem-backed
// Mail Service template catalog.
type TemplateID string
// String returns TemplateID as its stored identifier string.
func (id TemplateID) String() string {
return string(id)
}
// IsZero reports whether TemplateID does not contain a usable value.
func (id TemplateID) IsZero() bool {
return strings.TrimSpace(string(id)) == ""
}
// Validate reports whether TemplateID is non-empty and already normalized for
// domain use.
func (id TemplateID) Validate() error {
return validateToken("template id", string(id))
}
// IdempotencyKey stores the caller-owned key used to deduplicate accepted
// delivery commands.
type IdempotencyKey string
// String returns IdempotencyKey as its stored string.
func (key IdempotencyKey) String() string {
return string(key)
}
// IsZero reports whether IdempotencyKey does not contain a usable value.
func (key IdempotencyKey) IsZero() bool {
return strings.TrimSpace(string(key)) == ""
}
// Validate reports whether IdempotencyKey is non-empty and already normalized
// for domain use.
func (key IdempotencyKey) Validate() error {
return validateToken("idempotency key", string(key))
}
// Email stores one normalized recipient or reply-to address.
type Email string
// String returns Email as its stored canonical string.
func (email Email) String() string {
return string(email)
}
// IsZero reports whether Email does not contain a usable address.
func (email Email) IsZero() bool {
return strings.TrimSpace(string(email)) == ""
}
// Validate reports whether Email is non-empty, trimmed, and matches the same
// single-address syntax expected by the trusted Mail Service contracts.
func (email Email) Validate() error {
raw := string(email)
if err := validateToken("email", raw); err != nil {
return err
}
parsedAddress, err := mail.ParseAddress(raw)
if err != nil || parsedAddress.Name != "" || parsedAddress.Address != raw {
return fmt.Errorf("email %q must be a single valid email address", raw)
}
return nil
}
// Locale stores one canonical BCP 47 language tag used by template selection
// and rendering.
type Locale string
// ParseLocale validates value as a BCP 47 language tag and returns the
// canonical stored representation used by the Mail Service domain model.
func ParseLocale(value string) (Locale, error) {
if err := validateToken("locale", value); err != nil {
return "", err
}
tag, err := language.Parse(value)
if err != nil {
return "", fmt.Errorf("locale %q must be a valid BCP 47 language tag: %w", value, err)
}
return Locale(tag.String()), nil
}
// String returns Locale as its stored canonical string.
func (locale Locale) String() string {
return string(locale)
}
// IsZero reports whether Locale does not contain a usable value.
func (locale Locale) IsZero() bool {
return strings.TrimSpace(string(locale)) == ""
}
// Validate reports whether Locale stores a canonical BCP 47 language tag.
func (locale Locale) Validate() error {
raw := string(locale)
if err := validateToken("locale", raw); err != nil {
return err
}
tag, err := language.Parse(raw)
if err != nil {
return fmt.Errorf("locale %q must be a valid BCP 47 language tag: %w", raw, err)
}
canonical := tag.String()
if raw != canonical {
return fmt.Errorf("locale %q must use canonical BCP 47 form %q", raw, canonical)
}
return nil
}
// AttachmentMetadata stores only the durable audit metadata kept for one
// accepted attachment. Raw bytes remain outside the long-lived domain model.
type AttachmentMetadata struct {
// Filename stores the user-facing attachment filename.
Filename string
// ContentType stores the MIME media type used for SMTP body construction.
ContentType string
// SizeBytes stores the decoded payload size in bytes.
SizeBytes int64
}
// Validate reports whether AttachmentMetadata contains a complete attachment
// audit entry.
func (metadata AttachmentMetadata) Validate() error {
if err := validateToken("attachment filename", metadata.Filename); err != nil {
return err
}
if err := validateToken("attachment content type", metadata.ContentType); err != nil {
return err
}
if _, _, err := mime.ParseMediaType(metadata.ContentType); err != nil {
return fmt.Errorf("attachment content type %q must be a valid MIME media type: %w", metadata.ContentType, err)
}
if metadata.SizeBytes < 0 {
return fmt.Errorf("attachment size bytes must not be negative")
}
return nil
}
// ValidateTimestamp reports whether value is present.
func ValidateTimestamp(name string, value time.Time) error {
if value.IsZero() {
return fmt.Errorf("%s must not be zero", name)
}
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
}
}
+190
View File
@@ -0,0 +1,190 @@
package common
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestIdentifierValidate(t *testing.T) {
t.Parallel()
tests := []struct {
name string
run func() error
wantErr bool
}{
{
name: "valid delivery id",
run: func() error {
return DeliveryID("delivery-123").Validate()
},
},
{
name: "valid template id",
run: func() error {
return TemplateID("auth.login_code").Validate()
},
},
{
name: "valid idempotency key",
run: func() error {
return IdempotencyKey("notification:delivery-123").Validate()
},
},
{
name: "empty delivery id",
run: func() error {
return DeliveryID("").Validate()
},
wantErr: true,
},
{
name: "template id with whitespace",
run: func() error {
return TemplateID(" auth.login_code ").Validate()
},
wantErr: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := tt.run()
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
})
}
}
func TestEmailValidate(t *testing.T) {
t.Parallel()
tests := []struct {
name string
value Email
wantErr bool
}{
{name: "valid", value: Email("pilot@example.com")},
{name: "empty", value: Email(""), wantErr: true},
{name: "display name forbidden", value: Email("Pilot <pilot@example.com>"), wantErr: true},
{name: "whitespace forbidden", value: Email(" pilot@example.com "), wantErr: true},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := tt.value.Validate()
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
})
}
}
func TestParseLocale(t *testing.T) {
t.Parallel()
value, err := ParseLocale("fr-fr")
require.NoError(t, err)
require.Equal(t, Locale("fr-FR"), value)
require.NoError(t, value.Validate())
}
func TestLocaleValidate(t *testing.T) {
t.Parallel()
tests := []struct {
name string
value Locale
wantErr bool
}{
{name: "canonical language", value: Locale("en")},
{name: "canonical regional", value: Locale("fr-FR")},
{name: "non canonical", value: Locale("fr-fr"), wantErr: true},
{name: "invalid syntax", value: Locale("not a locale"), wantErr: true},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := tt.value.Validate()
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
})
}
}
func TestAttachmentMetadataValidate(t *testing.T) {
t.Parallel()
tests := []struct {
name string
value AttachmentMetadata
wantErr bool
}{
{
name: "valid",
value: AttachmentMetadata{
Filename: "report.txt",
ContentType: "text/plain; charset=utf-8",
SizeBytes: 512,
},
},
{
name: "invalid content type",
value: AttachmentMetadata{
Filename: "report.txt",
ContentType: "plain text",
SizeBytes: 512,
},
wantErr: true,
},
{
name: "negative size",
value: AttachmentMetadata{
Filename: "report.txt",
ContentType: "text/plain",
SizeBytes: -1,
},
wantErr: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := tt.value.Validate()
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
})
}
}
+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
}
+321
View File
@@ -0,0 +1,321 @@
package delivery
import (
"testing"
"time"
"galaxy/mail/internal/domain/attempt"
"galaxy/mail/internal/domain/common"
"github.com/stretchr/testify/require"
)
func TestStatusCanTransitionTo(t *testing.T) {
t.Parallel()
tests := []struct {
name string
from Status
to Status
want bool
}{
{name: "accepted to queued", from: StatusAccepted, to: StatusQueued, want: true},
{name: "accepted to suppressed", from: StatusAccepted, to: StatusSuppressed, want: true},
{name: "accepted to sent", from: StatusAccepted, to: StatusSent, want: false},
{name: "queued to rendered", from: StatusQueued, to: StatusRendered, want: true},
{name: "queued to sending", from: StatusQueued, to: StatusSending, want: true},
{name: "queued to failed", from: StatusQueued, to: StatusFailed, want: true},
{name: "rendered to sending", from: StatusRendered, to: StatusSending, want: true},
{name: "rendered to failed", from: StatusRendered, to: StatusFailed, want: true},
{name: "sending to sent", from: StatusSending, to: StatusSent, want: true},
{name: "sending to dead letter", from: StatusSending, to: StatusDeadLetter, want: true},
{name: "failed terminal", from: StatusFailed, to: StatusDeadLetter, want: false},
{name: "dead letter terminal", from: StatusDeadLetter, to: StatusQueued, want: false},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
require.Equal(t, tt.want, tt.from.CanTransitionTo(tt.to))
})
}
}
func TestStatusTerminalAndResend(t *testing.T) {
t.Parallel()
require.False(t, StatusAccepted.IsTerminal())
require.False(t, StatusQueued.AllowsResend())
require.True(t, StatusSent.IsTerminal())
require.True(t, StatusSent.AllowsResend())
require.True(t, StatusSuppressed.AllowsResend())
require.True(t, StatusFailed.AllowsResend())
require.True(t, StatusDeadLetter.AllowsResend())
}
func TestDeliveryValidate(t *testing.T) {
t.Parallel()
base := validRenderedDelivery(t)
templateQueued := validTemplateQueuedDelivery(t)
tests := []struct {
name string
record Delivery
wantErr bool
}{
{name: "valid rendered delivery", record: base},
{name: "valid template queued delivery", record: templateQueued},
{
name: "operator resend requires parent id",
record: func() Delivery {
record := base
record.Source = SourceOperatorResend
record.ResendParentDeliveryID = ""
return record
}(),
wantErr: true,
},
{
name: "non resend must not carry parent id",
record: func() Delivery {
record := base
record.ResendParentDeliveryID = common.DeliveryID("delivery-parent")
return record
}(),
wantErr: true,
},
{
name: "rendered status requires template mode",
record: func() Delivery {
record := base
record.Status = StatusRendered
record.UpdatedAt = record.CreatedAt.Add(time.Minute)
record.SentAt = nil
return record
}(),
wantErr: true,
},
{
name: "rendered payload requires materialized content",
record: func() Delivery {
record := base
record.Content = Content{}
return record
}(),
wantErr: true,
},
{
name: "template mode requires template id",
record: func() Delivery {
record := templateQueued
record.TemplateID = ""
return record
}(),
wantErr: true,
},
{
name: "template mode requires locale",
record: func() Delivery {
record := templateQueued
record.Locale = ""
return record
}(),
wantErr: true,
},
{
name: "template mode requires template variables",
record: func() Delivery {
record := templateQueued
record.TemplateVariables = nil
return record
}(),
wantErr: true,
},
{
name: "template rendered requires content",
record: func() Delivery {
record := templateQueued
record.Status = StatusRendered
record.UpdatedAt = record.CreatedAt.Add(2 * time.Minute)
record.Content = Content{}
return record
}(),
wantErr: true,
},
{
name: "non terminal must not carry terminal timestamps",
record: func() Delivery {
record := templateQueued
record.FailedAt = ptrTime(record.CreatedAt.Add(time.Minute))
return record
}(),
wantErr: true,
},
{
name: "rendered delivery must not contain template variables",
record: func() Delivery {
record := base
record.TemplateVariables = map[string]any{"code": "123456"}
return record
}(),
wantErr: true,
},
{
name: "template variables must be json serializable",
record: func() Delivery {
record := templateQueued
record.TemplateVariables = map[string]any{"invalid": func() {}}
return record
}(),
wantErr: true,
},
{
name: "failed requires failed at",
record: func() Delivery {
record := templateQueued
record.Status = StatusFailed
record.UpdatedAt = record.CreatedAt.Add(2 * time.Minute)
return record
}(),
wantErr: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := tt.record.Validate()
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
})
}
}
func TestValidateDeadLetterState(t *testing.T) {
t.Parallel()
record := validDeadLetterDelivery(t)
entry := validDeadLetterEntry(t, record)
require.NoError(t, ValidateDeadLetterState(record, &entry))
err := ValidateDeadLetterState(record, nil)
require.Error(t, err)
failed := validTemplateQueuedDelivery(t)
failed.Status = StatusFailed
failed.UpdatedAt = failed.CreatedAt.Add(2 * time.Minute)
failed.FailedAt = ptrTime(failed.CreatedAt.Add(2 * time.Minute))
require.NoError(t, ValidateDeadLetterState(failed, nil))
require.Error(t, ValidateDeadLetterState(failed, &entry))
mismatched := entry
mismatched.DeliveryID = common.DeliveryID("delivery-other")
require.Error(t, ValidateDeadLetterState(record, &mismatched))
}
func validRenderedDelivery(t *testing.T) Delivery {
t.Helper()
createdAt := time.Unix(1_775_121_700, 0).UTC()
sentAt := createdAt.Add(5 * time.Minute)
record := Delivery{
DeliveryID: common.DeliveryID("delivery-123"),
Source: SourceNotification,
PayloadMode: PayloadModeRendered,
Envelope: validEnvelope(),
Content: Content{Subject: "Turn ready", TextBody: "Turn 54 is ready."},
Attachments: []common.AttachmentMetadata{{Filename: "report.txt", ContentType: "text/plain", SizeBytes: 64}},
TemplateVariables: nil,
IdempotencyKey: common.IdempotencyKey("notification:delivery-123"),
Status: StatusSent,
AttemptCount: 1,
LastAttemptStatus: attempt.StatusProviderAccepted,
ProviderSummary: "queued by provider",
CreatedAt: createdAt,
UpdatedAt: sentAt,
SentAt: &sentAt,
}
require.NoError(t, record.Validate())
return record
}
func validTemplateQueuedDelivery(t *testing.T) Delivery {
t.Helper()
createdAt := time.Unix(1_775_121_700, 0).UTC()
locale, err := common.ParseLocale("fr-fr")
require.NoError(t, err)
record := Delivery{
DeliveryID: common.DeliveryID("delivery-124"),
Source: SourceNotification,
PayloadMode: PayloadModeTemplate,
TemplateID: common.TemplateID("game.turn_ready"),
Envelope: validEnvelope(),
Locale: locale,
TemplateVariables: map[string]any{
"turn_number": float64(54),
},
IdempotencyKey: common.IdempotencyKey("notification:delivery-124"),
Status: StatusQueued,
CreatedAt: createdAt,
UpdatedAt: createdAt.Add(time.Minute),
}
require.NoError(t, record.Validate())
return record
}
func validDeadLetterDelivery(t *testing.T) Delivery {
t.Helper()
record := validTemplateQueuedDelivery(t)
record.Status = StatusDeadLetter
record.AttemptCount = 3
record.LastAttemptStatus = attempt.StatusTimedOut
record.UpdatedAt = record.CreatedAt.Add(10 * time.Minute)
record.DeadLetteredAt = ptrTime(record.CreatedAt.Add(10 * time.Minute))
require.NoError(t, record.Validate())
return record
}
func validDeadLetterEntry(t *testing.T, record Delivery) DeadLetterEntry {
t.Helper()
entry := DeadLetterEntry{
DeliveryID: record.DeliveryID,
FinalAttemptNo: 3,
FailureClassification: "retry_exhausted",
ProviderSummary: "smtp timeout",
CreatedAt: record.DeadLetteredAt.Add(time.Second),
RecoveryHint: "check SMTP connectivity",
}
require.NoError(t, entry.ValidateFor(record))
return entry
}
func validEnvelope() Envelope {
return Envelope{
To: []common.Email{"pilot@example.com"},
}
}
func ptrTime(value time.Time) *time.Time {
return &value
}
+74
View File
@@ -0,0 +1,74 @@
// Package idempotency defines the deduplication record used by Mail Service
// acceptance flows.
package idempotency
import (
"fmt"
"strings"
"time"
"galaxy/mail/internal/domain/common"
"galaxy/mail/internal/domain/delivery"
)
// Record stores the first accepted fingerprint bound to one `(source,
// idempotency_key)` scope.
type Record struct {
// Source stores the frozen delivery source vocabulary value.
Source delivery.Source
// IdempotencyKey stores the caller-owned deduplication key.
IdempotencyKey common.IdempotencyKey
// DeliveryID stores the accepted delivery linked to the scope.
DeliveryID common.DeliveryID
// RequestFingerprint stores the stable fingerprint of the first accepted
// request.
RequestFingerprint string
// CreatedAt stores when the deduplication record was created.
CreatedAt time.Time
// ExpiresAt stores when the deduplication record becomes invalid.
ExpiresAt time.Time
}
// Validate reports whether Record satisfies the frozen Stage 2 structural
// invariants.
func (record Record) Validate() error {
if !record.Source.IsKnown() {
return fmt.Errorf("idempotency source %q is unsupported", record.Source)
}
if err := record.IdempotencyKey.Validate(); err != nil {
return fmt.Errorf("idempotency key: %w", err)
}
if err := record.DeliveryID.Validate(); err != nil {
return fmt.Errorf("idempotency delivery id: %w", err)
}
if err := validateToken("idempotency request fingerprint", record.RequestFingerprint); err != nil {
return err
}
if err := common.ValidateTimestamp("idempotency created at", record.CreatedAt); err != nil {
return err
}
if err := common.ValidateTimestamp("idempotency expires at", record.ExpiresAt); err != nil {
return err
}
if !record.ExpiresAt.After(record.CreatedAt) {
return fmt.Errorf("idempotency expires at must be after created at")
}
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
}
}
@@ -0,0 +1,74 @@
package idempotency
import (
"testing"
"time"
"galaxy/mail/internal/domain/common"
"galaxy/mail/internal/domain/delivery"
"github.com/stretchr/testify/require"
)
func TestRecordValidate(t *testing.T) {
t.Parallel()
createdAt := time.Unix(1_775_121_700, 0).UTC()
tests := []struct {
name string
record Record
wantErr bool
}{
{
name: "valid",
record: Record{
Source: delivery.SourceNotification,
IdempotencyKey: common.IdempotencyKey("notification:delivery-123"),
DeliveryID: common.DeliveryID("delivery-123"),
RequestFingerprint: "sha256:abcdef",
CreatedAt: createdAt,
ExpiresAt: createdAt.Add(7 * 24 * time.Hour),
},
},
{
name: "expires at must be after created at",
record: Record{
Source: delivery.SourceNotification,
IdempotencyKey: common.IdempotencyKey("notification:delivery-123"),
DeliveryID: common.DeliveryID("delivery-123"),
RequestFingerprint: "sha256:abcdef",
CreatedAt: createdAt,
ExpiresAt: createdAt,
},
wantErr: true,
},
{
name: "fingerprint required",
record: Record{
Source: delivery.SourceNotification,
IdempotencyKey: common.IdempotencyKey("notification:delivery-123"),
DeliveryID: common.DeliveryID("delivery-123"),
CreatedAt: createdAt,
ExpiresAt: createdAt.Add(time.Hour),
},
wantErr: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := tt.record.Validate()
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
})
}
}
@@ -0,0 +1,130 @@
// Package malformedcommand defines the operator-visible record used for
// malformed asynchronous generic delivery commands.
package malformedcommand
import (
"encoding/json"
"fmt"
"strings"
"time"
"galaxy/mail/internal/domain/common"
)
// FailureCode identifies the stable malformed-command rejection reason.
type FailureCode string
const (
// FailureCodeInvalidEnvelope reports that the command could not be accepted
// because the recipient envelope was invalid.
FailureCodeInvalidEnvelope FailureCode = "invalid_envelope"
// FailureCodeInvalidPayload reports that the command payload could not be
// decoded or validated.
FailureCodeInvalidPayload FailureCode = "invalid_payload"
// FailureCodeInvalidCommand reports that the top-level stream envelope was
// malformed or unsupported.
FailureCodeInvalidCommand FailureCode = "invalid_command"
// FailureCodeIdempotencyConflict reports that the stream command reused an
// existing idempotency scope with a different request fingerprint.
FailureCodeIdempotencyConflict FailureCode = "idempotency_conflict"
)
// IsKnown reports whether code belongs to the frozen malformed-command
// rejection surface.
func (code FailureCode) IsKnown() bool {
switch code {
case FailureCodeInvalidEnvelope,
FailureCodeInvalidPayload,
FailureCodeInvalidCommand,
FailureCodeIdempotencyConflict:
return true
default:
return false
}
}
// Entry stores one operator-visible malformed asynchronous command record.
type Entry struct {
// StreamEntryID stores the Redis Stream entry identifier of the malformed
// command.
StreamEntryID string
// DeliveryID stores the optional raw delivery identifier extracted from the
// stream entry when available.
DeliveryID string
// Source stores the optional raw source value extracted from the stream
// entry when available.
Source string
// IdempotencyKey stores the optional raw idempotency key extracted from the
// stream entry when available.
IdempotencyKey string
// FailureCode stores the stable malformed-command rejection reason.
FailureCode FailureCode
// FailureMessage stores the detailed validation or decoding failure.
FailureMessage string
// RawFields stores the raw top-level stream fields captured for later
// operator inspection.
RawFields map[string]any
// RecordedAt stores when the malformed command was durably recorded.
RecordedAt time.Time
}
// Validate reports whether entry contains a complete malformed-command record.
func (entry Entry) Validate() error {
if strings.TrimSpace(entry.StreamEntryID) == "" {
return fmt.Errorf("malformed command stream entry id must not be empty")
}
if !entry.FailureCode.IsKnown() {
return fmt.Errorf("malformed command failure code %q is unsupported", entry.FailureCode)
}
if strings.TrimSpace(entry.FailureMessage) == "" {
return fmt.Errorf("malformed command failure message must not be empty")
}
if strings.TrimSpace(entry.FailureMessage) != entry.FailureMessage {
return fmt.Errorf("malformed command failure message must not contain surrounding whitespace")
}
if entry.RawFields == nil {
return fmt.Errorf("malformed command raw fields must not be nil")
}
if err := validateJSONObject("malformed command raw fields", entry.RawFields); err != nil {
return err
}
if err := common.ValidateTimestamp("malformed command recorded at", entry.RecordedAt); err != nil {
return err
}
return nil
}
func validateJSONObject(name string, value map[string]any) error {
if value == nil {
return fmt.Errorf("%s must not be nil", name)
}
payload, err := json.Marshal(value)
if err != nil {
return fmt.Errorf("%s: %w", name, err)
}
if string(payload) == "null" {
return fmt.Errorf("%s must encode as a JSON object", name)
}
var decoded map[string]any
if err := json.Unmarshal(payload, &decoded); err != nil {
return fmt.Errorf("%s: %w", name, err)
}
if decoded == nil {
return fmt.Errorf("%s must encode as a JSON object", name)
}
return nil
}
@@ -0,0 +1,61 @@
package malformedcommand
import (
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestEntryValidate(t *testing.T) {
t.Parallel()
entry := Entry{
StreamEntryID: "1775121700000-0",
DeliveryID: "mail-123",
Source: "notification",
IdempotencyKey: "notification:mail-123",
FailureCode: FailureCodeInvalidPayload,
FailureMessage: "payload_json.subject is required",
RawFields: map[string]any{
"delivery_id": "mail-123",
"source": "notification",
"payload_mode": "rendered",
"idempotency_key": "notification:mail-123",
},
RecordedAt: time.Unix(1_775_121_700, 0).UTC(),
}
require.NoError(t, entry.Validate())
}
func TestEntryValidateRejectsInvalidValue(t *testing.T) {
t.Parallel()
entry := Entry{
StreamEntryID: "1775121700000-0",
FailureCode: FailureCode("unsupported"),
FailureMessage: "failure",
RawFields: map[string]any{},
RecordedAt: time.Unix(1_775_121_700, 0).UTC(),
}
err := entry.Validate()
require.Error(t, err)
require.ErrorContains(t, err, "failure code")
}
func TestEntryValidateRejectsNilRawFields(t *testing.T) {
t.Parallel()
entry := Entry{
StreamEntryID: "1775121700000-0",
FailureCode: FailureCodeInvalidCommand,
FailureMessage: "missing required fields",
RecordedAt: time.Unix(1_775_121_700, 0).UTC(),
}
err := entry.Validate()
require.Error(t, err)
require.ErrorContains(t, err, "raw fields")
}
+65
View File
@@ -0,0 +1,65 @@
// Package template defines the logical template entity used by the
// filesystem-backed Mail Service template catalog.
package template
import (
"fmt"
"strings"
"galaxy/mail/internal/domain/common"
)
// Template stores one locale-specific template bundle.
type Template struct {
// TemplateID identifies the template family.
TemplateID common.TemplateID
// Locale stores the canonical locale of the template variant.
Locale common.Locale
// SubjectTemplate stores the subject template source.
SubjectTemplate string
// TextTemplate stores the plaintext body template source.
TextTemplate string
// HTMLTemplate stores the optional HTML body template source.
HTMLTemplate string
// Version stores the template version marker projected into the domain
// model.
Version string
}
// Validate reports whether Template satisfies the frozen Stage 2 structural
// invariants.
func (record Template) Validate() error {
if err := record.TemplateID.Validate(); err != nil {
return fmt.Errorf("template id: %w", err)
}
if err := record.Locale.Validate(); err != nil {
return fmt.Errorf("template locale: %w", err)
}
if record.SubjectTemplate == "" {
return fmt.Errorf("template subject template must not be empty")
}
if record.TextTemplate == "" {
return fmt.Errorf("template text template must not be empty")
}
if err := validateToken("template version", record.Version); err != nil {
return err
}
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
}
}
@@ -0,0 +1,71 @@
package template
import (
"testing"
"galaxy/mail/internal/domain/common"
"github.com/stretchr/testify/require"
)
func TestTemplateValidate(t *testing.T) {
t.Parallel()
locale, err := common.ParseLocale("en-us")
require.NoError(t, err)
tests := []struct {
name string
record Template
wantErr bool
}{
{
name: "valid",
record: Template{
TemplateID: common.TemplateID("auth.login_code"),
Locale: locale,
SubjectTemplate: "Your code",
TextTemplate: "Code: {{.Code}}",
HTMLTemplate: "<p>Code: {{.Code}}</p>",
Version: "sha256:abcd",
},
},
{
name: "non canonical locale rejected",
record: Template{
TemplateID: common.TemplateID("auth.login_code"),
Locale: common.Locale("en-us"),
SubjectTemplate: "Your code",
TextTemplate: "Code: {{.Code}}",
Version: "sha256:abcd",
},
wantErr: true,
},
{
name: "missing subject template",
record: Template{
TemplateID: common.TemplateID("auth.login_code"),
Locale: locale,
TextTemplate: "Code: {{.Code}}",
Version: "sha256:abcd",
},
wantErr: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := tt.record.Validate()
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
})
}
}