Skip to content

[BACKEND] Implement email tracking & rate limiting system #374

@Andreawingardh

Description

@Andreawingardh

GitHub Issue: Implement Email Tracking & Rate Limiting System

Background

Currently considering adding rate limiting for email sending. Rather than adding transient rate-limiting data to the User model, we should implement a dedicated email tracking system that can:

  • Rate limit any type of email (confirmation, password reset, notifications, etc.)
  • Track email sending history
  • Be easily extended for future email types

Proposed Architecture

1. Create EmailLog Entity

public class EmailLog
{
    public int Id { get; set; }
    public string UserId { get; set; }  // Foreign key to User
    public User User { get; set; }
    public string EmailType { get; set; }  // "Confirmation", "PasswordReset", etc.
    public string RecipientEmail { get; set; }
    public DateTime SentAt { get; set; }
    public bool WasSuccessful { get; set; }
    public string? ErrorMessage { get; set; }
}

2. Add DbSet to AppDbContext

public DbSet<EmailLog> EmailLogs { get; set; }

3. Create IEmailRateLimiter Service

public interface IEmailRateLimiter
{
    Task<bool> CanSendEmailAsync(string userId, string emailType, int cooldownMinutes = 1);
    Task LogEmailSentAsync(string userId, string emailType, string recipientEmail, bool wasSuccessful, string? errorMessage = null);
}

4. Implement EmailRateLimiter

  • Query EmailLogs for user + email type
  • Check if last send was within cooldown period
  • Return true/false for rate limit check
  • Log each email attempt with timestamp and result

5. Integration Points

Update these endpoints to use the rate limiter:

  • POST /api/auth/resend-confirmation-email
  • POST /api/auth/forgot-password (future)
  • Any other email-sending endpoints

Pattern for each endpoint:

// 1. Check rate limit
if (!await _emailRateLimiter.CanSendEmailAsync(userId, "Confirmation"))
{
    return BadRequest(new { errors = new[] { "Please wait before requesting another email" } });
}

// 2. Send email
try 
{
    await _emailSender.SendEmailAsync(...);
    await _emailRateLimiter.LogEmailSentAsync(userId, "Confirmation", email, true);
}
catch (Exception ex)
{
    await _emailRateLimiter.LogEmailSentAsync(userId, "Confirmation", email, false, ex.Message);
    throw;
}

Benefits

  • ✅ Centralized rate limiting logic
  • ✅ Historical tracking of all emails sent
  • ✅ Easily extensible for new email types
  • ✅ Can implement different cooldown periods per email type
  • ✅ Debugging capabilities (can see when/why emails failed)
  • ✅ User model stays clean (only persistent user data)

Future Enhancements

  • Add cleanup job to delete old EmailLog records (e.g., older than 90 days)
  • Add admin dashboard to view email sending statistics
  • Implement exponential backoff for repeated failures
  • Add email type-specific configuration (different cooldowns)
  • Track attempt count per time window (e.g., max 3 per hour)

Testing Considerations

  • Unit tests for EmailRateLimiter service with in-memory database
  • Test cases:
    • First send (should allow)
    • Immediate resend (should block)
    • Resend after cooldown (should allow)
    • Multiple email types don't interfere
    • Failed sends are logged correctly

Priority

Medium - Can implement after Phase 2 (real email service) is complete.

Implementation Steps

  1. Create migration for EmailLog table
  2. Implement IEmailRateLimiter interface and service
  3. Register service in DI container
  4. Update resend-confirmation-email endpoint
  5. Write unit tests
  6. Test manually with real email service
  7. Document in API documentation

Current Workaround

For immediate needs, we can add LastEmailSentAt field to User model as a temporary solution, then refactor to this system when we have multiple email types.

Metadata

Metadata

Assignees

No one assigned

    Projects

    Status

    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions