142 lines
3.6 KiB
Go
142 lines
3.6 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"wucher/internal/shared/pkg/uuidv7"
|
|
"wucher/internal/queue"
|
|
)
|
|
|
|
type MockEmailSender struct {
|
|
dir string
|
|
}
|
|
|
|
type MockEmailRecord struct {
|
|
StoredAt time.Time `json:"stored_at"`
|
|
Provider string `json:"provider"`
|
|
JobID string `json:"job_id"`
|
|
MessageType string `json:"message_type"`
|
|
To string `json:"to"`
|
|
CC []string `json:"cc,omitempty"`
|
|
BCC []string `json:"bcc,omitempty"`
|
|
ReplyTo []string `json:"reply_to,omitempty"`
|
|
Subject string `json:"subject"`
|
|
TextBody string `json:"text_body,omitempty"`
|
|
HTMLBody string `json:"html_body,omitempty"`
|
|
Tags map[string]string `json:"tags,omitempty"`
|
|
Metadata map[string]string `json:"metadata,omitempty"`
|
|
TraceID string `json:"trace_id,omitempty"`
|
|
RequestID string `json:"request_id,omitempty"`
|
|
}
|
|
|
|
func NewMockEmailSender(dir string) *MockEmailSender {
|
|
return &MockEmailSender{dir: strings.TrimSpace(dir)}
|
|
}
|
|
|
|
func (s *MockEmailSender) Configured() bool {
|
|
return s != nil && strings.TrimSpace(s.dir) != ""
|
|
}
|
|
|
|
func (s *MockEmailSender) Send(ctx context.Context, job queue.EmailJob) (EmailDeliveryResult, error) {
|
|
if !s.Configured() {
|
|
return EmailDeliveryResult{}, errors.New("mock email sender is not configured")
|
|
}
|
|
job = job.Canonical()
|
|
if err := job.Validate(); err != nil {
|
|
return EmailDeliveryResult{}, err
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return EmailDeliveryResult{}, ctx.Err()
|
|
default:
|
|
}
|
|
|
|
if err := os.MkdirAll(s.dir, 0o755); err != nil {
|
|
return EmailDeliveryResult{}, err
|
|
}
|
|
|
|
jobID := strings.TrimSpace(job.JobID)
|
|
if jobID == "" {
|
|
var err error
|
|
jobID, err = uuidv7.String(uuidv7.MustNew())
|
|
if err != nil {
|
|
return EmailDeliveryResult{}, err
|
|
}
|
|
}
|
|
filename := time.Now().UTC().Format("20060102T150405.000000000Z") + "_" + sanitizeFileName(job.NormalizedType()) + "_" + sanitizeFileName(jobID) + ".json"
|
|
path := filepath.Join(s.dir, filename)
|
|
|
|
record := MockEmailRecord{
|
|
StoredAt: time.Now().UTC(),
|
|
Provider: "mock",
|
|
JobID: jobID,
|
|
MessageType: job.NormalizedType(),
|
|
To: job.To,
|
|
CC: append([]string(nil), job.CC...),
|
|
BCC: append([]string(nil), job.BCC...),
|
|
ReplyTo: append([]string(nil), job.ReplyTo...),
|
|
Subject: job.Subject,
|
|
TextBody: job.TextBodyValue(),
|
|
HTMLBody: job.HTMLBody,
|
|
Tags: cloneStringMap(job.Tags),
|
|
Metadata: cloneStringMap(job.Metadata),
|
|
TraceID: job.TraceID,
|
|
RequestID: job.RequestID,
|
|
}
|
|
payload, err := json.MarshalIndent(record, "", " ")
|
|
if err != nil {
|
|
return EmailDeliveryResult{}, err
|
|
}
|
|
payload = append(payload, '\n')
|
|
|
|
if err := os.WriteFile(path, payload, 0o600); err != nil {
|
|
return EmailDeliveryResult{}, err
|
|
}
|
|
return EmailDeliveryResult{
|
|
Provider: "mock",
|
|
ProviderMessageID: filename,
|
|
RequestID: path,
|
|
}, nil
|
|
}
|
|
|
|
func sanitizeFileName(value string) string {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" {
|
|
return "email"
|
|
}
|
|
var b strings.Builder
|
|
b.Grow(len(value))
|
|
for _, r := range value {
|
|
switch {
|
|
case r >= 'a' && r <= 'z':
|
|
b.WriteRune(r)
|
|
case r >= 'A' && r <= 'Z':
|
|
b.WriteRune(r)
|
|
case r >= '0' && r <= '9':
|
|
b.WriteRune(r)
|
|
case r == '-', r == '_':
|
|
b.WriteRune(r)
|
|
default:
|
|
b.WriteRune('_')
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func cloneStringMap(values map[string]string) map[string]string {
|
|
if len(values) == 0 {
|
|
return nil
|
|
}
|
|
out := make(map[string]string, len(values))
|
|
for key, value := range values {
|
|
out[key] = value
|
|
}
|
|
return out
|
|
}
|