Files
Memoh/internal/email/outbox.go
T

148 lines
3.8 KiB
Go

package email
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"github.com/memohai/memoh/internal/db"
"github.com/memohai/memoh/internal/db/sqlc"
)
// OutboxService manages the email outbox audit log.
type OutboxService struct {
queries *sqlc.Queries
logger *slog.Logger
}
func NewOutboxService(log *slog.Logger, queries *sqlc.Queries) *OutboxService {
return &OutboxService{
queries: queries,
logger: log.With(slog.String("service", "email_outbox")),
}
}
// Create records a pending outbound email.
func (s *OutboxService) Create(ctx context.Context, providerID, botID string, msg OutboundEmail, fromAddr string) (string, error) {
pgProviderID, err := db.ParseUUID(providerID)
if err != nil {
return "", fmt.Errorf("invalid provider_id: %w", err)
}
pgBotID, err := db.ParseUUID(botID)
if err != nil {
return "", fmt.Errorf("invalid bot_id: %w", err)
}
toJSON, _ := json.Marshal(msg.To)
bodyText, bodyHTML := msg.Body, ""
if msg.HTML {
bodyHTML = msg.Body
bodyText = ""
}
row, err := s.queries.CreateEmailOutbox(ctx, sqlc.CreateEmailOutboxParams{
ProviderID: pgProviderID,
BotID: pgBotID,
FromAddress: fromAddr,
ToAddresses: toJSON,
Subject: msg.Subject,
BodyText: bodyText,
BodyHtml: bodyHTML,
Attachments: []byte("[]"),
Status: "pending",
})
if err != nil {
return "", fmt.Errorf("create outbox: %w", err)
}
return row.ID.String(), nil
}
// MarkSent updates the outbox record with a successful send.
func (s *OutboxService) MarkSent(ctx context.Context, id, messageID string) error {
pgID, err := db.ParseUUID(id)
if err != nil {
return err
}
return s.queries.UpdateEmailOutboxSent(ctx, sqlc.UpdateEmailOutboxSentParams{
ID: pgID,
MessageID: messageID,
})
}
// MarkFailed updates the outbox record with an error.
func (s *OutboxService) MarkFailed(ctx context.Context, id, errMsg string) error {
pgID, err := db.ParseUUID(id)
if err != nil {
return err
}
return s.queries.UpdateEmailOutboxFailed(ctx, sqlc.UpdateEmailOutboxFailedParams{
ID: pgID,
Error: errMsg,
})
}
func (s *OutboxService) Get(ctx context.Context, id string) (OutboxItemResponse, error) {
pgID, err := db.ParseUUID(id)
if err != nil {
return OutboxItemResponse{}, err
}
row, err := s.queries.GetEmailOutboxByID(ctx, pgID)
if err != nil {
return OutboxItemResponse{}, fmt.Errorf("get outbox: %w", err)
}
return s.toOutboxResponse(row), nil
}
func (s *OutboxService) ListByBot(ctx context.Context, botID string, limit, offset int32) ([]OutboxItemResponse, int64, error) {
pgBotID, err := db.ParseUUID(botID)
if err != nil {
return nil, 0, err
}
rows, err := s.queries.ListEmailOutboxByBot(ctx, sqlc.ListEmailOutboxByBotParams{
BotID: pgBotID,
Lim: limit,
Off: offset,
})
if err != nil {
return nil, 0, fmt.Errorf("list outbox: %w", err)
}
count, err := s.queries.CountEmailOutboxByBot(ctx, pgBotID)
if err != nil {
return nil, 0, fmt.Errorf("count outbox: %w", err)
}
items := make([]OutboxItemResponse, 0, len(rows))
for _, row := range rows {
items = append(items, s.toOutboxResponse(row))
}
return items, count, nil
}
func (*OutboxService) toOutboxResponse(row sqlc.EmailOutbox) OutboxItemResponse {
var to []string
_ = json.Unmarshal(row.ToAddresses, &to)
var attachments []any
_ = json.Unmarshal(row.Attachments, &attachments)
var sentAt interface{ IsZero() bool }
resp := OutboxItemResponse{
ID: row.ID.String(),
ProviderID: row.ProviderID.String(),
BotID: row.BotID.String(),
MessageID: row.MessageID,
From: row.FromAddress,
To: to,
Subject: row.Subject,
BodyText: row.BodyText,
BodyHTML: row.BodyHtml,
Attachments: attachments,
Status: row.Status,
Error: row.Error,
CreatedAt: row.CreatedAt.Time,
}
_ = sentAt
if row.SentAt.Valid {
resp.SentAt = row.SentAt.Time
}
return resp
}