package redisstate import ( "bytes" "encoding/json" "fmt" "io" "strings" "time" "galaxy/mail/internal/domain/attempt" "galaxy/mail/internal/domain/common" deliverydomain "galaxy/mail/internal/domain/delivery" "galaxy/mail/internal/domain/idempotency" "galaxy/mail/internal/domain/malformedcommand" "galaxy/mail/internal/service/acceptgenericdelivery" ) type deliveryRecord struct { DeliveryID string `json:"delivery_id"` ResendParentDeliveryID string `json:"resend_parent_delivery_id,omitempty"` Source deliverydomain.Source `json:"source"` PayloadMode deliverydomain.PayloadMode `json:"payload_mode"` TemplateID string `json:"template_id,omitempty"` TemplateVariables *map[string]any `json:"template_variables,omitempty"` To []string `json:"to"` Cc []string `json:"cc"` Bcc []string `json:"bcc"` ReplyTo []string `json:"reply_to"` Subject string `json:"subject,omitempty"` TextBody string `json:"text_body,omitempty"` HTMLBody string `json:"html_body,omitempty"` Attachments []attachmentRecord `json:"attachments"` Locale string `json:"locale,omitempty"` LocaleFallbackUsed bool `json:"locale_fallback_used"` IdempotencyKey string `json:"idempotency_key"` Status deliverydomain.Status `json:"status"` AttemptCount int `json:"attempt_count"` LastAttemptStatus attempt.Status `json:"last_attempt_status,omitempty"` ProviderSummary string `json:"provider_summary,omitempty"` CreatedAtMS int64 `json:"created_at_ms"` UpdatedAtMS int64 `json:"updated_at_ms"` SentAtMS *int64 `json:"sent_at_ms,omitempty"` SuppressedAtMS *int64 `json:"suppressed_at_ms,omitempty"` FailedAtMS *int64 `json:"failed_at_ms,omitempty"` DeadLetteredAtMS *int64 `json:"dead_lettered_at_ms,omitempty"` } type attemptRecord struct { DeliveryID string `json:"delivery_id"` AttemptNo int `json:"attempt_no"` ScheduledForMS int64 `json:"scheduled_for_ms"` StartedAtMS *int64 `json:"started_at_ms,omitempty"` FinishedAtMS *int64 `json:"finished_at_ms,omitempty"` Status attempt.Status `json:"status"` ProviderClassification string `json:"provider_classification,omitempty"` ProviderSummary string `json:"provider_summary,omitempty"` } type idempotencyRecord struct { Source deliverydomain.Source `json:"source"` IdempotencyKey string `json:"idempotency_key"` DeliveryID string `json:"delivery_id"` RequestFingerprint string `json:"request_fingerprint"` CreatedAtMS int64 `json:"created_at_ms"` ExpiresAtMS int64 `json:"expires_at_ms"` } type deadLetterRecord struct { DeliveryID string `json:"delivery_id"` FinalAttemptNo int `json:"final_attempt_no"` FailureClassification string `json:"failure_classification"` ProviderSummary string `json:"provider_summary,omitempty"` CreatedAtMS int64 `json:"created_at_ms"` RecoveryHint string `json:"recovery_hint,omitempty"` } type deliveryPayloadRecord struct { DeliveryID string `json:"delivery_id"` Attachments []deliveryPayloadAttachmentRecord `json:"attachments"` } type deliveryPayloadAttachmentRecord struct { Filename string `json:"filename"` ContentType string `json:"content_type"` ContentBase64 string `json:"content_base64"` SizeBytes int64 `json:"size_bytes"` } type malformedCommandRecord struct { StreamEntryID string `json:"stream_entry_id"` DeliveryID string `json:"delivery_id,omitempty"` Source string `json:"source,omitempty"` IdempotencyKey string `json:"idempotency_key,omitempty"` FailureCode malformedcommand.FailureCode `json:"failure_code"` FailureMessage string `json:"failure_message"` RawFieldsJSON map[string]any `json:"raw_fields_json"` RecordedAtMS int64 `json:"recorded_at_ms"` } type streamOffsetRecord struct { Stream string `json:"stream"` LastProcessedEntryID string `json:"last_processed_entry_id"` UpdatedAtMS int64 `json:"updated_at_ms"` } // StreamOffset stores the persisted progress of one plain-XREAD consumer. type StreamOffset struct { // Stream stores the Redis Stream name. Stream string // LastProcessedEntryID stores the last durably processed entry id. LastProcessedEntryID string // UpdatedAt stores when the offset was updated. UpdatedAt time.Time } // Validate reports whether offset contains a complete persisted progress // record. func (offset StreamOffset) Validate() error { if strings.TrimSpace(offset.Stream) == "" { return fmt.Errorf("stream offset stream must not be empty") } if strings.TrimSpace(offset.LastProcessedEntryID) == "" { return fmt.Errorf("stream offset last processed entry id must not be empty") } if err := common.ValidateTimestamp("stream offset updated at", offset.UpdatedAt); err != nil { return err } return nil } type attachmentRecord struct { Filename string `json:"filename"` ContentType string `json:"content_type"` SizeBytes int64 `json:"size_bytes"` } // MarshalDelivery encodes record into the strict Redis JSON shape used for // mail_delivery records. func MarshalDelivery(record deliverydomain.Delivery) ([]byte, error) { if err := record.Validate(); err != nil { return nil, fmt.Errorf("marshal redis delivery record: %w", err) } stored := deliveryRecord{ DeliveryID: record.DeliveryID.String(), ResendParentDeliveryID: record.ResendParentDeliveryID.String(), Source: record.Source, PayloadMode: record.PayloadMode, TemplateID: record.TemplateID.String(), TemplateVariables: optionalJSONObject(record.TemplateVariables), To: cloneEmailStrings(record.Envelope.To), Cc: cloneEmailStrings(record.Envelope.Cc), Bcc: cloneEmailStrings(record.Envelope.Bcc), ReplyTo: cloneEmailStrings(record.Envelope.ReplyTo), Subject: record.Content.Subject, TextBody: record.Content.TextBody, HTMLBody: record.Content.HTMLBody, Attachments: cloneAttachments(record.Attachments), Locale: record.Locale.String(), LocaleFallbackUsed: record.LocaleFallbackUsed, IdempotencyKey: record.IdempotencyKey.String(), Status: record.Status, AttemptCount: record.AttemptCount, LastAttemptStatus: record.LastAttemptStatus, ProviderSummary: record.ProviderSummary, CreatedAtMS: record.CreatedAt.UTC().UnixMilli(), UpdatedAtMS: record.UpdatedAt.UTC().UnixMilli(), SentAtMS: optionalUnixMilli(record.SentAt), SuppressedAtMS: optionalUnixMilli(record.SuppressedAt), FailedAtMS: optionalUnixMilli(record.FailedAt), DeadLetteredAtMS: optionalUnixMilli(record.DeadLetteredAt), } payload, err := json.Marshal(stored) if err != nil { return nil, fmt.Errorf("marshal redis delivery record: %w", err) } return payload, nil } // UnmarshalDelivery decodes payload from the strict Redis JSON shape used for // mail_delivery records. func UnmarshalDelivery(payload []byte) (deliverydomain.Delivery, error) { var stored deliveryRecord if err := decodeStrictJSON("decode redis delivery record", payload, &stored); err != nil { return deliverydomain.Delivery{}, err } record := deliverydomain.Delivery{ DeliveryID: common.DeliveryID(stored.DeliveryID), ResendParentDeliveryID: common.DeliveryID(stored.ResendParentDeliveryID), Source: stored.Source, PayloadMode: stored.PayloadMode, TemplateID: common.TemplateID(stored.TemplateID), TemplateVariables: cloneJSONObjectPtr(stored.TemplateVariables), Envelope: deliverydomain.Envelope{ To: cloneEmails(stored.To), Cc: cloneEmails(stored.Cc), Bcc: cloneEmails(stored.Bcc), ReplyTo: cloneEmails(stored.ReplyTo), }, Content: deliverydomain.Content{ Subject: stored.Subject, TextBody: stored.TextBody, HTMLBody: stored.HTMLBody, }, Attachments: inflateAttachments(stored.Attachments), Locale: common.Locale(stored.Locale), LocaleFallbackUsed: stored.LocaleFallbackUsed, IdempotencyKey: common.IdempotencyKey(stored.IdempotencyKey), Status: stored.Status, AttemptCount: stored.AttemptCount, LastAttemptStatus: stored.LastAttemptStatus, ProviderSummary: stored.ProviderSummary, CreatedAt: time.UnixMilli(stored.CreatedAtMS).UTC(), UpdatedAt: time.UnixMilli(stored.UpdatedAtMS).UTC(), SentAt: inflateOptionalTime(stored.SentAtMS), SuppressedAt: inflateOptionalTime(stored.SuppressedAtMS), FailedAt: inflateOptionalTime(stored.FailedAtMS), DeadLetteredAt: inflateOptionalTime(stored.DeadLetteredAtMS), } if err := record.Validate(); err != nil { return deliverydomain.Delivery{}, fmt.Errorf("decode redis delivery record: %w", err) } return record, nil } // MarshalAttempt encodes record into the strict Redis JSON shape used for // mail_attempt records. func MarshalAttempt(record attempt.Attempt) ([]byte, error) { if err := record.Validate(); err != nil { return nil, fmt.Errorf("marshal redis attempt record: %w", err) } stored := attemptRecord{ DeliveryID: record.DeliveryID.String(), AttemptNo: record.AttemptNo, ScheduledForMS: record.ScheduledFor.UTC().UnixMilli(), StartedAtMS: optionalUnixMilli(record.StartedAt), FinishedAtMS: optionalUnixMilli(record.FinishedAt), Status: record.Status, ProviderClassification: record.ProviderClassification, ProviderSummary: record.ProviderSummary, } payload, err := json.Marshal(stored) if err != nil { return nil, fmt.Errorf("marshal redis attempt record: %w", err) } return payload, nil } // UnmarshalAttempt decodes payload from the strict Redis JSON shape used for // mail_attempt records. func UnmarshalAttempt(payload []byte) (attempt.Attempt, error) { var stored attemptRecord if err := decodeStrictJSON("decode redis attempt record", payload, &stored); err != nil { return attempt.Attempt{}, err } record := attempt.Attempt{ DeliveryID: common.DeliveryID(stored.DeliveryID), AttemptNo: stored.AttemptNo, ScheduledFor: time.UnixMilli(stored.ScheduledForMS).UTC(), StartedAt: inflateOptionalTime(stored.StartedAtMS), FinishedAt: inflateOptionalTime(stored.FinishedAtMS), Status: stored.Status, ProviderClassification: stored.ProviderClassification, ProviderSummary: stored.ProviderSummary, } if err := record.Validate(); err != nil { return attempt.Attempt{}, fmt.Errorf("decode redis attempt record: %w", err) } return record, nil } // MarshalIdempotency encodes record into the strict Redis JSON shape used for // mail_idempotency_record values. func MarshalIdempotency(record idempotency.Record) ([]byte, error) { if err := record.Validate(); err != nil { return nil, fmt.Errorf("marshal redis idempotency record: %w", err) } stored := idempotencyRecord{ Source: record.Source, IdempotencyKey: record.IdempotencyKey.String(), DeliveryID: record.DeliveryID.String(), RequestFingerprint: record.RequestFingerprint, CreatedAtMS: record.CreatedAt.UTC().UnixMilli(), ExpiresAtMS: record.ExpiresAt.UTC().UnixMilli(), } payload, err := json.Marshal(stored) if err != nil { return nil, fmt.Errorf("marshal redis idempotency record: %w", err) } return payload, nil } // UnmarshalIdempotency decodes payload from the strict Redis JSON shape used // for mail_idempotency_record values. func UnmarshalIdempotency(payload []byte) (idempotency.Record, error) { var stored idempotencyRecord if err := decodeStrictJSON("decode redis idempotency record", payload, &stored); err != nil { return idempotency.Record{}, err } record := idempotency.Record{ Source: stored.Source, IdempotencyKey: common.IdempotencyKey(stored.IdempotencyKey), DeliveryID: common.DeliveryID(stored.DeliveryID), RequestFingerprint: stored.RequestFingerprint, CreatedAt: time.UnixMilli(stored.CreatedAtMS).UTC(), ExpiresAt: time.UnixMilli(stored.ExpiresAtMS).UTC(), } if err := record.Validate(); err != nil { return idempotency.Record{}, fmt.Errorf("decode redis idempotency record: %w", err) } return record, nil } // MarshalDeadLetter encodes entry into the strict Redis JSON shape used for // mail_dead_letter_entry values. func MarshalDeadLetter(entry deliverydomain.DeadLetterEntry) ([]byte, error) { if err := entry.Validate(); err != nil { return nil, fmt.Errorf("marshal redis dead-letter record: %w", err) } stored := deadLetterRecord{ DeliveryID: entry.DeliveryID.String(), FinalAttemptNo: entry.FinalAttemptNo, FailureClassification: entry.FailureClassification, ProviderSummary: entry.ProviderSummary, CreatedAtMS: entry.CreatedAt.UTC().UnixMilli(), RecoveryHint: entry.RecoveryHint, } payload, err := json.Marshal(stored) if err != nil { return nil, fmt.Errorf("marshal redis dead-letter record: %w", err) } return payload, nil } // UnmarshalDeadLetter decodes payload from the strict Redis JSON shape used // for mail_dead_letter_entry values. func UnmarshalDeadLetter(payload []byte) (deliverydomain.DeadLetterEntry, error) { var stored deadLetterRecord if err := decodeStrictJSON("decode redis dead-letter record", payload, &stored); err != nil { return deliverydomain.DeadLetterEntry{}, err } entry := deliverydomain.DeadLetterEntry{ DeliveryID: common.DeliveryID(stored.DeliveryID), FinalAttemptNo: stored.FinalAttemptNo, FailureClassification: stored.FailureClassification, ProviderSummary: stored.ProviderSummary, CreatedAt: time.UnixMilli(stored.CreatedAtMS).UTC(), RecoveryHint: stored.RecoveryHint, } if err := entry.Validate(); err != nil { return deliverydomain.DeadLetterEntry{}, fmt.Errorf("decode redis dead-letter record: %w", err) } return entry, nil } // MarshalDeliveryPayload encodes payload into the strict Redis JSON shape used // for raw generic-delivery attachment bundles. func MarshalDeliveryPayload(payload acceptgenericdelivery.DeliveryPayload) ([]byte, error) { if err := payload.Validate(); err != nil { return nil, fmt.Errorf("marshal redis delivery payload record: %w", err) } stored := deliveryPayloadRecord{ DeliveryID: payload.DeliveryID.String(), Attachments: cloneDeliveryPayloadAttachments(payload.Attachments), } encoded, err := json.Marshal(stored) if err != nil { return nil, fmt.Errorf("marshal redis delivery payload record: %w", err) } return encoded, nil } // UnmarshalDeliveryPayload decodes payload from the strict Redis JSON shape // used for raw generic-delivery attachment bundles. func UnmarshalDeliveryPayload(payload []byte) (acceptgenericdelivery.DeliveryPayload, error) { var stored deliveryPayloadRecord if err := decodeStrictJSON("decode redis delivery payload record", payload, &stored); err != nil { return acceptgenericdelivery.DeliveryPayload{}, err } record := acceptgenericdelivery.DeliveryPayload{ DeliveryID: common.DeliveryID(stored.DeliveryID), Attachments: inflateDeliveryPayloadAttachments(stored.Attachments), } if err := record.Validate(); err != nil { return acceptgenericdelivery.DeliveryPayload{}, fmt.Errorf("decode redis delivery payload record: %w", err) } return record, nil } // MarshalMalformedCommand encodes entry into the strict Redis JSON shape used // for operator-visible malformed async command records. func MarshalMalformedCommand(entry malformedcommand.Entry) ([]byte, error) { if err := entry.Validate(); err != nil { return nil, fmt.Errorf("marshal redis malformed command record: %w", err) } stored := malformedCommandRecord{ StreamEntryID: entry.StreamEntryID, DeliveryID: entry.DeliveryID, Source: entry.Source, IdempotencyKey: entry.IdempotencyKey, FailureCode: entry.FailureCode, FailureMessage: entry.FailureMessage, RawFieldsJSON: cloneJSONObject(entry.RawFields), RecordedAtMS: entry.RecordedAt.UTC().UnixMilli(), } encoded, err := json.Marshal(stored) if err != nil { return nil, fmt.Errorf("marshal redis malformed command record: %w", err) } return encoded, nil } // UnmarshalMalformedCommand decodes payload from the strict Redis JSON shape // used for operator-visible malformed async command records. func UnmarshalMalformedCommand(payload []byte) (malformedcommand.Entry, error) { var stored malformedCommandRecord if err := decodeStrictJSON("decode redis malformed command record", payload, &stored); err != nil { return malformedcommand.Entry{}, err } entry := malformedcommand.Entry{ StreamEntryID: stored.StreamEntryID, DeliveryID: stored.DeliveryID, Source: stored.Source, IdempotencyKey: stored.IdempotencyKey, FailureCode: stored.FailureCode, FailureMessage: stored.FailureMessage, RawFields: cloneJSONObject(stored.RawFieldsJSON), RecordedAt: time.UnixMilli(stored.RecordedAtMS).UTC(), } if err := entry.Validate(); err != nil { return malformedcommand.Entry{}, fmt.Errorf("decode redis malformed command record: %w", err) } return entry, nil } // MarshalStreamOffset encodes offset into the strict Redis JSON shape used for // persisted consumer progress. func MarshalStreamOffset(offset StreamOffset) ([]byte, error) { if err := offset.Validate(); err != nil { return nil, fmt.Errorf("marshal redis stream offset record: %w", err) } stored := streamOffsetRecord{ Stream: offset.Stream, LastProcessedEntryID: offset.LastProcessedEntryID, UpdatedAtMS: offset.UpdatedAt.UTC().UnixMilli(), } encoded, err := json.Marshal(stored) if err != nil { return nil, fmt.Errorf("marshal redis stream offset record: %w", err) } return encoded, nil } // UnmarshalStreamOffset decodes payload from the strict Redis JSON shape used // for persisted consumer progress. func UnmarshalStreamOffset(payload []byte) (StreamOffset, error) { var stored streamOffsetRecord if err := decodeStrictJSON("decode redis stream offset record", payload, &stored); err != nil { return StreamOffset{}, err } offset := StreamOffset{ Stream: stored.Stream, LastProcessedEntryID: stored.LastProcessedEntryID, UpdatedAt: time.UnixMilli(stored.UpdatedAtMS).UTC(), } if err := offset.Validate(); err != nil { return StreamOffset{}, fmt.Errorf("decode redis stream offset record: %w", err) } return offset, nil } func decodeStrictJSON(operation string, payload []byte, target any) error { decoder := json.NewDecoder(bytes.NewReader(payload)) decoder.DisallowUnknownFields() if err := decoder.Decode(target); err != nil { return fmt.Errorf("%s: %w", operation, err) } if err := decoder.Decode(&struct{}{}); err != io.EOF { if err == nil { return fmt.Errorf("%s: unexpected trailing JSON input", operation) } return fmt.Errorf("%s: %w", operation, err) } return nil } func cloneEmailStrings(values []common.Email) []string { if values == nil { return nil } cloned := make([]string, len(values)) for index, value := range values { cloned[index] = value.String() } return cloned } func cloneEmails(values []string) []common.Email { if values == nil { return nil } cloned := make([]common.Email, len(values)) for index, value := range values { cloned[index] = common.Email(value) } return cloned } func cloneAttachments(values []common.AttachmentMetadata) []attachmentRecord { if values == nil { return nil } cloned := make([]attachmentRecord, len(values)) for index, value := range values { cloned[index] = attachmentRecord{ Filename: value.Filename, ContentType: value.ContentType, SizeBytes: value.SizeBytes, } } return cloned } func inflateAttachments(values []attachmentRecord) []common.AttachmentMetadata { if values == nil { return nil } cloned := make([]common.AttachmentMetadata, len(values)) for index, value := range values { cloned[index] = common.AttachmentMetadata{ Filename: value.Filename, ContentType: value.ContentType, SizeBytes: value.SizeBytes, } } return cloned } func optionalJSONObject(value map[string]any) *map[string]any { if value == nil { return nil } cloned := make(map[string]any, len(value)) for key, item := range value { cloned[key] = cloneJSONValue(item) } return &cloned } func cloneJSONObjectPtr(value *map[string]any) map[string]any { if value == nil { return nil } cloned := make(map[string]any, len(*value)) for key, item := range *value { cloned[key] = cloneJSONValue(item) } 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, item := range value { cloned[key] = cloneJSONValue(item) } return cloned } func cloneJSONValue(value any) any { switch typed := value.(type) { case map[string]any: cloned := make(map[string]any, len(typed)) for key, item := range typed { cloned[key] = cloneJSONValue(item) } return cloned case []any: cloned := make([]any, len(typed)) for index, item := range typed { cloned[index] = cloneJSONValue(item) } return cloned default: return typed } } func cloneDeliveryPayloadAttachments(values []acceptgenericdelivery.AttachmentPayload) []deliveryPayloadAttachmentRecord { if values == nil { return nil } cloned := make([]deliveryPayloadAttachmentRecord, len(values)) for index, value := range values { cloned[index] = deliveryPayloadAttachmentRecord{ Filename: value.Filename, ContentType: value.ContentType, ContentBase64: value.ContentBase64, SizeBytes: value.SizeBytes, } } return cloned } func inflateDeliveryPayloadAttachments(values []deliveryPayloadAttachmentRecord) []acceptgenericdelivery.AttachmentPayload { if values == nil { return nil } cloned := make([]acceptgenericdelivery.AttachmentPayload, len(values)) for index, value := range values { cloned[index] = acceptgenericdelivery.AttachmentPayload{ Filename: value.Filename, ContentType: value.ContentType, ContentBase64: value.ContentBase64, SizeBytes: value.SizeBytes, } } return cloned } func optionalUnixMilli(value *time.Time) *int64 { if value == nil { return nil } milliseconds := value.UTC().UnixMilli() return &milliseconds } func inflateOptionalTime(value *int64) *time.Time { if value == nil { return nil } converted := time.UnixMilli(*value).UTC() return &converted }