feat: mail service
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
// Package malformedcommand defines the operator-visible record used for
|
||||
// malformed asynchronous generic delivery commands.
|
||||
package malformedcommand
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"galaxy/mail/internal/domain/common"
|
||||
)
|
||||
|
||||
// FailureCode identifies the stable malformed-command rejection reason.
|
||||
type FailureCode string
|
||||
|
||||
const (
|
||||
// FailureCodeInvalidEnvelope reports that the command could not be accepted
|
||||
// because the recipient envelope was invalid.
|
||||
FailureCodeInvalidEnvelope FailureCode = "invalid_envelope"
|
||||
|
||||
// FailureCodeInvalidPayload reports that the command payload could not be
|
||||
// decoded or validated.
|
||||
FailureCodeInvalidPayload FailureCode = "invalid_payload"
|
||||
|
||||
// FailureCodeInvalidCommand reports that the top-level stream envelope was
|
||||
// malformed or unsupported.
|
||||
FailureCodeInvalidCommand FailureCode = "invalid_command"
|
||||
|
||||
// FailureCodeIdempotencyConflict reports that the stream command reused an
|
||||
// existing idempotency scope with a different request fingerprint.
|
||||
FailureCodeIdempotencyConflict FailureCode = "idempotency_conflict"
|
||||
)
|
||||
|
||||
// IsKnown reports whether code belongs to the frozen malformed-command
|
||||
// rejection surface.
|
||||
func (code FailureCode) IsKnown() bool {
|
||||
switch code {
|
||||
case FailureCodeInvalidEnvelope,
|
||||
FailureCodeInvalidPayload,
|
||||
FailureCodeInvalidCommand,
|
||||
FailureCodeIdempotencyConflict:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Entry stores one operator-visible malformed asynchronous command record.
|
||||
type Entry struct {
|
||||
// StreamEntryID stores the Redis Stream entry identifier of the malformed
|
||||
// command.
|
||||
StreamEntryID string
|
||||
|
||||
// DeliveryID stores the optional raw delivery identifier extracted from the
|
||||
// stream entry when available.
|
||||
DeliveryID string
|
||||
|
||||
// Source stores the optional raw source value extracted from the stream
|
||||
// entry when available.
|
||||
Source string
|
||||
|
||||
// IdempotencyKey stores the optional raw idempotency key extracted from the
|
||||
// stream entry when available.
|
||||
IdempotencyKey string
|
||||
|
||||
// FailureCode stores the stable malformed-command rejection reason.
|
||||
FailureCode FailureCode
|
||||
|
||||
// FailureMessage stores the detailed validation or decoding failure.
|
||||
FailureMessage string
|
||||
|
||||
// RawFields stores the raw top-level stream fields captured for later
|
||||
// operator inspection.
|
||||
RawFields map[string]any
|
||||
|
||||
// RecordedAt stores when the malformed command was durably recorded.
|
||||
RecordedAt time.Time
|
||||
}
|
||||
|
||||
// Validate reports whether entry contains a complete malformed-command record.
|
||||
func (entry Entry) Validate() error {
|
||||
if strings.TrimSpace(entry.StreamEntryID) == "" {
|
||||
return fmt.Errorf("malformed command stream entry id must not be empty")
|
||||
}
|
||||
if !entry.FailureCode.IsKnown() {
|
||||
return fmt.Errorf("malformed command failure code %q is unsupported", entry.FailureCode)
|
||||
}
|
||||
if strings.TrimSpace(entry.FailureMessage) == "" {
|
||||
return fmt.Errorf("malformed command failure message must not be empty")
|
||||
}
|
||||
if strings.TrimSpace(entry.FailureMessage) != entry.FailureMessage {
|
||||
return fmt.Errorf("malformed command failure message must not contain surrounding whitespace")
|
||||
}
|
||||
if entry.RawFields == nil {
|
||||
return fmt.Errorf("malformed command raw fields must not be nil")
|
||||
}
|
||||
if err := validateJSONObject("malformed command raw fields", entry.RawFields); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := common.ValidateTimestamp("malformed command recorded at", entry.RecordedAt); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateJSONObject(name string, value map[string]any) error {
|
||||
if value == nil {
|
||||
return fmt.Errorf("%s must not be nil", name)
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", name, err)
|
||||
}
|
||||
if string(payload) == "null" {
|
||||
return fmt.Errorf("%s must encode as a JSON object", name)
|
||||
}
|
||||
|
||||
var decoded map[string]any
|
||||
if err := json.Unmarshal(payload, &decoded); err != nil {
|
||||
return fmt.Errorf("%s: %w", name, err)
|
||||
}
|
||||
if decoded == nil {
|
||||
return fmt.Errorf("%s must encode as a JSON object", name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user