Skip to content

feat: add email warmup send limits for connected accounts (#206)#452

Open
Kishalll wants to merge 1 commit into
Kuldeeep18:mainfrom
Kishalll:feature/206-email-warmup-limits
Open

feat: add email warmup send limits for connected accounts (#206)#452
Kishalll wants to merge 1 commit into
Kuldeeep18:mainfrom
Kishalll:feature/206-email-warmup-limits

Conversation

@Kishalll

@Kishalll Kishalll commented Jun 24, 2026

Copy link
Copy Markdown

🔗 Related Issue

Closes #206


📝 Summary of Changes

Implemented email warmup send limits for connected email accounts to prevent new accounts from triggering spam filters by immediately sending hundreds of emails.

Core changes:

  • Added warmup_enabled, daily_sending_limit, current_daily_count, and warmup_started_at fields to the ConnectedEmailAccount model
  • Added a warmup guard in the send_email_step Celery task that blocks sends when the daily limit is reached and retries after 1 hour
  • Created a warmup_daily_reset periodic task that runs at midnight UTC to reset daily send counts and increment warmup limits by +5/day (capped at WARMUP_MAX_DAILY)
  • Used F() expressions for atomic increment/decrement to prevent race conditions with concurrent Celery workers
  • Added WARMUP_MAX_DAILY setting (default 100, configurable via env var)

Cleanup:

  • Fixed corrupted .gitignore (had null bytes preventing proper ignore rules)
  • Removed all __pycache__ files and db.sqlite3 from git tracking

🏷️ Type of Change

  • 🐛 Bug fix
  • ✨ New feature
  • ♻️ Refactor
  • 📝 Documentation update
  • 🎨 UI / Style change
  • 🔧 Chore

🧪 Testing

Steps to test:

  1. Run python manage.py check — should pass with no issues
  2. Run python manage.py test campaigns — 53 tests pass (1 pre-existing Django/Python 3.14 compatibility error unrelated to this change)
  3. Connect an email account, enable warmup via the model, and verify that sends are blocked once current_daily_count reaches daily_sending_limit
  4. Verify warmup_daily_reset task resets counts and increments limits correctly

📸 Screenshots (if applicable)

N/A — backend-only change, no UI modifications.


✅ Checklist

  • No merge conflicts
  • Changes follow the project guidelines
  • Documentation updated (if applicable)
  • Related issue linked
  • Changes tested locally (if applicable)

Note: The migration file (0011_connectedemailaccount_current_daily_count_and_more.py) was not included in this commit. It needs to be generated and applied during deployment via python manage.py makemigrations campaigns && python manage.py migrate.

Summary by CodeRabbit

  • New Features
    • Added email warmup controls for connected accounts, including a daily sending limit, today’s send counter, and a warmup start date.
    • Implemented a daily warmup reset at midnight that clears today’s counters and recalculates the daily limit with an upper cap via configuration.
  • Bug Fixes
    • Enforces the daily limit by delaying sends and retrying later.
    • Keeps daily send counters accurate by decrementing the counter when an email send fails.

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e9a0973d-7c5d-4b8f-a338-c69cb05e8825

📥 Commits

Reviewing files that changed from the base of the PR and between b3c083f and 61fd5c4.

⛔ Files ignored due to path filters (51)
  • backend/backend/__pycache__/__init__.cpython-314.pyc is excluded by !**/*.pyc
  • backend/backend/__pycache__/celery.cpython-314.pyc is excluded by !**/*.pyc
  • backend/backend/__pycache__/settings.cpython-314.pyc is excluded by !**/*.pyc
  • backend/backend/__pycache__/urls.cpython-314.pyc is excluded by !**/*.pyc
  • backend/backend/__pycache__/wsgi.cpython-314.pyc is excluded by !**/*.pyc
  • backend/campaigns/__pycache__/__init__.cpython-314.pyc is excluded by !**/*.pyc
  • backend/campaigns/__pycache__/ai.cpython-314.pyc is excluded by !**/*.pyc
  • backend/campaigns/__pycache__/apps.cpython-314.pyc is excluded by !**/*.pyc
  • backend/campaigns/__pycache__/gmail_service.cpython-314.pyc is excluded by !**/*.pyc
  • backend/campaigns/__pycache__/google_auth_views.cpython-314.pyc is excluded by !**/*.pyc
  • backend/campaigns/__pycache__/models.cpython-314.pyc is excluded by !**/*.pyc
  • backend/campaigns/__pycache__/serializers.cpython-314.pyc is excluded by !**/*.pyc
  • backend/campaigns/__pycache__/tasks.cpython-314.pyc is excluded by !**/*.pyc
  • backend/campaigns/__pycache__/tests.cpython-314.pyc is excluded by !**/*.pyc
  • backend/campaigns/__pycache__/views.cpython-314.pyc is excluded by !**/*.pyc
  • backend/campaigns/migrations/__pycache__/0001_initial.cpython-314.pyc is excluded by !**/*.pyc
  • backend/campaigns/migrations/__pycache__/0002_campaignlead_last_sent_message_id_and_more.cpython-314.pyc is excluded by !**/*.pyc
  • backend/campaigns/migrations/__pycache__/0003_alter_sequencestep_channel_type.cpython-314.pyc is excluded by !**/*.pyc
  • backend/campaigns/migrations/__pycache__/0004_connectedemailaccount_connected_by.cpython-314.pyc is excluded by !**/*.pyc
  • backend/campaigns/migrations/__pycache__/__init__.cpython-314.pyc is excluded by !**/*.pyc
  • backend/config/__pycache__/__init__.cpython-314.pyc is excluded by !**/*.pyc
  • backend/config/__pycache__/settings.cpython-314.pyc is excluded by !**/*.pyc
  • backend/config/__pycache__/urls.cpython-314.pyc is excluded by !**/*.pyc
  • backend/config/__pycache__/wsgi.cpython-314.pyc is excluded by !**/*.pyc
  • backend/leads/__pycache__/__init__.cpython-314.pyc is excluded by !**/*.pyc
  • backend/leads/__pycache__/apps.cpython-314.pyc is excluded by !**/*.pyc
  • backend/leads/__pycache__/models.cpython-314.pyc is excluded by !**/*.pyc
  • backend/leads/__pycache__/serializers.cpython-314.pyc is excluded by !**/*.pyc
  • backend/leads/__pycache__/tasks.cpython-314.pyc is excluded by !**/*.pyc
  • backend/leads/__pycache__/tests.cpython-314.pyc is excluded by !**/*.pyc
  • backend/leads/__pycache__/views.cpython-314.pyc is excluded by !**/*.pyc
  • backend/leads/migrations/__pycache__/0001_initial.cpython-314.pyc is excluded by !**/*.pyc
  • backend/leads/migrations/__pycache__/__init__.cpython-314.pyc is excluded by !**/*.pyc
  • backend/tenants/__pycache__/__init__.cpython-314.pyc is excluded by !**/*.pyc
  • backend/tenants/__pycache__/admin.cpython-314.pyc is excluded by !**/*.pyc
  • backend/tenants/__pycache__/apps.cpython-314.pyc is excluded by !**/*.pyc
  • backend/tenants/__pycache__/middleware.cpython-314.pyc is excluded by !**/*.pyc
  • backend/tenants/__pycache__/models.cpython-314.pyc is excluded by !**/*.pyc
  • backend/tenants/__pycache__/security.cpython-314.pyc is excluded by !**/*.pyc
  • backend/tenants/__pycache__/tests.cpython-314.pyc is excluded by !**/*.pyc
  • backend/tenants/migrations/__pycache__/0001_initial.cpython-314.pyc is excluded by !**/*.pyc
  • backend/tenants/migrations/__pycache__/__init__.cpython-314.pyc is excluded by !**/*.pyc
  • backend/users/__pycache__/__init__.cpython-314.pyc is excluded by !**/*.pyc
  • backend/users/__pycache__/apps.cpython-314.pyc is excluded by !**/*.pyc
  • backend/users/__pycache__/jwt.cpython-314.pyc is excluded by !**/*.pyc
  • backend/users/__pycache__/models.cpython-314.pyc is excluded by !**/*.pyc
  • backend/users/__pycache__/serializers.cpython-314.pyc is excluded by !**/*.pyc
  • backend/users/__pycache__/tests.cpython-314.pyc is excluded by !**/*.pyc
  • backend/users/__pycache__/views.cpython-314.pyc is excluded by !**/*.pyc
  • backend/users/migrations/__pycache__/0001_initial.cpython-314.pyc is excluded by !**/*.pyc
  • backend/users/migrations/__pycache__/__init__.cpython-314.pyc is excluded by !**/*.pyc
📒 Files selected for processing (5)
  • .gitignore
  • backend/backend/settings.py
  • backend/campaigns/models.py
  • backend/campaigns/tasks.py
  • backend/db.sqlite3
🚧 Files skipped from review as they are similar to previous changes (2)
  • backend/campaigns/models.py
  • backend/backend/settings.py

📝 Walkthrough

Walkthrough

Adds email warmup rate-limiting to ConnectedEmailAccount: four new model fields track warmup state and daily send counts. send_email_step enforces the daily limit with a Celery retry-on-overflow and an F()-based failure decrement. A new nightly warmup_daily_reset task resets counters and ramps limits up by 5 per day, capped by WARMUP_MAX_DAILY.

Changes

Email Warmup Send Limits

Layer / File(s) Summary
Warmup model fields and settings config
backend/campaigns/models.py, backend/backend/settings.py
ConnectedEmailAccount gains warmup_enabled, daily_sending_limit, current_daily_count, and warmup_started_at fields. Settings adds crontab import, WARMUP_MAX_DAILY env-based config (default 100), and the warmup-daily-reset beat schedule entry running daily at midnight.
send_email_step throttle and failure decrement
backend/campaigns/tasks.py
Imports Case, F, Q, and When; adds a pre-send check that defers the lead and retries after 1 hour when current_daily_count >= daily_sending_limit, increments current_daily_count via F() on a successful send path, and decrements it via F() on the failure path.
warmup_daily_reset periodic task
backend/campaigns/tasks.py
New @shared_task resets current_daily_count to 0 for all accounts and uses a Case/When expression to increment daily_sending_limit by 5 for warmup-enabled accounts, capped at WARMUP_MAX_DAILY.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

  • #206 (LO-027): Add "Warm Up" Send Limits for Connected Email Accounts — This PR directly implements the model fields, throttle enforcement in tasks.py, and the daily periodic reset/increment task described in the issue.
  • #118 — Describes the same warmup model fields, throttle/increment logic, and daily limit reset automation as implemented here.

Poem

🐇 Hop, hop, easy does it now,
No spam blizzard — we don't allow!
Each morning resets the daily count,
And five more emails is the new amount.
Warmup slowly, climb the ladder right,
Inbox glory, never spam's plight! 🌅

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding warmup send limits for connected email accounts.
Linked Issues check ✅ Passed The PR adds the required model fields, enforces the daily send limit in the task, and adds a daily limit-increase job.
Out of Scope Changes check ✅ Passed The reviewed changes stay focused on email warmup throttling and its supporting settings and model updates.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/campaigns/models.py`:
- Around line 42-45: The new persisted warmup fields on ConnectedEmailAccount in
models.py are not backed by a schema migration, so add a migration under
backend/campaigns/migrations that introduces warmup_enabled,
daily_sending_limit, current_daily_count, and warmup_started_at. Use the
existing ConnectedEmailAccount model as the reference point and ensure the
migration matches the field types/defaults currently defined in the model so
deployments stay in sync.

In `@backend/campaigns/tasks.py`:
- Around line 632-635: The failure handler in the task that updates
ConnectedEmailAccount current_daily_count can underflow if the midnight reset
already cleared the counter, so guard the decrement before applying the F()
update. In the failure path around account.warmup_enabled, check the current
value on the account model or use a conditional update in the same
ConnectedEmailAccount.objects.filter(...) call so current_daily_count never goes
below zero, then continue restoring next_execution_time as before.
- Around line 592-604: Make the warmup limit check and counter reservation
atomic in the task logic around account.warmup_enabled so concurrent workers
cannot overshoot daily_sending_limit. Update the flow in the task method that
handles the current_daily_count check to reserve a sending slot with a single
conditional ConnectedEmailAccount.objects.filter(...).update(...) using the
limit predicate, then branch on whether the update succeeded; if it did not,
keep the existing retry/next_execution_time behavior, and if it did, proceed
with sending. Ensure the logger message and retry path remain in the same warmup
handling block.
- Line 601: The retry handling in the task is broken because send_email_step is
unbound, so self.retry cannot be used safely and Celery’s Retry may be swallowed
by the broad exception handler. Bind send_email_step so it can call retry
correctly, and add an explicit except Retry: raise before the generic except
Exception block so the retry propagates instead of being caught.
- Around line 896-900: The warmup update in
ConnectedEmailAccount.objects.filter(...).update() is still allowing
daily_sending_limit to exceed the configured cap because the Case only preserves
values already at or above max_daily and otherwise always adds 5. Update the
logic in the warmup task to clamp the result so it never goes past
WARMUP_MAX_DAILY, using the existing daily_sending_limit field and the
max_daily/WARMUP_MAX_DAILY symbols to locate the affected update expression.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 546d7edf-ee74-444c-a38d-88c6f24940f9

📥 Commits

Reviewing files that changed from the base of the PR and between 4a33158 and b3c083f.

⛔ Files ignored due to path filters (51)
  • backend/backend/__pycache__/__init__.cpython-314.pyc is excluded by !**/*.pyc
  • backend/backend/__pycache__/celery.cpython-314.pyc is excluded by !**/*.pyc
  • backend/backend/__pycache__/settings.cpython-314.pyc is excluded by !**/*.pyc
  • backend/backend/__pycache__/urls.cpython-314.pyc is excluded by !**/*.pyc
  • backend/backend/__pycache__/wsgi.cpython-314.pyc is excluded by !**/*.pyc
  • backend/campaigns/__pycache__/__init__.cpython-314.pyc is excluded by !**/*.pyc
  • backend/campaigns/__pycache__/ai.cpython-314.pyc is excluded by !**/*.pyc
  • backend/campaigns/__pycache__/apps.cpython-314.pyc is excluded by !**/*.pyc
  • backend/campaigns/__pycache__/gmail_service.cpython-314.pyc is excluded by !**/*.pyc
  • backend/campaigns/__pycache__/google_auth_views.cpython-314.pyc is excluded by !**/*.pyc
  • backend/campaigns/__pycache__/models.cpython-314.pyc is excluded by !**/*.pyc
  • backend/campaigns/__pycache__/serializers.cpython-314.pyc is excluded by !**/*.pyc
  • backend/campaigns/__pycache__/tasks.cpython-314.pyc is excluded by !**/*.pyc
  • backend/campaigns/__pycache__/tests.cpython-314.pyc is excluded by !**/*.pyc
  • backend/campaigns/__pycache__/views.cpython-314.pyc is excluded by !**/*.pyc
  • backend/campaigns/migrations/__pycache__/0001_initial.cpython-314.pyc is excluded by !**/*.pyc
  • backend/campaigns/migrations/__pycache__/0002_campaignlead_last_sent_message_id_and_more.cpython-314.pyc is excluded by !**/*.pyc
  • backend/campaigns/migrations/__pycache__/0003_alter_sequencestep_channel_type.cpython-314.pyc is excluded by !**/*.pyc
  • backend/campaigns/migrations/__pycache__/0004_connectedemailaccount_connected_by.cpython-314.pyc is excluded by !**/*.pyc
  • backend/campaigns/migrations/__pycache__/__init__.cpython-314.pyc is excluded by !**/*.pyc
  • backend/config/__pycache__/__init__.cpython-314.pyc is excluded by !**/*.pyc
  • backend/config/__pycache__/settings.cpython-314.pyc is excluded by !**/*.pyc
  • backend/config/__pycache__/urls.cpython-314.pyc is excluded by !**/*.pyc
  • backend/config/__pycache__/wsgi.cpython-314.pyc is excluded by !**/*.pyc
  • backend/leads/__pycache__/__init__.cpython-314.pyc is excluded by !**/*.pyc
  • backend/leads/__pycache__/apps.cpython-314.pyc is excluded by !**/*.pyc
  • backend/leads/__pycache__/models.cpython-314.pyc is excluded by !**/*.pyc
  • backend/leads/__pycache__/serializers.cpython-314.pyc is excluded by !**/*.pyc
  • backend/leads/__pycache__/tasks.cpython-314.pyc is excluded by !**/*.pyc
  • backend/leads/__pycache__/tests.cpython-314.pyc is excluded by !**/*.pyc
  • backend/leads/__pycache__/views.cpython-314.pyc is excluded by !**/*.pyc
  • backend/leads/migrations/__pycache__/0001_initial.cpython-314.pyc is excluded by !**/*.pyc
  • backend/leads/migrations/__pycache__/__init__.cpython-314.pyc is excluded by !**/*.pyc
  • backend/tenants/__pycache__/__init__.cpython-314.pyc is excluded by !**/*.pyc
  • backend/tenants/__pycache__/admin.cpython-314.pyc is excluded by !**/*.pyc
  • backend/tenants/__pycache__/apps.cpython-314.pyc is excluded by !**/*.pyc
  • backend/tenants/__pycache__/middleware.cpython-314.pyc is excluded by !**/*.pyc
  • backend/tenants/__pycache__/models.cpython-314.pyc is excluded by !**/*.pyc
  • backend/tenants/__pycache__/security.cpython-314.pyc is excluded by !**/*.pyc
  • backend/tenants/__pycache__/tests.cpython-314.pyc is excluded by !**/*.pyc
  • backend/tenants/migrations/__pycache__/0001_initial.cpython-314.pyc is excluded by !**/*.pyc
  • backend/tenants/migrations/__pycache__/__init__.cpython-314.pyc is excluded by !**/*.pyc
  • backend/users/__pycache__/__init__.cpython-314.pyc is excluded by !**/*.pyc
  • backend/users/__pycache__/apps.cpython-314.pyc is excluded by !**/*.pyc
  • backend/users/__pycache__/jwt.cpython-314.pyc is excluded by !**/*.pyc
  • backend/users/__pycache__/models.cpython-314.pyc is excluded by !**/*.pyc
  • backend/users/__pycache__/serializers.cpython-314.pyc is excluded by !**/*.pyc
  • backend/users/__pycache__/tests.cpython-314.pyc is excluded by !**/*.pyc
  • backend/users/__pycache__/views.cpython-314.pyc is excluded by !**/*.pyc
  • backend/users/migrations/__pycache__/0001_initial.cpython-314.pyc is excluded by !**/*.pyc
  • backend/users/migrations/__pycache__/__init__.cpython-314.pyc is excluded by !**/*.pyc
📒 Files selected for processing (5)
  • .gitignore
  • backend/backend/settings.py
  • backend/campaigns/models.py
  • backend/campaigns/tasks.py
  • backend/db.sqlite3

Comment thread backend/campaigns/models.py
Comment thread backend/campaigns/tasks.py
Comment thread backend/campaigns/tasks.py
Comment thread backend/campaigns/tasks.py Outdated
Comment thread backend/campaigns/tasks.py
…#206)

- Add warmup_enabled, daily_sending_limit, current_daily_count,
  warmup_started_at fields to ConnectedEmailAccount model
- Add warmup guard in send_email_step: blocks sends when daily limit
  reached, retries in 1 hour, uses F() for atomic increment
- Set warmup_started_at on first warmup trigger
- Guard decrement on send failure to prevent negative count
- Add warmup_daily_reset periodic task: resets daily counts and
  increments limits by +5/day (capped at WARMUP_MAX_DAILY)
- Register midnight UTC beat schedule for daily reset
- Add WARMUP_MAX_DAILY setting (default 100, env-configurable)
- Fix .gitignore: remove corrupted null-byte entries, untrack
  __pycache__ files and db.sqlite3
@Kishalll Kishalll force-pushed the feature/206-email-warmup-limits branch from b3c083f to 61fd5c4 Compare June 24, 2026 12:55
@Kishalll

Copy link
Copy Markdown
Author

@Kuldeeep18 pls review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LO-027 [Medium]: Add "Warm Up" Send Limits for Connected Email Accounts

1 participant