package redisstate import ( "encoding/base64" "sort" "strconv" "time" "galaxy/mail/internal/domain/common" deliverydomain "galaxy/mail/internal/domain/delivery" ) const defaultPrefix = "mail:" const ( // IdempotencyTTL is the frozen Redis retention for idempotency records. IdempotencyTTL = 7 * 24 * time.Hour // DeliveryTTL is the frozen Redis retention for accepted delivery records. DeliveryTTL = 30 * 24 * time.Hour // AttemptTTL is the frozen Redis retention for attempt records. AttemptTTL = 90 * 24 * time.Hour // DeadLetterTTL is the frozen Redis retention for dead-letter entries. DeadLetterTTL = 90 * 24 * time.Hour ) // Keyspace builds the frozen Mail Service Redis keys. All dynamic key // segments are encoded with base64url so raw key structure does not depend on // user-provided or caller-provided characters. type Keyspace struct{} // Delivery returns the primary Redis key for one mail_delivery record. func (Keyspace) Delivery(deliveryID common.DeliveryID) string { return defaultPrefix + "deliveries:" + encodeKeyComponent(deliveryID.String()) } // Attempt returns the primary Redis key for one mail_attempt record. func (Keyspace) Attempt(deliveryID common.DeliveryID, attemptNo int) string { return defaultPrefix + "attempts:" + encodeKeyComponent(deliveryID.String()) + ":" + encodeKeyComponent(strconv.Itoa(attemptNo)) } // Idempotency returns the primary Redis key for one mail_idempotency_record. func (Keyspace) Idempotency(source deliverydomain.Source, key common.IdempotencyKey) string { return defaultPrefix + "idempotency:" + encodeKeyComponent(string(source)) + ":" + encodeKeyComponent(key.String()) } // DeadLetter returns the primary Redis key for one mail_dead_letter_entry. func (Keyspace) DeadLetter(deliveryID common.DeliveryID) string { return defaultPrefix + "dead_letters:" + encodeKeyComponent(deliveryID.String()) } // DeliveryPayload returns the primary Redis key for one raw generic-delivery // payload bundle. func (Keyspace) DeliveryPayload(deliveryID common.DeliveryID) string { return defaultPrefix + "delivery_payloads:" + encodeKeyComponent(deliveryID.String()) } // MalformedCommand returns the primary Redis key for one operator-visible // malformed async command record. func (Keyspace) MalformedCommand(streamEntryID string) string { return defaultPrefix + "malformed_commands:" + encodeKeyComponent(streamEntryID) } // StreamOffset returns the primary Redis key for one persisted stream-consumer // offset. func (Keyspace) StreamOffset(stream string) string { return defaultPrefix + "stream_offsets:" + encodeKeyComponent(stream) } // DeliveryCommands returns the frozen async ingress Redis Stream key. func (Keyspace) DeliveryCommands() string { return defaultPrefix + "delivery_commands" } // AttemptSchedule returns the frozen attempt schedule sorted-set key. func (Keyspace) AttemptSchedule() string { return defaultPrefix + "attempt_schedule" } // RecipientIndex returns the secondary index key for one effective recipient. func (Keyspace) RecipientIndex(email common.Email) string { return defaultPrefix + "idx:recipient:" + encodeKeyComponent(email.String()) } // StatusIndex returns the secondary index key for one delivery status. func (Keyspace) StatusIndex(status deliverydomain.Status) string { return defaultPrefix + "idx:status:" + encodeKeyComponent(string(status)) } // SourceIndex returns the secondary index key for one delivery source. func (Keyspace) SourceIndex(source deliverydomain.Source) string { return defaultPrefix + "idx:source:" + encodeKeyComponent(string(source)) } // TemplateIndex returns the secondary index key for one template id. func (Keyspace) TemplateIndex(templateID common.TemplateID) string { return defaultPrefix + "idx:template:" + encodeKeyComponent(templateID.String()) } // IdempotencyIndex returns the secondary lookup key for one `(source, // idempotency_key)` scope. func (Keyspace) IdempotencyIndex(source deliverydomain.Source, key common.IdempotencyKey) string { return defaultPrefix + "idx:idempotency:" + encodeKeyComponent(string(source)) + ":" + encodeKeyComponent(key.String()) } // CreatedAtIndex returns the newest-first delivery ordering index key. func (Keyspace) CreatedAtIndex() string { return defaultPrefix + "idx:created_at" } // MalformedCommandCreatedAtIndex returns the newest-first malformed-command // ordering index key. func (Keyspace) MalformedCommandCreatedAtIndex() string { return defaultPrefix + "idx:malformed_command:created_at" } // SecondaryIndexPattern returns the key-scan pattern that matches every // delivery-level secondary index owned by Mail Service. func (Keyspace) SecondaryIndexPattern() string { return defaultPrefix + "idx:*" } // DeliveryIndexKeys returns the full set of secondary index keys that must // reference record at creation time. Recipient indexing covers `to`, `cc`, and // `bcc`, but intentionally excludes `reply_to`. func (keyspace Keyspace) DeliveryIndexKeys(record deliverydomain.Delivery) []string { keys := []string{ keyspace.StatusIndex(record.Status), keyspace.SourceIndex(record.Source), keyspace.IdempotencyIndex(record.Source, record.IdempotencyKey), keyspace.CreatedAtIndex(), } if !record.TemplateID.IsZero() { keys = append(keys, keyspace.TemplateIndex(record.TemplateID)) } seen := make(map[string]struct{}, len(keys)+len(record.Envelope.To)+len(record.Envelope.Cc)+len(record.Envelope.Bcc)) for _, key := range keys { seen[key] = struct{}{} } for _, group := range [][]common.Email{record.Envelope.To, record.Envelope.Cc, record.Envelope.Bcc} { for _, email := range group { seen[keyspace.RecipientIndex(email)] = struct{}{} } } keys = keys[:0] for key := range seen { keys = append(keys, key) } sort.Strings(keys) return keys } // CreatedAtScore returns the frozen sorted-set score representation for // delivery creation timestamps. func CreatedAtScore(createdAt time.Time) float64 { return float64(createdAt.UTC().UnixMilli()) } // ScheduledForScore returns the frozen sorted-set score representation for // attempt schedule timestamps. func ScheduledForScore(scheduledFor time.Time) float64 { return float64(scheduledFor.UTC().UnixMilli()) } func encodeKeyComponent(value string) string { return base64.RawURLEncoding.EncodeToString([]byte(value)) }