gohorsejobs/backend/internal/services/email_service.go

116 lines
3.2 KiB
Go

package services
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"log"
amqp "github.com/rabbitmq/amqp091-go"
)
// EmailService defines interface for email operations
type EmailService interface {
SendEmail(to, subject, body string) error
SendTemplateEmail(ctx context.Context, to, templateSlug string, variables map[string]interface{}) error
}
type emailServiceImpl struct {
db *sql.DB
credentialsService *CredentialsService
}
func NewEmailService(db *sql.DB, cs *CredentialsService) EmailService {
return &emailServiceImpl{
db: db,
credentialsService: cs,
}
}
type EmailJob struct {
To string `json:"to"`
Template string `json:"template"` // slug
Variables map[string]interface{} `json:"variables"`
}
// SendTemplateEmail queues an email via RabbitMQ
func (s *emailServiceImpl) SendTemplateEmail(ctx context.Context, to, templateSlug string, variables map[string]interface{}) error {
// 1. Get AMQP URL from email_settings
var amqpURL sql.NullString
err := s.db.QueryRowContext(ctx, "SELECT amqp_url FROM email_settings LIMIT 1").Scan(&amqpURL)
if err != nil && err != sql.ErrNoRows {
log.Printf("[EmailService] Failed to fetch AMQP URL: %v", err)
}
url := ""
if amqpURL.Valid {
url = amqpURL.String
}
if url == "" {
// Log but don't error hard if just testing, but for "system" we need IT.
// Return error so caller knows.
return fmt.Errorf("AMQP URL not configured in email_settings")
}
// 2. Connect & Publish
conn, err := amqp.Dial(url)
if err != nil {
return fmt.Errorf("failed to connect to RabbitMQ: %w", err)
}
defer conn.Close()
ch, err := conn.Channel()
if err != nil {
return fmt.Errorf("failed to open channel: %w", err)
}
defer ch.Close()
q, err := ch.QueueDeclare(
"mail_queue", // name
true, // durable
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // arguments
)
if err != nil {
return fmt.Errorf("failed to declare queue: %w", err)
}
job := EmailJob{To: to, Template: templateSlug, Variables: variables}
body, _ := json.Marshal(job)
err = ch.PublishWithContext(ctx,
"", // exchange
q.Name, // routing key
false, // mandatory
false, // immediate
amqp.Publishing{
ContentType: "application/json",
Body: body,
})
if err != nil {
return fmt.Errorf("failed to publish message: %w", err)
}
log.Printf("[EmailService] Queued email to %s (Template: %s)", to, templateSlug)
return nil
}
// SendEmail queues a simple email via RabbitMQ (using default template or direct logic)
// For compatibility with HEAD interface
func (s *emailServiceImpl) SendEmail(to, subject, body string) error {
// Wrap simpler calls into template engine if needed, or implement direct send.
// For now, we reuse SendTemplateEmail with a "generic" template or similar.
// Or we create a specific job for raw email.
// Let's assume we map it to a "generic_notification" template
vars := map[string]interface{}{
"subject": subject,
"body": body,
}
// Using background context for simple interface
return s.SendTemplateEmail(context.Background(), to, "generic_notification", vars)
}