feat: mail service
This commit is contained in:
@@ -0,0 +1,625 @@
|
||||
package internalhttp
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"galaxy/mail/internal/domain/attempt"
|
||||
"galaxy/mail/internal/domain/common"
|
||||
deliverydomain "galaxy/mail/internal/domain/delivery"
|
||||
"galaxy/mail/internal/service/listdeliveries"
|
||||
)
|
||||
|
||||
const (
|
||||
// ErrorCodeDeliveryNotFound identifies a missing trusted delivery lookup
|
||||
// target.
|
||||
ErrorCodeDeliveryNotFound = "delivery_not_found"
|
||||
|
||||
// ErrorCodeResendNotAllowed identifies resend requests against non-terminal
|
||||
// deliveries.
|
||||
ErrorCodeResendNotAllowed = "resend_not_allowed"
|
||||
|
||||
deliveryIDPathValue = "delivery_id"
|
||||
)
|
||||
|
||||
// DeliveryListQuery stores the raw trusted query-string values accepted by the
|
||||
// operator delivery-list route before normalization.
|
||||
type DeliveryListQuery struct {
|
||||
// Recipient stores the optional recipient filter covering `to`, `cc`, and
|
||||
// `bcc`.
|
||||
Recipient string
|
||||
|
||||
// Status stores the optional delivery-status filter.
|
||||
Status string
|
||||
|
||||
// Source stores the optional delivery-source filter.
|
||||
Source string
|
||||
|
||||
// TemplateID stores the optional template-family filter.
|
||||
TemplateID string
|
||||
|
||||
// IdempotencyKey stores the optional idempotency-key filter.
|
||||
IdempotencyKey string
|
||||
|
||||
// FromCreatedAtMS stores the optional inclusive lower creation-time bound.
|
||||
FromCreatedAtMS string
|
||||
|
||||
// ToCreatedAtMS stores the optional inclusive upper creation-time bound.
|
||||
ToCreatedAtMS string
|
||||
|
||||
// Limit stores the optional page size.
|
||||
Limit string
|
||||
|
||||
// Cursor stores the optional opaque continuation cursor.
|
||||
Cursor string
|
||||
}
|
||||
|
||||
// DeliverySummaryResponse stores one brief operator-facing delivery record.
|
||||
type DeliverySummaryResponse struct {
|
||||
DeliveryID string `json:"delivery_id"`
|
||||
ResendParentDeliveryID string `json:"resend_parent_delivery_id,omitempty"`
|
||||
Source string `json:"source"`
|
||||
PayloadMode string `json:"payload_mode"`
|
||||
TemplateID string `json:"template_id,omitempty"`
|
||||
To []string `json:"to"`
|
||||
Cc []string `json:"cc"`
|
||||
Bcc []string `json:"bcc"`
|
||||
ReplyTo []string `json:"reply_to"`
|
||||
Locale string `json:"locale,omitempty"`
|
||||
LocaleFallbackUsed bool `json:"locale_fallback_used"`
|
||||
IdempotencyKey string `json:"idempotency_key"`
|
||||
Status string `json:"status"`
|
||||
AttemptCount int `json:"attempt_count"`
|
||||
LastAttemptStatus string `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"`
|
||||
}
|
||||
|
||||
// DeliveryListResponse stores one deterministic page of brief delivery
|
||||
// summaries.
|
||||
type DeliveryListResponse struct {
|
||||
Items []DeliverySummaryResponse `json:"items"`
|
||||
NextCursor string `json:"next_cursor,omitempty"`
|
||||
}
|
||||
|
||||
// AttachmentResponse stores one durable attachment audit record.
|
||||
type AttachmentResponse struct {
|
||||
Filename string `json:"filename"`
|
||||
ContentType string `json:"content_type"`
|
||||
SizeBytes int64 `json:"size_bytes"`
|
||||
}
|
||||
|
||||
// DeadLetterResponse stores one operator-visible dead-letter entry.
|
||||
type DeadLetterResponse 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"`
|
||||
}
|
||||
|
||||
// DeliveryDetailResponse stores one full operator-facing delivery view.
|
||||
type DeliveryDetailResponse struct {
|
||||
DeliveryID string `json:"delivery_id"`
|
||||
ResendParentDeliveryID string `json:"resend_parent_delivery_id,omitempty"`
|
||||
Source string `json:"source"`
|
||||
PayloadMode string `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 []AttachmentResponse `json:"attachments"`
|
||||
Locale string `json:"locale,omitempty"`
|
||||
LocaleFallbackUsed bool `json:"locale_fallback_used"`
|
||||
IdempotencyKey string `json:"idempotency_key"`
|
||||
Status string `json:"status"`
|
||||
AttemptCount int `json:"attempt_count"`
|
||||
LastAttemptStatus string `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"`
|
||||
DeadLetter *DeadLetterResponse `json:"dead_letter,omitempty"`
|
||||
}
|
||||
|
||||
// AttemptResponse stores one operator-facing delivery-attempt record.
|
||||
type AttemptResponse 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 string `json:"status"`
|
||||
ProviderClassification string `json:"provider_classification,omitempty"`
|
||||
ProviderSummary string `json:"provider_summary,omitempty"`
|
||||
}
|
||||
|
||||
// DeliveryAttemptsResponse stores the attempt history of one accepted
|
||||
// delivery.
|
||||
type DeliveryAttemptsResponse struct {
|
||||
Items []AttemptResponse `json:"items"`
|
||||
}
|
||||
|
||||
// DeliveryResendResponse stores the identifier of the clone delivery created
|
||||
// by one resend request.
|
||||
type DeliveryResendResponse struct {
|
||||
DeliveryID string `json:"delivery_id"`
|
||||
}
|
||||
|
||||
// DecodeDeliveryListInput validates one trusted operator delivery-list
|
||||
// request and returns the normalized list input.
|
||||
func DecodeDeliveryListInput(request *http.Request) (listdeliveries.Input, error) {
|
||||
if request == nil {
|
||||
return listdeliveries.Input{}, errors.New("delivery list request must not be nil")
|
||||
}
|
||||
|
||||
query, err := decodeDeliveryListQuery(request)
|
||||
if err != nil {
|
||||
return listdeliveries.Input{}, err
|
||||
}
|
||||
|
||||
input, err := query.Normalize()
|
||||
if err != nil {
|
||||
return listdeliveries.Input{}, err
|
||||
}
|
||||
|
||||
return input, nil
|
||||
}
|
||||
|
||||
// DecodeDeliveryIDFromPath validates one trusted path delivery identifier.
|
||||
func DecodeDeliveryIDFromPath(request *http.Request) (common.DeliveryID, error) {
|
||||
if request == nil {
|
||||
return "", errors.New("delivery lookup request must not be nil")
|
||||
}
|
||||
|
||||
return parseDeliveryID(request.PathValue(deliveryIDPathValue))
|
||||
}
|
||||
|
||||
// EncodeDeliveryListCursor encodes cursor into the frozen opaque base64url
|
||||
// format `created_at_ms:delivery_id`.
|
||||
func EncodeDeliveryListCursor(cursor listdeliveries.Cursor) (string, error) {
|
||||
if err := cursor.Validate(); err != nil {
|
||||
return "", fmt.Errorf("encode delivery list cursor: %w", err)
|
||||
}
|
||||
|
||||
payload := fmt.Sprintf("%d:%s", cursor.CreatedAt.UTC().UnixMilli(), cursor.DeliveryID.String())
|
||||
|
||||
return base64.RawURLEncoding.EncodeToString([]byte(payload)), nil
|
||||
}
|
||||
|
||||
// DecodeDeliveryListCursor decodes raw from the frozen opaque cursor format.
|
||||
func DecodeDeliveryListCursor(raw string) (listdeliveries.Cursor, error) {
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
return listdeliveries.Cursor{}, errors.New("cursor must not be empty")
|
||||
}
|
||||
if strings.TrimSpace(raw) != raw {
|
||||
return listdeliveries.Cursor{}, errors.New("cursor must not contain surrounding whitespace")
|
||||
}
|
||||
|
||||
payload, err := base64.RawURLEncoding.DecodeString(raw)
|
||||
if err != nil {
|
||||
return listdeliveries.Cursor{}, fmt.Errorf("decode cursor: %w", err)
|
||||
}
|
||||
|
||||
createdAtRaw, deliveryIDRaw, ok := strings.Cut(string(payload), ":")
|
||||
if !ok {
|
||||
return listdeliveries.Cursor{}, errors.New("decode cursor: invalid cursor payload")
|
||||
}
|
||||
|
||||
createdAtMS, err := strconv.ParseInt(createdAtRaw, 10, 64)
|
||||
if err != nil {
|
||||
return listdeliveries.Cursor{}, fmt.Errorf("decode cursor created_at_ms: %w", err)
|
||||
}
|
||||
|
||||
cursor := listdeliveries.Cursor{
|
||||
CreatedAt: time.UnixMilli(createdAtMS).UTC(),
|
||||
DeliveryID: common.DeliveryID(deliveryIDRaw),
|
||||
}
|
||||
if err := cursor.Validate(); err != nil {
|
||||
return listdeliveries.Cursor{}, fmt.Errorf("decode cursor: %w", err)
|
||||
}
|
||||
|
||||
return cursor, nil
|
||||
}
|
||||
|
||||
// Normalize converts the raw trusted query-string shape into the operator list
|
||||
// input consumed by the service layer.
|
||||
func (query DeliveryListQuery) Normalize() (listdeliveries.Input, error) {
|
||||
var input listdeliveries.Input
|
||||
|
||||
recipient, err := parseOptionalEmail(query.Recipient, "recipient")
|
||||
if err != nil {
|
||||
return listdeliveries.Input{}, err
|
||||
}
|
||||
status, err := parseOptionalStatus(query.Status)
|
||||
if err != nil {
|
||||
return listdeliveries.Input{}, err
|
||||
}
|
||||
source, err := parseOptionalSource(query.Source)
|
||||
if err != nil {
|
||||
return listdeliveries.Input{}, err
|
||||
}
|
||||
templateID, err := parseOptionalTemplateID(query.TemplateID)
|
||||
if err != nil {
|
||||
return listdeliveries.Input{}, err
|
||||
}
|
||||
idempotencyKey, err := parseOptionalIdempotencyKey(query.IdempotencyKey)
|
||||
if err != nil {
|
||||
return listdeliveries.Input{}, err
|
||||
}
|
||||
fromCreatedAt, err := parseOptionalUnixMilli(query.FromCreatedAtMS, "from_created_at_ms")
|
||||
if err != nil {
|
||||
return listdeliveries.Input{}, err
|
||||
}
|
||||
toCreatedAt, err := parseOptionalUnixMilli(query.ToCreatedAtMS, "to_created_at_ms")
|
||||
if err != nil {
|
||||
return listdeliveries.Input{}, err
|
||||
}
|
||||
limit, err := parseOptionalLimit(query.Limit)
|
||||
if err != nil {
|
||||
return listdeliveries.Input{}, err
|
||||
}
|
||||
cursor, err := parseOptionalCursor(query.Cursor)
|
||||
if err != nil {
|
||||
return listdeliveries.Input{}, err
|
||||
}
|
||||
|
||||
input = listdeliveries.Input{
|
||||
Limit: limit,
|
||||
Cursor: cursor,
|
||||
Filters: listdeliveries.Filters{
|
||||
Recipient: recipient,
|
||||
Status: status,
|
||||
Source: source,
|
||||
TemplateID: templateID,
|
||||
IdempotencyKey: idempotencyKey,
|
||||
FromCreatedAt: fromCreatedAt,
|
||||
ToCreatedAt: toCreatedAt,
|
||||
},
|
||||
}
|
||||
if err := input.Validate(); err != nil {
|
||||
return listdeliveries.Input{}, err
|
||||
}
|
||||
|
||||
return input, nil
|
||||
}
|
||||
|
||||
func decodeDeliveryListQuery(request *http.Request) (DeliveryListQuery, error) {
|
||||
values := request.URL.Query()
|
||||
|
||||
recipient, err := singleQueryValue(values, "recipient")
|
||||
if err != nil {
|
||||
return DeliveryListQuery{}, err
|
||||
}
|
||||
status, err := singleQueryValue(values, "status")
|
||||
if err != nil {
|
||||
return DeliveryListQuery{}, err
|
||||
}
|
||||
source, err := singleQueryValue(values, "source")
|
||||
if err != nil {
|
||||
return DeliveryListQuery{}, err
|
||||
}
|
||||
templateID, err := singleQueryValue(values, "template_id")
|
||||
if err != nil {
|
||||
return DeliveryListQuery{}, err
|
||||
}
|
||||
idempotencyKey, err := singleQueryValue(values, "idempotency_key")
|
||||
if err != nil {
|
||||
return DeliveryListQuery{}, err
|
||||
}
|
||||
fromCreatedAtMS, err := singleQueryValue(values, "from_created_at_ms")
|
||||
if err != nil {
|
||||
return DeliveryListQuery{}, err
|
||||
}
|
||||
toCreatedAtMS, err := singleQueryValue(values, "to_created_at_ms")
|
||||
if err != nil {
|
||||
return DeliveryListQuery{}, err
|
||||
}
|
||||
limit, err := singleQueryValue(values, "limit")
|
||||
if err != nil {
|
||||
return DeliveryListQuery{}, err
|
||||
}
|
||||
cursor, err := singleQueryValue(values, "cursor")
|
||||
if err != nil {
|
||||
return DeliveryListQuery{}, err
|
||||
}
|
||||
|
||||
return DeliveryListQuery{
|
||||
Recipient: recipient,
|
||||
Status: status,
|
||||
Source: source,
|
||||
TemplateID: templateID,
|
||||
IdempotencyKey: idempotencyKey,
|
||||
FromCreatedAtMS: fromCreatedAtMS,
|
||||
ToCreatedAtMS: toCreatedAtMS,
|
||||
Limit: limit,
|
||||
Cursor: cursor,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func singleQueryValue(values map[string][]string, key string) (string, error) {
|
||||
rawValues := values[key]
|
||||
switch len(rawValues) {
|
||||
case 0:
|
||||
return "", nil
|
||||
case 1:
|
||||
return rawValues[0], nil
|
||||
default:
|
||||
return "", fmt.Errorf("query parameter %q must appear at most once", key)
|
||||
}
|
||||
}
|
||||
|
||||
func parseDeliveryID(raw string) (common.DeliveryID, error) {
|
||||
deliveryID := common.DeliveryID(raw)
|
||||
if err := deliveryID.Validate(); err != nil {
|
||||
return "", fmt.Errorf("delivery id: %w", err)
|
||||
}
|
||||
|
||||
return deliveryID, nil
|
||||
}
|
||||
|
||||
func parseOptionalEmail(raw string, name string) (common.Email, error) {
|
||||
if raw == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
email := common.Email(strings.TrimSpace(raw))
|
||||
if err := email.Validate(); err != nil {
|
||||
return "", fmt.Errorf("%s: %w", name, err)
|
||||
}
|
||||
|
||||
return email, nil
|
||||
}
|
||||
|
||||
func parseOptionalStatus(raw string) (deliverydomain.Status, error) {
|
||||
if raw == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
status := deliverydomain.Status(strings.TrimSpace(raw))
|
||||
if !status.IsKnown() {
|
||||
return "", fmt.Errorf("status %q is unsupported", raw)
|
||||
}
|
||||
|
||||
return status, nil
|
||||
}
|
||||
|
||||
func parseOptionalSource(raw string) (deliverydomain.Source, error) {
|
||||
if raw == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
source := deliverydomain.Source(strings.TrimSpace(raw))
|
||||
if !source.IsKnown() {
|
||||
return "", fmt.Errorf("source %q is unsupported", raw)
|
||||
}
|
||||
|
||||
return source, nil
|
||||
}
|
||||
|
||||
func parseOptionalTemplateID(raw string) (common.TemplateID, error) {
|
||||
if raw == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
templateID := common.TemplateID(strings.TrimSpace(raw))
|
||||
if err := templateID.Validate(); err != nil {
|
||||
return "", fmt.Errorf("template id: %w", err)
|
||||
}
|
||||
|
||||
return templateID, nil
|
||||
}
|
||||
|
||||
func parseOptionalIdempotencyKey(raw string) (common.IdempotencyKey, error) {
|
||||
if raw == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
key := common.IdempotencyKey(strings.TrimSpace(raw))
|
||||
if err := key.Validate(); err != nil {
|
||||
return "", fmt.Errorf("idempotency key: %w", err)
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func parseOptionalUnixMilli(raw string, name string) (*time.Time, error) {
|
||||
if raw == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
value, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", name, err)
|
||||
}
|
||||
timestamp := time.UnixMilli(value).UTC()
|
||||
if err := common.ValidateTimestamp(name, timestamp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ×tamp, nil
|
||||
}
|
||||
|
||||
func parseOptionalLimit(raw string) (int, error) {
|
||||
if raw == "" {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
value, err := strconv.Atoi(strings.TrimSpace(raw))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("limit: %w", err)
|
||||
}
|
||||
if value < 1 {
|
||||
return 0, errors.New("limit must be at least 1")
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func parseOptionalCursor(raw string) (*listdeliveries.Cursor, error) {
|
||||
if raw == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
cursor, err := DecodeDeliveryListCursor(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &cursor, nil
|
||||
}
|
||||
|
||||
func summaryResponseFromDelivery(record deliverydomain.Delivery) DeliverySummaryResponse {
|
||||
return DeliverySummaryResponse{
|
||||
DeliveryID: record.DeliveryID.String(),
|
||||
ResendParentDeliveryID: record.ResendParentDeliveryID.String(),
|
||||
Source: string(record.Source),
|
||||
PayloadMode: string(record.PayloadMode),
|
||||
TemplateID: record.TemplateID.String(),
|
||||
To: emailStrings(record.Envelope.To),
|
||||
Cc: emailStrings(record.Envelope.Cc),
|
||||
Bcc: emailStrings(record.Envelope.Bcc),
|
||||
ReplyTo: emailStrings(record.Envelope.ReplyTo),
|
||||
Locale: record.Locale.String(),
|
||||
LocaleFallbackUsed: record.LocaleFallbackUsed,
|
||||
IdempotencyKey: record.IdempotencyKey.String(),
|
||||
Status: string(record.Status),
|
||||
AttemptCount: record.AttemptCount,
|
||||
LastAttemptStatus: string(record.LastAttemptStatus),
|
||||
ProviderSummary: record.ProviderSummary,
|
||||
CreatedAtMS: record.CreatedAt.UTC().UnixMilli(),
|
||||
UpdatedAtMS: record.UpdatedAt.UTC().UnixMilli(),
|
||||
SentAtMS: unixMilliPtr(record.SentAt),
|
||||
SuppressedAtMS: unixMilliPtr(record.SuppressedAt),
|
||||
FailedAtMS: unixMilliPtr(record.FailedAt),
|
||||
DeadLetteredAtMS: unixMilliPtr(record.DeadLetteredAt),
|
||||
}
|
||||
}
|
||||
|
||||
func detailResponseFromDelivery(record deliverydomain.Delivery, deadLetter *deliverydomain.DeadLetterEntry) DeliveryDetailResponse {
|
||||
response := DeliveryDetailResponse{
|
||||
DeliveryID: record.DeliveryID.String(),
|
||||
ResendParentDeliveryID: record.ResendParentDeliveryID.String(),
|
||||
Source: string(record.Source),
|
||||
PayloadMode: string(record.PayloadMode),
|
||||
TemplateID: record.TemplateID.String(),
|
||||
TemplateVariables: cloneJSONObject(record.TemplateVariables),
|
||||
To: emailStrings(record.Envelope.To),
|
||||
Cc: emailStrings(record.Envelope.Cc),
|
||||
Bcc: emailStrings(record.Envelope.Bcc),
|
||||
ReplyTo: emailStrings(record.Envelope.ReplyTo),
|
||||
Subject: record.Content.Subject,
|
||||
TextBody: record.Content.TextBody,
|
||||
HTMLBody: record.Content.HTMLBody,
|
||||
Attachments: attachmentResponses(record.Attachments),
|
||||
Locale: record.Locale.String(),
|
||||
LocaleFallbackUsed: record.LocaleFallbackUsed,
|
||||
IdempotencyKey: record.IdempotencyKey.String(),
|
||||
Status: string(record.Status),
|
||||
AttemptCount: record.AttemptCount,
|
||||
LastAttemptStatus: string(record.LastAttemptStatus),
|
||||
ProviderSummary: record.ProviderSummary,
|
||||
CreatedAtMS: record.CreatedAt.UTC().UnixMilli(),
|
||||
UpdatedAtMS: record.UpdatedAt.UTC().UnixMilli(),
|
||||
SentAtMS: unixMilliPtr(record.SentAt),
|
||||
SuppressedAtMS: unixMilliPtr(record.SuppressedAt),
|
||||
FailedAtMS: unixMilliPtr(record.FailedAt),
|
||||
DeadLetteredAtMS: unixMilliPtr(record.DeadLetteredAt),
|
||||
}
|
||||
if deadLetter != nil {
|
||||
response.DeadLetter = &DeadLetterResponse{
|
||||
DeliveryID: deadLetter.DeliveryID.String(),
|
||||
FinalAttemptNo: deadLetter.FinalAttemptNo,
|
||||
FailureClassification: deadLetter.FailureClassification,
|
||||
ProviderSummary: deadLetter.ProviderSummary,
|
||||
CreatedAtMS: deadLetter.CreatedAt.UTC().UnixMilli(),
|
||||
RecoveryHint: deadLetter.RecoveryHint,
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
func attemptResponseFromRecord(record attempt.Attempt) AttemptResponse {
|
||||
return AttemptResponse{
|
||||
DeliveryID: record.DeliveryID.String(),
|
||||
AttemptNo: record.AttemptNo,
|
||||
ScheduledForMS: record.ScheduledFor.UTC().UnixMilli(),
|
||||
StartedAtMS: unixMilliPtr(record.StartedAt),
|
||||
FinishedAtMS: unixMilliPtr(record.FinishedAt),
|
||||
Status: string(record.Status),
|
||||
ProviderClassification: record.ProviderClassification,
|
||||
ProviderSummary: record.ProviderSummary,
|
||||
}
|
||||
}
|
||||
|
||||
func attachmentResponses(attachments []common.AttachmentMetadata) []AttachmentResponse {
|
||||
if len(attachments) == 0 {
|
||||
return []AttachmentResponse{}
|
||||
}
|
||||
|
||||
result := make([]AttachmentResponse, len(attachments))
|
||||
for index, attachment := range attachments {
|
||||
result[index] = AttachmentResponse{
|
||||
Filename: attachment.Filename,
|
||||
ContentType: attachment.ContentType,
|
||||
SizeBytes: attachment.SizeBytes,
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func emailStrings(values []common.Email) []string {
|
||||
if len(values) == 0 {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
result := make([]string, len(values))
|
||||
for index, value := range values {
|
||||
result[index] = value.String()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func unixMilliPtr(value *time.Time) *int64 {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
encoded := value.UTC().UnixMilli()
|
||||
return &encoded
|
||||
}
|
||||
|
||||
func cloneJSONObject(value map[string]any) map[string]any {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
cloned := make(map[string]any, len(value))
|
||||
for key, entry := range value {
|
||||
cloned[key] = entry
|
||||
}
|
||||
|
||||
return cloned
|
||||
}
|
||||
Reference in New Issue
Block a user