281 lines
8.0 KiB
Go
281 lines
8.0 KiB
Go
// Package listdeliveries implements trusted operator listing of accepted mail
|
|
// deliveries.
|
|
package listdeliveries
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"galaxy/mail/internal/domain/common"
|
|
deliverydomain "galaxy/mail/internal/domain/delivery"
|
|
)
|
|
|
|
var (
|
|
// ErrInvalidCursor reports that the supplied opaque pagination cursor is
|
|
// malformed or no longer matches durable state.
|
|
ErrInvalidCursor = errors.New("list deliveries invalid cursor")
|
|
|
|
// ErrServiceUnavailable reports that trusted listing could not load durable
|
|
// state safely.
|
|
ErrServiceUnavailable = errors.New("list deliveries service unavailable")
|
|
)
|
|
|
|
const (
|
|
// DefaultLimit stores the frozen default page size used by the operator
|
|
// listing surface.
|
|
DefaultLimit = 50
|
|
|
|
// MaxLimit stores the frozen maximum page size accepted by the operator
|
|
// listing surface.
|
|
MaxLimit = 200
|
|
)
|
|
|
|
// Cursor stores one deterministic continuation position in the delivery sort
|
|
// order `created_at_ms DESC, delivery_id DESC`.
|
|
type Cursor struct {
|
|
// CreatedAt stores the durable creation time of the last visible delivery.
|
|
CreatedAt time.Time
|
|
|
|
// DeliveryID stores the durable identifier of the last visible delivery.
|
|
DeliveryID common.DeliveryID
|
|
}
|
|
|
|
// Validate reports whether cursor contains a complete continuation tuple.
|
|
func (cursor Cursor) Validate() error {
|
|
if err := common.ValidateTimestamp("delivery list cursor created at", cursor.CreatedAt); err != nil {
|
|
return err
|
|
}
|
|
if err := cursor.DeliveryID.Validate(); err != nil {
|
|
return fmt.Errorf("delivery list cursor delivery id: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Filters stores the supported operator-listing filters.
|
|
type Filters struct {
|
|
// Recipient stores the optional recipient envelope filter covering `to`,
|
|
// `cc`, and `bcc`.
|
|
Recipient common.Email
|
|
|
|
// Status stores the optional delivery lifecycle filter.
|
|
Status deliverydomain.Status
|
|
|
|
// Source stores the optional delivery source filter.
|
|
Source deliverydomain.Source
|
|
|
|
// TemplateID stores the optional template family filter.
|
|
TemplateID common.TemplateID
|
|
|
|
// IdempotencyKey stores the optional idempotency-key filter.
|
|
IdempotencyKey common.IdempotencyKey
|
|
|
|
// FromCreatedAt stores the optional inclusive lower creation-time bound.
|
|
FromCreatedAt *time.Time
|
|
|
|
// ToCreatedAt stores the optional inclusive upper creation-time bound.
|
|
ToCreatedAt *time.Time
|
|
}
|
|
|
|
// Validate reports whether filters is structurally valid.
|
|
func (filters Filters) Validate() error {
|
|
if !filters.Recipient.IsZero() {
|
|
if err := filters.Recipient.Validate(); err != nil {
|
|
return fmt.Errorf("recipient: %w", err)
|
|
}
|
|
}
|
|
if filters.Status != "" && !filters.Status.IsKnown() {
|
|
return fmt.Errorf("status %q is unsupported", filters.Status)
|
|
}
|
|
if filters.Source != "" && !filters.Source.IsKnown() {
|
|
return fmt.Errorf("source %q is unsupported", filters.Source)
|
|
}
|
|
if !filters.TemplateID.IsZero() {
|
|
if err := filters.TemplateID.Validate(); err != nil {
|
|
return fmt.Errorf("template id: %w", err)
|
|
}
|
|
}
|
|
if !filters.IdempotencyKey.IsZero() {
|
|
if err := filters.IdempotencyKey.Validate(); err != nil {
|
|
return fmt.Errorf("idempotency key: %w", err)
|
|
}
|
|
}
|
|
if filters.FromCreatedAt != nil {
|
|
if err := common.ValidateTimestamp("from created at", *filters.FromCreatedAt); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if filters.ToCreatedAt != nil {
|
|
if err := common.ValidateTimestamp("to created at", *filters.ToCreatedAt); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if filters.FromCreatedAt != nil && filters.ToCreatedAt != nil && filters.FromCreatedAt.After(*filters.ToCreatedAt) {
|
|
return errors.New("from created at must not be after to created at")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Input stores one trusted operator-listing request.
|
|
type Input struct {
|
|
// Limit stores the maximum number of returned deliveries. The zero value
|
|
// selects the frozen default limit.
|
|
Limit int
|
|
|
|
// Cursor stores the optional continuation cursor for the next page.
|
|
Cursor *Cursor
|
|
|
|
// Filters stores the normalized listing filters.
|
|
Filters Filters
|
|
}
|
|
|
|
// Validate reports whether input contains a complete supported listing
|
|
// request.
|
|
func (input Input) Validate() error {
|
|
switch {
|
|
case input.Limit < 0:
|
|
return errors.New("limit must not be negative")
|
|
case input.Limit > MaxLimit:
|
|
return fmt.Errorf("limit must be at most %d", MaxLimit)
|
|
}
|
|
if input.Cursor != nil {
|
|
if err := input.Cursor.Validate(); err != nil {
|
|
return fmt.Errorf("cursor: %w", err)
|
|
}
|
|
}
|
|
if err := input.Filters.Validate(); err != nil {
|
|
return fmt.Errorf("filters: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Result stores one deterministic ordered page of delivery records.
|
|
type Result struct {
|
|
// Items stores the returned deliveries in `created_at DESC, delivery_id
|
|
// DESC` order.
|
|
Items []deliverydomain.Delivery
|
|
|
|
// NextCursor stores the optional cursor for the next page.
|
|
NextCursor *Cursor
|
|
}
|
|
|
|
// Validate reports whether result contains valid delivery records and an
|
|
// optional next cursor.
|
|
func (result Result) Validate() error {
|
|
for index, record := range result.Items {
|
|
if err := record.Validate(); err != nil {
|
|
return fmt.Errorf("items[%d]: %w", index, err)
|
|
}
|
|
}
|
|
if result.NextCursor != nil {
|
|
if err := result.NextCursor.Validate(); err != nil {
|
|
return fmt.Errorf("next cursor: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Store provides deterministic ordered listing over durable delivery state.
|
|
type Store interface {
|
|
// List returns one filtered ordered page of delivery records.
|
|
List(context.Context, Input) (Result, error)
|
|
}
|
|
|
|
// Config stores the dependencies used by Service.
|
|
type Config struct {
|
|
// Store loads one deterministic ordered page of durable deliveries.
|
|
Store Store
|
|
}
|
|
|
|
// Service executes trusted operator delivery-list reads.
|
|
type Service struct {
|
|
store Store
|
|
}
|
|
|
|
// New constructs Service from cfg.
|
|
func New(cfg Config) (*Service, error) {
|
|
if cfg.Store == nil {
|
|
return nil, errors.New("new list deliveries service: nil store")
|
|
}
|
|
|
|
return &Service{store: cfg.Store}, nil
|
|
}
|
|
|
|
// Execute validates input, applies the default limit when omitted, and loads
|
|
// one deterministic page of deliveries.
|
|
func (service *Service) Execute(ctx context.Context, input Input) (Result, error) {
|
|
if ctx == nil {
|
|
return Result{}, errors.New("execute list deliveries: nil context")
|
|
}
|
|
if service == nil {
|
|
return Result{}, errors.New("execute list deliveries: nil service")
|
|
}
|
|
if input.Limit == 0 {
|
|
input.Limit = DefaultLimit
|
|
}
|
|
if err := input.Validate(); err != nil {
|
|
return Result{}, fmt.Errorf("execute list deliveries: %w", err)
|
|
}
|
|
|
|
result, err := service.store.List(ctx, input)
|
|
switch {
|
|
case errors.Is(err, ErrInvalidCursor):
|
|
return Result{}, err
|
|
case err != nil:
|
|
return Result{}, fmt.Errorf("%w: %v", ErrServiceUnavailable, err)
|
|
}
|
|
if err := result.Validate(); err != nil {
|
|
return Result{}, fmt.Errorf("%w: invalid result: %v", ErrServiceUnavailable, err)
|
|
}
|
|
if len(result.Items) > input.Limit {
|
|
return Result{}, fmt.Errorf("%w: invalid result: returned %d items for limit %d", ErrServiceUnavailable, len(result.Items), input.Limit)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// Matches reports whether record satisfies filters.
|
|
func (filters Filters) Matches(record deliverydomain.Delivery) bool {
|
|
if filters.Recipient != "" && !containsRecipient(record.Envelope, filters.Recipient) {
|
|
return false
|
|
}
|
|
if filters.Status != "" && record.Status != filters.Status {
|
|
return false
|
|
}
|
|
if filters.Source != "" && record.Source != filters.Source {
|
|
return false
|
|
}
|
|
if filters.TemplateID != "" && record.TemplateID != filters.TemplateID {
|
|
return false
|
|
}
|
|
if filters.IdempotencyKey != "" && record.IdempotencyKey != filters.IdempotencyKey {
|
|
return false
|
|
}
|
|
if filters.FromCreatedAt != nil && record.CreatedAt.Before(filters.FromCreatedAt.UTC()) {
|
|
return false
|
|
}
|
|
if filters.ToCreatedAt != nil && record.CreatedAt.After(filters.ToCreatedAt.UTC()) {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func containsRecipient(envelope deliverydomain.Envelope, email common.Email) bool {
|
|
for _, group := range [][]common.Email{envelope.To, envelope.Cc, envelope.Bcc} {
|
|
for _, candidate := range group {
|
|
if strings.EqualFold(candidate.String(), email.String()) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|