feat: mail service
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user