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 }