package email import ( "context" "errors" "fmt" "log/slog" "sync" ) // Manager manages the lifecycle of all email receiving connections. type Manager struct { logger *slog.Logger service *Service trigger *Trigger outbox *OutboxService mu sync.Mutex conns map[string]Stopper // provider_id -> stopper stopped bool } func NewManager(log *slog.Logger, service *Service, trigger *Trigger, outbox *OutboxService) *Manager { return &Manager{ logger: log.With(slog.String("component", "email_manager")), service: service, trigger: trigger, outbox: outbox, conns: make(map[string]Stopper), } } // Start initializes receiving for all providers that have readable bindings. func (m *Manager) Start(ctx context.Context) error { providers, err := m.service.ListProviders(ctx, "") if err != nil { return fmt.Errorf("list email providers: %w", err) } for _, p := range providers { bindings, err := m.service.ListReadableBindingsByProvider(ctx, p.ID) if err != nil { m.logger.Error("failed to list bindings", slog.String("provider", p.ID), slog.Any("error", err)) continue } if len(bindings) == 0 { continue } if err := m.startProvider(ctx, p); err != nil { m.logger.Error("failed to start provider", slog.String("provider", p.ID), slog.Any("error", err)) } } return nil } func (m *Manager) startProvider(ctx context.Context, p ProviderResponse) error { m.mu.Lock() defer m.mu.Unlock() if m.stopped { return errors.New("manager is stopped") } if _, exists := m.conns[p.ID]; exists { return nil } receiver, err := m.service.registry.GetReceiver(ProviderName(p.Provider)) if err != nil { return err } config := p.Config if config == nil { config = make(map[string]any) } config["_provider_id"] = p.ID stopper, err := receiver.StartReceiving(ctx, config, m.trigger.HandleInbound) if err != nil { return fmt.Errorf("start receiving for %s: %w", p.ID, err) } m.conns[p.ID] = stopper m.logger.Info("started email receiving", slog.String("provider_id", p.ID), slog.String("type", p.Provider)) return nil } // RefreshProvider restarts receiving for a specific provider. func (m *Manager) RefreshProvider(ctx context.Context, providerID string) error { m.stopProvider(ctx, providerID) p, err := m.service.GetProvider(ctx, providerID) if err != nil { return err } bindings, err := m.service.ListReadableBindingsByProvider(ctx, providerID) if err != nil { return err } if len(bindings) == 0 { return nil } return m.startProvider(ctx, p) } func (m *Manager) stopProvider(ctx context.Context, providerID string) { m.mu.Lock() stopper, exists := m.conns[providerID] if exists { delete(m.conns, providerID) } m.mu.Unlock() if exists && stopper != nil { if err := stopper.Stop(ctx); err != nil { m.logger.Error("failed to stop provider", slog.String("provider_id", providerID), slog.Any("error", err)) } } } // Stop gracefully shuts down all receiving connections. func (m *Manager) Stop(ctx context.Context) { m.mu.Lock() m.stopped = true conns := make(map[string]Stopper, len(m.conns)) for k, v := range m.conns { conns[k] = v } m.conns = make(map[string]Stopper) m.mu.Unlock() for id, stopper := range conns { if err := stopper.Stop(ctx); err != nil { m.logger.Error("failed to stop provider", slog.String("provider_id", id), slog.Any("error", err)) } } } // SendEmail sends an email through the specified provider, recording to outbox. // If providerID is empty, it falls back to the first writable binding for the bot. func (m *Manager) SendEmail(ctx context.Context, botID string, providerID string, msg OutboundEmail) (string, error) { if providerID == "" { binding, err := m.service.GetBotBinding(ctx, botID) if err != nil { return "", err } if !binding.CanWrite { return "", fmt.Errorf("email write permission denied for bot %s", botID) } providerID = binding.EmailProviderID } providerName, config, err := m.service.ProviderConfig(ctx, providerID) if err != nil { return "", err } if config == nil { config = make(map[string]any) } config["_provider_id"] = providerID sender, err := m.service.registry.GetSender(providerName) if err != nil { return "", err } fromAddr, _ := config["username"].(string) outboxID, err := m.outbox.Create(ctx, providerID, botID, msg, fromAddr) if err != nil { return "", fmt.Errorf("record outbox: %w", err) } messageID, err := sender.Send(ctx, config, msg) if err != nil { _ = m.outbox.MarkFailed(ctx, outboxID, err.Error()) return "", fmt.Errorf("send email: %w", err) } _ = m.outbox.MarkSent(ctx, outboxID, messageID) return messageID, nil }