init push
This commit is contained in:
119
internal/service/email_job_handler.go
Normal file
119
internal/service/email_job_handler.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/aws/smithy-go"
|
||||
|
||||
"wucher/internal/queue"
|
||||
)
|
||||
|
||||
type EmailDeliveryHandler struct {
|
||||
sender EmailSender
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func NewEmailDeliveryHandler(sender EmailSender, logger *slog.Logger) *EmailDeliveryHandler {
|
||||
if logger == nil {
|
||||
logger = slog.Default()
|
||||
}
|
||||
return &EmailDeliveryHandler{
|
||||
sender: sender,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *EmailDeliveryHandler) Handle(ctx context.Context, envelope *queue.Envelope) error {
|
||||
if h.sender == nil {
|
||||
return queue.Permanent(errors.New("email sender is required"))
|
||||
}
|
||||
if envelope == nil {
|
||||
return queue.Permanent(errors.New("email envelope is required"))
|
||||
}
|
||||
if err := envelope.Payload.Validate(); err != nil {
|
||||
return queue.Permanent(err)
|
||||
}
|
||||
|
||||
job := envelope.Payload
|
||||
shouldSkipSend := strings.EqualFold(strings.TrimSpace(job.Metadata["skip_send"]), "true")
|
||||
// Defense-in-depth: register verification emails should never be delivered.
|
||||
if !shouldSkipSend &&
|
||||
strings.EqualFold(strings.TrimSpace(job.MessageType), "email_verify") &&
|
||||
strings.EqualFold(strings.TrimSpace(job.Tags["source"]), "register") {
|
||||
shouldSkipSend = true
|
||||
}
|
||||
if shouldSkipSend {
|
||||
h.logger.Info("email delivery skipped",
|
||||
slog.String("message_id", envelope.MessageID),
|
||||
slog.String("job_type", job.NormalizedType()),
|
||||
slog.String("recipient_hash", job.RecipientHash()),
|
||||
slog.String("correlation_id", envelope.CorrelationID),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
result, err := h.sender.Send(ctx, job)
|
||||
if err != nil {
|
||||
classified := classifyDeliveryError(err)
|
||||
h.logger.Error("email delivery failed",
|
||||
slog.String("message_id", envelope.MessageID),
|
||||
slog.String("job_type", job.NormalizedType()),
|
||||
slog.String("recipient_hash", job.RecipientHash()),
|
||||
slog.String("correlation_id", envelope.CorrelationID),
|
||||
slog.Any("error", err),
|
||||
)
|
||||
return classified
|
||||
}
|
||||
|
||||
h.logger.Info("email delivered",
|
||||
slog.String("message_id", envelope.MessageID),
|
||||
slog.String("job_type", job.NormalizedType()),
|
||||
slog.String("recipient_hash", job.RecipientHash()),
|
||||
slog.String("correlation_id", envelope.CorrelationID),
|
||||
slog.String("provider", result.Provider),
|
||||
slog.String("provider_message_id", result.ProviderMessageID),
|
||||
slog.String("provider_request_id", result.RequestID),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
func classifyDeliveryError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||
return queue.Transient(err)
|
||||
}
|
||||
var netErr net.Error
|
||||
if errors.As(err, &netErr) {
|
||||
return queue.Transient(err)
|
||||
}
|
||||
var apiErr smithy.APIError
|
||||
if errors.As(err, &apiErr) {
|
||||
code := strings.ToLower(strings.TrimSpace(apiErr.ErrorCode()))
|
||||
switch code {
|
||||
case "throttling", "throttlingexception", "toomanyrequestsexception", "serviceunavailableexception", "internalserviceerror", "sendingpausedexception":
|
||||
return queue.Transient(err)
|
||||
case "message_rejected", "messagerejected", "badrequestexception", "mailfromdomainnotverifiedexception", "configurationsetdoesnotexistexception", "notfoundexception":
|
||||
return queue.Permanent(err)
|
||||
}
|
||||
}
|
||||
message := strings.ToLower(strings.TrimSpace(err.Error()))
|
||||
switch {
|
||||
case strings.Contains(message, "recipient is required"):
|
||||
return queue.Permanent(err)
|
||||
case strings.Contains(message, "subject is required"):
|
||||
return queue.Permanent(err)
|
||||
case strings.Contains(message, "body is required"):
|
||||
return queue.Permanent(err)
|
||||
case strings.Contains(message, "invalid email job"):
|
||||
return queue.Permanent(err)
|
||||
case strings.Contains(message, "ses is not configured"):
|
||||
return queue.Permanent(err)
|
||||
default:
|
||||
return queue.Transient(err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user