feat: mail service

This commit is contained in:
Ilia Denisov
2026-04-17 18:39:16 +02:00
committed by GitHub
parent 23ffcb7535
commit 5b7593e6f6
183 changed files with 31215 additions and 248 deletions
+294
View File
@@ -0,0 +1,294 @@
// Package internalhttp defines the frozen trusted internal HTTP contract used
// by Mail Service.
package internalhttp
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"mime"
"net/http"
"strings"
"galaxy/mail/internal/domain/common"
)
const (
// LoginCodeDeliveriesPath is the dedicated trusted route used by
// Auth / Session Service for auth login-code delivery intake.
LoginCodeDeliveriesPath = "/api/v1/internal/login-code-deliveries"
// IdempotencyKeyHeader is the required header that scopes auth-delivery
// deduplication.
IdempotencyKeyHeader = "Idempotency-Key"
// ErrorCodeInvalidRequest identifies trusted validation failures.
ErrorCodeInvalidRequest = "invalid_request"
// ErrorCodeInternalError identifies trusted invariant failures.
ErrorCodeInternalError = "internal_error"
// ErrorCodeServiceUnavailable identifies trusted availability failures.
ErrorCodeServiceUnavailable = "service_unavailable"
// ErrorCodeConflict identifies conflicting idempotency replays.
ErrorCodeConflict = "conflict"
jsonMediaType = "application/json"
)
// LoginCodeDeliveryRequest stores the strict JSON body accepted on the frozen
// auth-delivery route before normalization.
type LoginCodeDeliveryRequest struct {
// Email stores the destination e-mail address.
Email string `json:"email"`
// Code stores the exact login code generated by Auth / Session Service.
Code string `json:"code"`
// Locale stores the caller-selected BCP 47 language tag.
Locale string `json:"locale"`
}
// LoginCodeDeliveryCommand stores the normalized auth-delivery request shape
// that later Mail Service handlers and services can consume directly.
type LoginCodeDeliveryCommand struct {
// IdempotencyKey stores the caller-owned stable deduplication key.
IdempotencyKey common.IdempotencyKey
// Email stores the normalized recipient address.
Email common.Email
// Code stores the exact login code after boundary validation.
Code string
// Locale stores the canonical BCP 47 language tag.
Locale common.Locale
}
// Validate reports whether command satisfies the frozen auth-delivery
// contract.
func (command LoginCodeDeliveryCommand) Validate() error {
if err := command.IdempotencyKey.Validate(); err != nil {
return fmt.Errorf("idempotency key: %w", err)
}
if err := command.Email.Validate(); err != nil {
return fmt.Errorf("email: %w", err)
}
if strings.TrimSpace(command.Code) == "" {
return errors.New("code must not be empty")
}
if strings.TrimSpace(command.Code) != command.Code {
return errors.New("code must not contain surrounding whitespace")
}
if err := command.Locale.Validate(); err != nil {
return fmt.Errorf("locale: %w", err)
}
return nil
}
// Fingerprint returns the stable auth-delivery idempotency fingerprint of
// command.
func (command LoginCodeDeliveryCommand) Fingerprint() (string, error) {
if err := command.Validate(); err != nil {
return "", err
}
normalized := struct {
IdempotencyKey string `json:"idempotency_key"`
Email string `json:"email"`
Code string `json:"code"`
Locale string `json:"locale"`
}{
IdempotencyKey: command.IdempotencyKey.String(),
Email: command.Email.String(),
Code: command.Code,
Locale: command.Locale.String(),
}
payload, err := json.Marshal(normalized)
if err != nil {
return "", fmt.Errorf("marshal login code delivery fingerprint: %w", err)
}
sum := sha256.Sum256(payload)
return "sha256:" + hex.EncodeToString(sum[:]), nil
}
// LoginCodeDeliveryOutcome identifies the stable successful auth-delivery
// intake outcomes.
type LoginCodeDeliveryOutcome string
const (
// LoginCodeDeliveryOutcomeSent reports durable acceptance into the internal
// mail-delivery pipeline.
LoginCodeDeliveryOutcomeSent LoginCodeDeliveryOutcome = "sent"
// LoginCodeDeliveryOutcomeSuppressed reports intentional outward delivery
// suppression while keeping the auth flow success-shaped.
LoginCodeDeliveryOutcomeSuppressed LoginCodeDeliveryOutcome = "suppressed"
)
// IsKnown reports whether outcome belongs to the frozen auth success surface.
func (outcome LoginCodeDeliveryOutcome) IsKnown() bool {
switch outcome {
case LoginCodeDeliveryOutcomeSent, LoginCodeDeliveryOutcomeSuppressed:
return true
default:
return false
}
}
// LoginCodeDeliveryResponse stores the stable successful auth-delivery
// response body.
type LoginCodeDeliveryResponse struct {
// Outcome stores the stable coarse acceptance result.
Outcome LoginCodeDeliveryOutcome `json:"outcome"`
}
// Validate reports whether response satisfies the frozen success contract.
func (response LoginCodeDeliveryResponse) Validate() error {
if !response.Outcome.IsKnown() {
return fmt.Errorf("login code delivery outcome %q is unsupported", response.Outcome)
}
return nil
}
// ErrorResponse stores the stable trusted error envelope used by Mail Service.
type ErrorResponse struct {
// Error stores the stable trusted error body.
Error ErrorBody `json:"error"`
}
// Validate reports whether response satisfies the frozen trusted error
// envelope contract.
func (response ErrorResponse) Validate() error {
return response.Error.Validate()
}
// ErrorBody stores the stable trusted error shape returned by Mail Service.
type ErrorBody struct {
// Code stores the stable machine-readable error code.
Code string `json:"code"`
// Message stores the trusted human-readable error message.
Message string `json:"message"`
}
// Validate reports whether body contains a complete trusted error payload.
func (body ErrorBody) Validate() error {
switch {
case strings.TrimSpace(body.Code) == "":
return errors.New("error code must not be empty")
case strings.TrimSpace(body.Code) != body.Code:
return errors.New("error code must not contain surrounding whitespace")
case strings.TrimSpace(body.Message) == "":
return errors.New("error message must not be empty")
default:
return nil
}
}
// DecodeLoginCodeDeliveryCommand validates one trusted HTTP request and
// returns the normalized auth-delivery command shape frozen by Stage 04.
func DecodeLoginCodeDeliveryCommand(request *http.Request) (LoginCodeDeliveryCommand, error) {
if request == nil {
return LoginCodeDeliveryCommand{}, errors.New("login code delivery request must not be nil")
}
if err := validateJSONContentType(request.Header.Get("Content-Type")); err != nil {
return LoginCodeDeliveryCommand{}, err
}
idempotencyKey, err := parseIdempotencyKey(request.Header.Get(IdempotencyKeyHeader))
if err != nil {
return LoginCodeDeliveryCommand{}, err
}
body, err := decodeLoginCodeDeliveryRequest(request.Body)
if err != nil {
return LoginCodeDeliveryCommand{}, err
}
command := LoginCodeDeliveryCommand{
IdempotencyKey: idempotencyKey,
Email: common.Email(strings.TrimSpace(body.Email)),
Code: body.Code,
}
locale, err := common.ParseLocale(strings.TrimSpace(body.Locale))
if err != nil {
return LoginCodeDeliveryCommand{}, fmt.Errorf("locale: %w", err)
}
command.Locale = locale
if err := command.Validate(); err != nil {
return LoginCodeDeliveryCommand{}, err
}
return command, nil
}
func decodeLoginCodeDeliveryRequest(body io.ReadCloser) (LoginCodeDeliveryRequest, error) {
if body == nil {
return LoginCodeDeliveryRequest{}, errors.New("request body must not be nil")
}
defer body.Close()
payload, err := io.ReadAll(body)
if err != nil {
return LoginCodeDeliveryRequest{}, fmt.Errorf("read request body: %w", err)
}
decoder := json.NewDecoder(bytes.NewReader(payload))
decoder.DisallowUnknownFields()
var request LoginCodeDeliveryRequest
if err := decoder.Decode(&request); err != nil {
return LoginCodeDeliveryRequest{}, fmt.Errorf("decode request body: %w", err)
}
if err := decoder.Decode(&struct{}{}); err != io.EOF {
if err == nil {
return LoginCodeDeliveryRequest{}, errors.New("decode request body: unexpected trailing JSON input")
}
return LoginCodeDeliveryRequest{}, fmt.Errorf("decode request body: %w", err)
}
return request, nil
}
func parseIdempotencyKey(value string) (common.IdempotencyKey, error) {
switch {
case strings.TrimSpace(value) == "":
return "", errors.New("Idempotency-Key header must not be empty")
case strings.TrimSpace(value) != value:
return "", errors.New("Idempotency-Key header must not contain surrounding whitespace")
default:
key := common.IdempotencyKey(value)
if err := key.Validate(); err != nil {
return "", fmt.Errorf("idempotency key: %w", err)
}
return key, nil
}
}
func validateJSONContentType(value string) error {
mediaType, _, err := mime.ParseMediaType(value)
if err != nil {
return fmt.Errorf("Content-Type must be %s", jsonMediaType)
}
if mediaType != jsonMediaType {
return fmt.Errorf("Content-Type must be %s", jsonMediaType)
}
return nil
}