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
@@ -0,0 +1,366 @@
// Package resenddelivery implements trusted operator resend by clone creation.
package resenddelivery
import (
"context"
"errors"
"fmt"
"log/slog"
"time"
"galaxy/mail/internal/domain/attempt"
"galaxy/mail/internal/domain/common"
deliverydomain "galaxy/mail/internal/domain/delivery"
"galaxy/mail/internal/logging"
"galaxy/mail/internal/service/acceptgenericdelivery"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
oteltrace "go.opentelemetry.io/otel/trace"
)
var (
// ErrNotFound reports that the requested original delivery does not exist.
ErrNotFound = errors.New("resend delivery not found")
// ErrNotAllowed reports that the original delivery is not in a terminal
// state and therefore cannot be cloned for resend.
ErrNotAllowed = errors.New("resend delivery not allowed")
// ErrServiceUnavailable reports that clone creation could not load or
// persist durable state safely.
ErrServiceUnavailable = errors.New("resend delivery service unavailable")
)
const tracerName = "galaxy/mail/resenddelivery"
// Input stores one trusted resend request by original delivery identifier.
type Input struct {
// DeliveryID stores the original accepted delivery identifier to clone.
DeliveryID common.DeliveryID
}
// Validate reports whether input contains a complete resend target.
func (input Input) Validate() error {
if err := input.DeliveryID.Validate(); err != nil {
return fmt.Errorf("delivery id: %w", err)
}
return nil
}
// Result stores the new clone delivery identifier created by resend.
type Result struct {
// DeliveryID stores the identifier of the newly created clone delivery.
DeliveryID common.DeliveryID
}
// Validate reports whether result contains a usable clone delivery identifier.
func (result Result) Validate() error {
if err := result.DeliveryID.Validate(); err != nil {
return fmt.Errorf("delivery id: %w", err)
}
return nil
}
// CreateResendInput stores the durable write set required for one clone-only
// resend operation.
type CreateResendInput struct {
// Delivery stores the new cloned delivery record.
Delivery deliverydomain.Delivery
// FirstAttempt stores the initial scheduled attempt of the clone.
FirstAttempt attempt.Attempt
// DeliveryPayload stores the optional cloned raw attachment payload bundle.
DeliveryPayload *acceptgenericdelivery.DeliveryPayload
}
// Validate reports whether input contains a complete resend write set.
func (input CreateResendInput) Validate() error {
if err := input.Delivery.Validate(); err != nil {
return fmt.Errorf("delivery: %w", err)
}
if input.Delivery.Source != deliverydomain.SourceOperatorResend {
return fmt.Errorf("delivery source must be %q", deliverydomain.SourceOperatorResend)
}
if input.Delivery.Status != deliverydomain.StatusQueued {
return fmt.Errorf("delivery status must be %q", deliverydomain.StatusQueued)
}
if input.Delivery.AttemptCount != 1 {
return errors.New("delivery attempt count must equal 1")
}
if input.Delivery.LastAttemptStatus != "" {
return errors.New("delivery last attempt status must be empty")
}
if input.Delivery.ProviderSummary != "" {
return errors.New("delivery provider summary must be empty")
}
if input.Delivery.SentAt != nil || input.Delivery.SuppressedAt != nil || input.Delivery.FailedAt != nil || input.Delivery.DeadLetteredAt != nil {
return errors.New("delivery terminal timestamps must be empty")
}
if err := input.FirstAttempt.Validate(); err != nil {
return fmt.Errorf("first attempt: %w", err)
}
if input.FirstAttempt.DeliveryID != input.Delivery.DeliveryID {
return errors.New("first attempt delivery id must match delivery id")
}
if input.FirstAttempt.AttemptNo != 1 {
return errors.New("first attempt number must equal 1")
}
if input.FirstAttempt.Status != attempt.StatusScheduled {
return fmt.Errorf("first attempt status must be %q", attempt.StatusScheduled)
}
if input.DeliveryPayload != nil {
if err := input.DeliveryPayload.Validate(); err != nil {
return fmt.Errorf("delivery payload: %w", err)
}
if input.DeliveryPayload.DeliveryID != input.Delivery.DeliveryID {
return errors.New("delivery payload delivery id must match delivery id")
}
}
return nil
}
// Store provides the durable delivery state required by clone-only resend.
type Store interface {
// GetDelivery loads one accepted delivery by its identifier.
GetDelivery(context.Context, common.DeliveryID) (deliverydomain.Delivery, bool, error)
// GetDeliveryPayload loads the raw attachment payload bundle of deliveryID
// when one exists.
GetDeliveryPayload(context.Context, common.DeliveryID) (acceptgenericdelivery.DeliveryPayload, bool, error)
// CreateResend atomically creates the cloned delivery, its first attempt,
// the optional cloned delivery payload, and the related delivery indexes.
CreateResend(context.Context, CreateResendInput) error
}
// DeliveryIDGenerator describes the source of new internal delivery
// identifiers.
type DeliveryIDGenerator interface {
// NewDeliveryID returns one new internal delivery identifier.
NewDeliveryID() (common.DeliveryID, error)
}
// Clock provides the current wall-clock time.
type Clock interface {
// Now returns the current time.
Now() time.Time
}
// Telemetry records low-cardinality resend metrics.
type Telemetry interface {
// RecordDeliveryStatusTransition records one durable delivery status
// transition.
RecordDeliveryStatusTransition(context.Context, string, string)
}
// Config stores the dependencies used by Service.
type Config struct {
// Store owns durable resend state.
Store Store
// DeliveryIDGenerator builds internal clone identifiers.
DeliveryIDGenerator DeliveryIDGenerator
// Clock provides wall-clock timestamps.
Clock Clock
// Telemetry records low-cardinality resend metrics.
Telemetry Telemetry
// TracerProvider constructs the application span recorder used by resend.
TracerProvider oteltrace.TracerProvider
// Logger writes structured resend logs.
Logger *slog.Logger
}
// Service executes clone-only trusted resend requests.
type Service struct {
store Store
deliveryIDGenerator DeliveryIDGenerator
clock Clock
telemetry Telemetry
tracerProvider oteltrace.TracerProvider
logger *slog.Logger
}
// New constructs Service from cfg.
func New(cfg Config) (*Service, error) {
switch {
case cfg.Store == nil:
return nil, errors.New("new resend delivery service: nil store")
case cfg.DeliveryIDGenerator == nil:
return nil, errors.New("new resend delivery service: nil delivery id generator")
case cfg.Clock == nil:
return nil, errors.New("new resend delivery service: nil clock")
default:
tracerProvider := cfg.TracerProvider
if tracerProvider == nil {
tracerProvider = otel.GetTracerProvider()
}
logger := cfg.Logger
if logger == nil {
logger = slog.Default()
}
return &Service{
store: cfg.Store,
deliveryIDGenerator: cfg.DeliveryIDGenerator,
clock: cfg.Clock,
telemetry: cfg.Telemetry,
tracerProvider: tracerProvider,
logger: logger.With("component", "resend_delivery"),
}, nil
}
}
// Execute clones one terminal delivery into a new queued delivery with a
// fresh first attempt.
func (service *Service) Execute(ctx context.Context, input Input) (Result, error) {
if ctx == nil {
return Result{}, errors.New("execute resend delivery: nil context")
}
if service == nil {
return Result{}, errors.New("execute resend delivery: nil service")
}
if err := input.Validate(); err != nil {
return Result{}, fmt.Errorf("execute resend delivery: %w", err)
}
ctx, span := service.tracerProvider.Tracer(tracerName).Start(
ctx,
"mail.resend_delivery",
oteltrace.WithAttributes(attribute.String("mail.parent_delivery_id", input.DeliveryID.String())),
)
defer span.End()
original, found, err := service.store.GetDelivery(ctx, input.DeliveryID)
switch {
case err != nil:
return Result{}, fmt.Errorf("%w: load original delivery: %v", ErrServiceUnavailable, err)
case !found:
return Result{}, ErrNotFound
case !original.Status.AllowsResend():
return Result{}, ErrNotAllowed
}
now := service.clock.Now().UTC().Truncate(time.Millisecond)
cloneID, err := service.deliveryIDGenerator.NewDeliveryID()
if err != nil {
return Result{}, fmt.Errorf("%w: generate delivery id: %v", ErrServiceUnavailable, err)
}
clone := buildClonedDelivery(original, cloneID, now)
firstAttempt := attempt.Attempt{
DeliveryID: cloneID,
AttemptNo: 1,
ScheduledFor: now,
Status: attempt.StatusScheduled,
}
var clonedPayload *acceptgenericdelivery.DeliveryPayload
if len(original.Attachments) > 0 {
payload, found, err := service.store.GetDeliveryPayload(ctx, original.DeliveryID)
switch {
case err != nil:
return Result{}, fmt.Errorf("%w: load original delivery payload: %v", ErrServiceUnavailable, err)
case !found:
return Result{}, fmt.Errorf("%w: missing original delivery payload for %q", ErrServiceUnavailable, original.DeliveryID)
default:
cloned := cloneDeliveryPayload(payload, cloneID)
clonedPayload = &cloned
}
}
createInput := CreateResendInput{
Delivery: clone,
FirstAttempt: firstAttempt,
DeliveryPayload: clonedPayload,
}
if err := createInput.Validate(); err != nil {
return Result{}, fmt.Errorf("%w: build resend input: %v", ErrServiceUnavailable, err)
}
if err := service.store.CreateResend(ctx, createInput); err != nil {
return Result{}, fmt.Errorf("%w: create resend clone: %v", ErrServiceUnavailable, err)
}
service.recordStatusTransition(ctx, createInput.Delivery)
result := Result{DeliveryID: cloneID}
if err := result.Validate(); err != nil {
return Result{}, fmt.Errorf("%w: invalid result: %v", ErrServiceUnavailable, err)
}
span.SetAttributes(
attribute.String("mail.delivery_id", cloneID.String()),
attribute.String("mail.source", string(createInput.Delivery.Source)),
)
logArgs := logging.DeliveryAttrs(createInput.Delivery)
logArgs = append(logArgs,
"parent_delivery_id", original.DeliveryID.String(),
"status", string(createInput.Delivery.Status),
)
logArgs = append(logArgs, logging.TraceAttrsFromContext(ctx)...)
service.logger.Info("resend clone created", logArgs...)
return result, nil
}
func (service *Service) recordStatusTransition(ctx context.Context, record deliverydomain.Delivery) {
if service == nil || service.telemetry == nil {
return
}
service.telemetry.RecordDeliveryStatusTransition(ctx, string(record.Status), string(record.Source))
}
func buildClonedDelivery(original deliverydomain.Delivery, cloneID common.DeliveryID, now time.Time) deliverydomain.Delivery {
return deliverydomain.Delivery{
DeliveryID: cloneID,
ResendParentDeliveryID: original.DeliveryID,
Source: deliverydomain.SourceOperatorResend,
PayloadMode: original.PayloadMode,
TemplateID: original.TemplateID,
Envelope: deliverydomain.Envelope{
To: append([]common.Email(nil), original.Envelope.To...),
Cc: append([]common.Email(nil), original.Envelope.Cc...),
Bcc: append([]common.Email(nil), original.Envelope.Bcc...),
ReplyTo: append([]common.Email(nil), original.Envelope.ReplyTo...),
},
Content: original.Content,
Attachments: append([]common.AttachmentMetadata(nil), original.Attachments...),
Locale: original.Locale,
LocaleFallbackUsed: original.LocaleFallbackUsed,
TemplateVariables: cloneJSONObject(original.TemplateVariables),
IdempotencyKey: common.IdempotencyKey("operator:resend:" + original.DeliveryID.String()),
Status: deliverydomain.StatusQueued,
AttemptCount: 1,
CreatedAt: now,
UpdatedAt: now,
}
}
func cloneDeliveryPayload(payload acceptgenericdelivery.DeliveryPayload, cloneID common.DeliveryID) acceptgenericdelivery.DeliveryPayload {
cloned := acceptgenericdelivery.DeliveryPayload{
DeliveryID: cloneID,
Attachments: make([]acceptgenericdelivery.AttachmentPayload, len(payload.Attachments)),
}
copy(cloned.Attachments, payload.Attachments)
return cloned
}
func cloneJSONObject(value map[string]any) map[string]any {
if value == nil {
return nil
}
cloned := make(map[string]any, len(value))
for key, entry := range value {
cloned[key] = entry
}
return cloned
}
@@ -0,0 +1,273 @@
package resenddelivery
import (
"bytes"
"context"
"log/slog"
"testing"
"time"
"galaxy/mail/internal/domain/attempt"
"galaxy/mail/internal/domain/common"
deliverydomain "galaxy/mail/internal/domain/delivery"
"galaxy/mail/internal/service/acceptgenericdelivery"
"github.com/stretchr/testify/require"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
)
func TestServiceExecuteRejectsNonTerminalStatus(t *testing.T) {
t.Parallel()
tests := []deliverydomain.Status{
deliverydomain.StatusAccepted,
deliverydomain.StatusQueued,
deliverydomain.StatusRendered,
deliverydomain.StatusSending,
}
for _, status := range tests {
status := status
t.Run(string(status), func(t *testing.T) {
t.Parallel()
record := validOriginalDelivery()
record.Status = status
record.SentAt = nil
record.FailedAt = nil
record.DeadLetteredAt = nil
record.SuppressedAt = nil
require.NoError(t, record.Validate())
store := &stubStore{delivery: &record}
service := newTestService(t, Config{
Store: store,
DeliveryIDGenerator: &stubIDGenerator{ids: []common.DeliveryID{"clone-1"}},
Clock: stubClock{now: fixedNow()},
})
_, err := service.Execute(context.Background(), Input{DeliveryID: record.DeliveryID})
require.ErrorIs(t, err, ErrNotAllowed)
})
}
}
func TestServiceExecuteCreatesLinkedClone(t *testing.T) {
t.Parallel()
original := validOriginalDelivery()
originalCopy := original
payload := validPayload(original.DeliveryID)
store := &stubStore{
delivery: &original,
payload: &payload,
}
service := newTestService(t, Config{
Store: store,
DeliveryIDGenerator: &stubIDGenerator{ids: []common.DeliveryID{"clone-123"}},
Clock: stubClock{now: fixedNow()},
})
result, err := service.Execute(context.Background(), Input{DeliveryID: original.DeliveryID})
require.NoError(t, err)
require.Equal(t, Result{DeliveryID: common.DeliveryID("clone-123")}, result)
require.Len(t, store.createInputs, 1)
createInput := store.createInputs[0]
require.Equal(t, common.DeliveryID("clone-123"), createInput.Delivery.DeliveryID)
require.Equal(t, original.DeliveryID, createInput.Delivery.ResendParentDeliveryID)
require.Equal(t, deliverydomain.SourceOperatorResend, createInput.Delivery.Source)
require.Equal(t, common.IdempotencyKey("operator:resend:"+original.DeliveryID.String()), createInput.Delivery.IdempotencyKey)
require.Equal(t, deliverydomain.StatusQueued, createInput.Delivery.Status)
require.Equal(t, 1, createInput.Delivery.AttemptCount)
require.Empty(t, createInput.Delivery.LastAttemptStatus)
require.Nil(t, createInput.Delivery.SentAt)
require.Nil(t, createInput.Delivery.FailedAt)
require.Equal(t, attempt.StatusScheduled, createInput.FirstAttempt.Status)
require.Equal(t, 1, createInput.FirstAttempt.AttemptNo)
require.NotNil(t, createInput.DeliveryPayload)
require.Equal(t, common.DeliveryID("clone-123"), createInput.DeliveryPayload.DeliveryID)
require.Equal(t, payload.Attachments, createInput.DeliveryPayload.Attachments)
require.Equal(t, originalCopy, original)
}
func TestServiceExecuteLogsCloneCreationAndCreatesSpan(t *testing.T) {
t.Parallel()
original := validOriginalDelivery()
payload := validPayload(original.DeliveryID)
loggerBuffer := &bytes.Buffer{}
recorder := tracetest.NewSpanRecorder()
tracerProvider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(recorder))
telemetry := &stubTelemetry{}
store := &stubStore{
delivery: &original,
payload: &payload,
}
service := newTestService(t, Config{
Store: store,
DeliveryIDGenerator: &stubIDGenerator{ids: []common.DeliveryID{"clone-456"}},
Clock: stubClock{now: fixedNow()},
Telemetry: telemetry,
TracerProvider: tracerProvider,
Logger: slog.New(slog.NewJSONHandler(loggerBuffer, nil)),
})
_, err := service.Execute(context.Background(), Input{DeliveryID: original.DeliveryID})
require.NoError(t, err)
require.Equal(t, []string{"operator_resend:queued"}, telemetry.statuses)
require.Contains(t, loggerBuffer.String(), "\"delivery_id\":\"clone-456\"")
require.Contains(t, loggerBuffer.String(), "\"source\":\"operator_resend\"")
require.Contains(t, loggerBuffer.String(), "\"template_id\":\"game.turn_ready\"")
require.Contains(t, loggerBuffer.String(), "\"otel_trace_id\":")
require.True(t, hasResendSpanNamed(recorder.Ended(), "mail.resend_delivery"))
}
type stubStore struct {
delivery *deliverydomain.Delivery
payload *acceptgenericdelivery.DeliveryPayload
createInputs []CreateResendInput
}
func (store *stubStore) GetDelivery(context.Context, common.DeliveryID) (deliverydomain.Delivery, bool, error) {
if store.delivery == nil {
return deliverydomain.Delivery{}, false, nil
}
return *store.delivery, true, nil
}
func (store *stubStore) GetDeliveryPayload(context.Context, common.DeliveryID) (acceptgenericdelivery.DeliveryPayload, bool, error) {
if store.payload == nil {
return acceptgenericdelivery.DeliveryPayload{}, false, nil
}
return *store.payload, true, nil
}
func (store *stubStore) CreateResend(_ context.Context, input CreateResendInput) error {
store.createInputs = append(store.createInputs, input)
return nil
}
type stubIDGenerator struct {
ids []common.DeliveryID
}
func (generator *stubIDGenerator) NewDeliveryID() (common.DeliveryID, error) {
if len(generator.ids) == 0 {
return "", nil
}
next := generator.ids[0]
generator.ids = generator.ids[1:]
return next, nil
}
type stubClock struct {
now time.Time
}
func (clock stubClock) Now() time.Time {
return clock.now
}
type stubTelemetry struct {
statuses []string
}
func (telemetry *stubTelemetry) RecordDeliveryStatusTransition(_ context.Context, status string, source string) {
telemetry.statuses = append(telemetry.statuses, source+":"+status)
}
func newTestService(t *testing.T, cfg Config) *Service {
t.Helper()
service, err := New(cfg)
require.NoError(t, err)
return service
}
func fixedNow() time.Time {
return time.Unix(1_775_122_100, 0).UTC()
}
func validOriginalDelivery() deliverydomain.Delivery {
createdAt := time.Unix(1_775_121_700, 0).UTC()
updatedAt := createdAt.Add(time.Minute)
sentAt := updatedAt
record := deliverydomain.Delivery{
DeliveryID: common.DeliveryID("delivery-original"),
Source: deliverydomain.SourceNotification,
PayloadMode: deliverydomain.PayloadModeTemplate,
TemplateID: common.TemplateID("game.turn_ready"),
Envelope: deliverydomain.Envelope{
To: []common.Email{common.Email("pilot@example.com")},
Cc: []common.Email{common.Email("copilot@example.com")},
Bcc: []common.Email{common.Email("ops@example.com")},
ReplyTo: []common.Email{common.Email("noreply@example.com")},
},
Content: deliverydomain.Content{
Subject: "Turn ready",
TextBody: "Your next turn is ready",
},
Attachments: []common.AttachmentMetadata{
{Filename: "instructions.txt", ContentType: "text/plain; charset=utf-8", SizeBytes: 7},
},
Locale: common.Locale("en"),
TemplateVariables: map[string]any{"turn": 7},
LocaleFallbackUsed: true,
IdempotencyKey: common.IdempotencyKey("notification:delivery-original"),
Status: deliverydomain.StatusSent,
AttemptCount: 2,
LastAttemptStatus: attempt.StatusProviderAccepted,
ProviderSummary: "provider=smtp result=accepted",
CreatedAt: createdAt,
UpdatedAt: updatedAt,
SentAt: &sentAt,
}
if err := record.Validate(); err != nil {
panic(err)
}
return record
}
func validPayload(deliveryID common.DeliveryID) acceptgenericdelivery.DeliveryPayload {
payload := acceptgenericdelivery.DeliveryPayload{
DeliveryID: deliveryID,
Attachments: []acceptgenericdelivery.AttachmentPayload{
{
Filename: "instructions.txt",
ContentType: "text/plain; charset=utf-8",
ContentBase64: "cmVhZCBtZQ==",
SizeBytes: 7,
},
},
}
if err := payload.Validate(); err != nil {
panic(err)
}
return payload
}
var _ Store = (*stubStore)(nil)
var _ DeliveryIDGenerator = (*stubIDGenerator)(nil)
var _ Clock = stubClock{}
var _ Telemetry = (*stubTelemetry)(nil)
func hasResendSpanNamed(spans []sdktrace.ReadOnlySpan, name string) bool {
for _, span := range spans {
if span.Name() == name {
return true
}
}
return false
}