Files
warmbly/internal/app/consumer/event_email_error.go
T

327 lines
9.5 KiB
Go

package jobs
import (
"context"
"github.com/google/uuid"
"github.com/rs/zerolog/log"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/repository"
)
// HandleEmailAuthError handles authentication errors that require user re-authorization
func (s *JobsService) HandleEmailAuthError(ctx context.Context, event models.EmailErrorEvent) error {
log.Info().
Str("email_account_id", event.EmailAccountID).
Str("error_code", event.ErrorCode).
Msg("Handling email auth error")
emailAccountID, err := uuid.Parse(event.EmailAccountID)
if err != nil {
log.Error().Err(err).Str("email_account_id", event.EmailAccountID).Msg("Invalid email account ID")
return err
}
userID, err := uuid.Parse(event.UserID)
if err != nil {
log.Error().Err(err).Str("user_id", event.UserID).Msg("Invalid user ID")
return err
}
var taskID *uuid.UUID
if event.TaskID != "" {
tid, err := uuid.Parse(event.TaskID)
if err == nil {
taskID = &tid
}
}
// Store error in database
if s.EmailAccountErrorRepository != nil {
errorRecord := &repository.CreateEmailAccountError{
EmailAccountID: emailAccountID,
UserID: userID,
ErrorCode: event.ErrorCode,
Severity: event.ErrorType,
ResolveMethod: event.ResolveMethod,
Title: event.UserTitle,
Message: event.Message,
UserMessage: ptrString(event.UserMessage),
ActionRequired: ptrString(event.ActionRequired),
TaskID: taskID,
}
if _, xerr := s.EmailAccountErrorRepository.Create(ctx, errorRecord); xerr != nil {
log.Error().Str("error", xerr.Message).Msg("Failed to store email auth error")
}
}
// Mark email account as needing re-auth (set status to inactive)
s.deactivateAccount(ctx, userID, emailAccountID)
// Send Pub/Sub notification to user
if s.StreamingPublisher != nil && event.UserVisible {
s.StreamingPublisher.PublishEmailError(
ctx,
event.UserID,
emailAccountID,
uuid.Nil,
event.UserTitle,
event.UserMessage,
)
}
return nil
}
// HandleEmailDisabled handles errors indicating the account has been disabled
func (s *JobsService) HandleEmailDisabled(ctx context.Context, event models.EmailErrorEvent) error {
log.Info().
Str("email_account_id", event.EmailAccountID).
Str("error_code", event.ErrorCode).
Msg("Handling email disabled error")
emailAccountID, err := uuid.Parse(event.EmailAccountID)
if err != nil {
log.Error().Err(err).Str("email_account_id", event.EmailAccountID).Msg("Invalid email account ID")
return err
}
userID, err := uuid.Parse(event.UserID)
if err != nil {
log.Error().Err(err).Str("user_id", event.UserID).Msg("Invalid user ID")
return err
}
var taskID *uuid.UUID
if event.TaskID != "" {
tid, err := uuid.Parse(event.TaskID)
if err == nil {
taskID = &tid
}
}
// Store error in database
if s.EmailAccountErrorRepository != nil {
errorRecord := &repository.CreateEmailAccountError{
EmailAccountID: emailAccountID,
UserID: userID,
ErrorCode: event.ErrorCode,
Severity: event.ErrorType,
ResolveMethod: event.ResolveMethod,
Title: event.UserTitle,
Message: event.Message,
UserMessage: ptrString(event.UserMessage),
ActionRequired: ptrString(event.ActionRequired),
TaskID: taskID,
}
if _, xerr := s.EmailAccountErrorRepository.Create(ctx, errorRecord); xerr != nil {
log.Error().Str("error", xerr.Message).Msg("Failed to store email disabled error")
}
}
// Mark email account as inactive
s.deactivateAccount(ctx, userID, emailAccountID)
// Send Pub/Sub notification to user
if s.StreamingPublisher != nil && event.UserVisible {
s.StreamingPublisher.PublishEmailError(
ctx,
event.UserID,
emailAccountID,
uuid.Nil,
event.UserTitle,
event.UserMessage,
)
}
return nil
}
// HandleEmailRateLimited handles rate limit exceeded errors (anti-abuse)
func (s *JobsService) HandleEmailRateLimited(ctx context.Context, event models.EmailErrorEvent) error {
log.Warn().
Str("email_account_id", event.EmailAccountID).
Str("error_code", event.ErrorCode).
Msg("Handling email rate limit exceeded")
emailAccountID, err := uuid.Parse(event.EmailAccountID)
if err != nil {
log.Error().Err(err).Str("email_account_id", event.EmailAccountID).Msg("Invalid email account ID")
return err
}
userID, err := uuid.Parse(event.UserID)
if err != nil {
log.Error().Err(err).Str("user_id", event.UserID).Msg("Invalid user ID")
return err
}
var taskID *uuid.UUID
if event.TaskID != "" {
tid, err := uuid.Parse(event.TaskID)
if err == nil {
taskID = &tid
}
}
// Store error in database
if s.EmailAccountErrorRepository != nil {
errorRecord := &repository.CreateEmailAccountError{
EmailAccountID: emailAccountID,
UserID: userID,
ErrorCode: event.ErrorCode,
Severity: event.ErrorType,
ResolveMethod: event.ResolveMethod,
Title: event.UserTitle,
Message: event.Message,
UserMessage: ptrString(event.UserMessage),
ActionRequired: ptrString(event.ActionRequired),
TaskID: taskID,
}
if _, xerr := s.EmailAccountErrorRepository.Create(ctx, errorRecord); xerr != nil {
log.Error().Str("error", xerr.Message).Msg("Failed to store rate limit error")
}
}
// Mark email account as inactive (terminated due to abuse)
s.deactivateAccount(ctx, userID, emailAccountID)
if s.WarmupService != nil {
health, _ := s.WarmupService.ApplyRateLimitExceeded(ctx, emailAccountID, "worker sync/email rate limit exceeded")
s.markRiskBandFromWarmupHealth(ctx, emailAccountID, health)
}
// Send Pub/Sub notification to user (this is a warning notification)
if s.StreamingPublisher != nil && event.UserVisible {
s.StreamingPublisher.PublishEmailWarning(
ctx,
event.UserID,
emailAccountID,
event.UserTitle,
event.UserMessage,
)
}
return nil
}
// HandleEmailServerError handles temporary server errors (may auto-resolve)
func (s *JobsService) HandleEmailServerError(ctx context.Context, event models.EmailErrorEvent) error {
log.Info().
Str("email_account_id", event.EmailAccountID).
Str("error_code", event.ErrorCode).
Msg("Handling email server error")
emailAccountID, err := uuid.Parse(event.EmailAccountID)
if err != nil {
log.Error().Err(err).Str("email_account_id", event.EmailAccountID).Msg("Invalid email account ID")
return err
}
userID, err := uuid.Parse(event.UserID)
if err != nil {
log.Error().Err(err).Str("user_id", event.UserID).Msg("Invalid user ID")
return err
}
var taskID *uuid.UUID
if event.TaskID != "" {
tid, err := uuid.Parse(event.TaskID)
if err == nil {
taskID = &tid
}
}
// Store error in database (as warning, not critical)
if s.EmailAccountErrorRepository != nil {
errorRecord := &repository.CreateEmailAccountError{
EmailAccountID: emailAccountID,
UserID: userID,
ErrorCode: event.ErrorCode,
Severity: "WARNING",
ResolveMethod: event.ResolveMethod,
Title: event.UserTitle,
Message: event.Message,
UserMessage: ptrString(event.UserMessage),
ActionRequired: ptrString(event.ActionRequired),
TaskID: taskID,
}
if _, xerr := s.EmailAccountErrorRepository.Create(ctx, errorRecord); xerr != nil {
log.Error().Str("error", xerr.Message).Msg("Failed to store server error")
}
}
// Server errors are temporary - don't change account status
// The error will be auto-resolved when connectivity is restored
// Only send notification if error persists (based on user visibility flag)
if s.StreamingPublisher != nil && event.UserVisible {
s.StreamingPublisher.PublishEmailWarning(
ctx,
event.UserID,
emailAccountID,
event.UserTitle,
event.UserMessage,
)
}
return nil
}
// ptrString returns a pointer to a string, or nil if empty
func ptrString(s string) *string {
if s == "" {
return nil
}
return &s
}
// deactivateAccount marks a mailbox inactive AND tells the worker holding it
// to drop it. The per-worker load filters on status, but that only decides what
// a worker picks up when it starts, and the send path relays an account-level
// failure without stopping the mailbox's own sync loop: without this the worker
// keeps syncing a mailbox that has just been deactivated, failing every pass
// and writing an email_account_errors row each time, until someone restarts it.
func (s *JobsService) deactivateAccount(ctx context.Context, userID, emailAccountID uuid.UUID) {
if s.EmailRepository == nil {
return
}
inactive := "inactive"
if _, xerr := s.EmailRepository.Update(ctx, userID.String(), emailAccountID.String(), &models.UpdateEmail{
Status: &inactive,
}); xerr != nil {
log.Error().Str("error", xerr.Message).Msg("Failed to update email account status")
return
}
if s.Publisher == nil {
return
}
// Asked for on its own rather than read off the row Update returned: that
// row is the mailbox as the dashboard sees it and carries no assignment.
workerID, xerr := s.EmailRepository.GetWorkerID(ctx, emailAccountID)
if xerr != nil {
log.Warn().
Str("error", xerr.Message).
Str("email_account_id", emailAccountID.String()).
Msg("Cannot tell the worker to drop the deactivated mailbox: assignment lookup failed")
return
}
if workerID == nil {
return
}
if err := s.Publisher.PublishRemoveEmail(ctx, *workerID, &models.RemoveWorkerEmail{
UserID: userID.String(),
EmailID: emailAccountID.String(),
}); err != nil {
log.Warn().Err(err).
Str("email_account_id", emailAccountID.String()).
Str("worker_id", workerID.String()).
Msg("Failed to tell the worker to drop the deactivated mailbox")
}
}