mirror of
https://github.com/warmbly/warmbly.git
synced 2026-09-13 08:05:05 +00:00
327 lines
9.5 KiB
Go
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")
|
|
}
|
|
}
|