Skip to content

Conversation

@mosa-riel
Copy link
Contributor

@mosa-riel mosa-riel commented Oct 27, 2025

Add DATA_UPLOAD_MAX_MEMORY_SIZE to settings.py to allow configuration via environment variable. Django's default is 2.5MB which is too small for emails with larger attachments.

Without this setting in settings.py, Django ignores the environment variable and uses its hardcoded default, causing RequestDataTooBig errors when the MTA tries to deliver emails exceeding 2.5MB.

Can be overridden via DATA_UPLOAD_MAX_MEMORY_SIZE environment variable.

Summary by CodeRabbit

  • New Features

    • Public config now exposes outgoing body and attachment size limits; in-memory upload limit is auto-adjusted.
    • New image-proxy API endpoint.
  • Bug Fixes / Validation

    • Inbound emails over the limit return 413 with a clear error.
    • Outgoing drafts/sends enforce body and cumulative attachment size limits with user-facing errors.
  • UI

    • Attachment uploader enforces cumulative limits and shows toast/errors; size display formatting updated (binary KB/MB).
  • Tests

    • Added tests covering inbound and draft/attachment size behaviors.

Add DATA_UPLOAD_MAX_MEMORY_SIZE to settings.py to allow configuration
via environment variable. Django's default is 2.5MB which is too small
for emails with larger attachments.

Without this setting in settings.py, Django ignores the environment
variable and uses its hardcoded default, causing RequestDataTooBig
errors when the MTA tries to deliver emails exceeding 2.5MB.

Default: 2621440 (2.5MB)
Can be overridden via DATA_UPLOAD_MAX_MEMORY_SIZE environment variable.
@coderabbitai
Copy link

coderabbitai bot commented Oct 27, 2025

Walkthrough

Adds configurable byte-size limits for uploads, incoming emails, outgoing attachments and outgoing bodies; clamps DATA_UPLOAD_MAX_MEMORY_SIZE accordingly; enforces limits at inbound MTA intake, draft creation/update, and outbound MIME composition; updates API/OpenAPI, frontend config/uploader, translations, and tests.

Changes

Cohort / File(s) Summary
Settings / config defaults
src/backend/messages/settings.py
Added DATA_UPLOAD_MAX_MEMORY_SIZE, MAX_INCOMING_EMAIL_SIZE, MAX_OUTGOING_ATTACHMENT_SIZE, MAX_OUTGOING_BODY_SIZE and clamp logic to set DATA_UPLOAD_MAX_MEMORY_SIZE to the max of relevant limits.
Inbound MTA validation
src/backend/core/api/viewsets/inbound/mta.py
Measure raw RFC822 payload size and return HTTP 413 if it exceeds MAX_INCOMING_EMAIL_SIZE (logged and returned before further processing).
Send API flow
src/backend/core/api/viewsets/send.py
Call prepare_outbound_message() directly and rely on ValidationError for size-related failures instead of checking a falsy return.
Draft creation/update validations
src/backend/core/mda/draft.py
Validate outgoing body size against MAX_OUTGOING_BODY_SIZE; enforce cumulative attachments total against MAX_OUTGOING_ATTACHMENT_SIZE; use localized ValidationError messages and unpack get_or_create results as _, _created.
Outbound MIME composition / signing
src/backend/core/mda/outbound.py
Compute body and total MIME content sizes (including attachment blobs); raise ValidationError for body/attachment limit breaches; raise DRF APIException on MIME composition failure; localized messages.
Tests: inbound & attachments
src/backend/core/tests/api/test_inbound_mta.py, src/backend/core/tests/api/test_attachments.py
Added tests for incoming size limits (413/200) and draft/attachment size behaviors (exceed, cumulative, within-limit, replace, send-time failures).
API / OpenAPI & config view
src/backend/core/api/openapi.json, src/backend/core/api/viewsets/config.py
Exposed MAX_OUTGOING_ATTACHMENT_SIZE and MAX_OUTGOING_BODY_SIZE in public config schema/response (required); added image-proxy endpoint declaration in OpenAPI.
Frontend config & UI
src/frontend/src/features/providers/config.tsx, src/frontend/src/features/forms/components/message-form/attachment-uploader.tsx, src/frontend/public/locales/common/en-US.json
Frontend default config includes MAX_OUTGOING_ATTACHMENT_SIZE and MAX_OUTGOING_BODY_SIZE; attachment uploader uses config-driven max and cumulative-size validation with toast errors; added localized attachment-size error string.
Attachment helper & tests
src/frontend/src/features/utils/attachment-helper/index.ts, .../index.test.ts
Switched to binary (1024) size formatting with updated tests and expectations for KB/MB formatting.

Sequence Diagram(s)

Incoming MTA size check

sequenceDiagram
    participant Client
    participant MTA as Inbound MTA Endpoint
    participant Auth
    participant Processor

    Client->>MTA: POST /inbound/mta/deliver (message/rfc822)
    MTA->>Auth: validate JWT
    MTA->>MTA: measure raw payload size
    alt payload size > MAX_INCOMING_EMAIL_SIZE
        MTA-->>Client: 413 Request Entity Too Large (JSON)
        Note right of MTA #f9f0c1: log size violation
    else payload size ≤ limit
        MTA->>Processor: continue delivery processing
        Processor-->>MTA: delivery result
        MTA-->>Client: 200 OK (delivered count)
    end
Loading

Outbound/draft size enforcement (high level)

sequenceDiagram
    participant API as Send/Draft API
    participant Settings
    participant DraftSvc as Draft service
    participant Outbound as Outbound composer/signer
    participant Tasks

    API->>DraftSvc: create/update draft with attachments/body
    DraftSvc->>Settings: read MAX_OUTGOING_BODY_SIZE, MAX_OUTGOING_ATTACHMENT_SIZE
    DraftSvc->>DraftSvc: compute body_size and attachments_total
    alt body_size > MAX_OUTGOING_BODY_SIZE or attachments_total > MAX_OUTGOING_ATTACHMENT_SIZE
        DraftSvc-->>API: 400 ValidationError (localized)
    else within limits
        DraftSvc-->>API: 200/201 OK
        API->>Outbound: prepare_outbound_message()
        Outbound->>Outbound: compose MIME + compute total_mime_size
        alt total_mime_size attachments/body exceed limits
            Outbound-->>API: ValidationError (size exceeded)
        else
            Outbound->>Tasks: dispatch send task
            Tasks-->>API: accepted
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Areas to focus: settings clamp correctness and ordering, consistent byte-unit handling (bytes vs displayed MB/KB), places returning 413 vs 400, OpenAPI schema vs runtime values, and message localization strings.

Possibly related PRs

Suggested reviewers

  • sylvinus

Poem

🐰 I count the bytes that hop and play,
I guard the gates of mail each day.
Too large a parcel gets declined,
Small ones pass with care aligned —
Rabbit hums: balanced mail, hooray!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Title Check ⚠️ Warning The title states "make DATA_UPLOAD_MAX_MEMORY_SIZE configurable via env var," which accurately describes one part of the changes made in this PR. However, the changeset encompasses significantly more than just configuring this single setting. The PR introduces a comprehensive email size limit framework, including new configuration fields (MAX_INCOMING_EMAIL_SIZE, MAX_OUTGOING_ATTACHMENT_SIZE, MAX_OUTGOING_BODY_SIZE), implements validation logic at multiple entry points (inbound delivery, draft creation, message sending), exposes these limits via the public API, and adds corresponding frontend support. While the title refers to a real aspect of the changes, it fails to capture the main objective of the PR, which is implementing configurable email size limits across the entire system. The PR objectives confirm this broader scope, discussing incoming/outgoing email constraints and validation strategies beyond just making a single setting configurable. Consider updating the title to reflect the primary focus of the PR. A more descriptive title might emphasize the core objective, such as "feat(backend): implement configurable email size limits for incoming and outgoing messages" or "feat: add email size validation and configurable limits across system." This would better align with the comprehensive nature of the changes and help teammates understand the PR's actual scope at a glance.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 93.75% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5ba002a and 24736f9.

📒 Files selected for processing (1)
  • src/frontend/src/features/utils/attachment-helper/index.test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/frontend/src/features/utils/attachment-helper/index.test.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: docker-publish-socks-proxy / docker-build-push
  • GitHub Check: check-api-state
  • GitHub Check: test-back
  • GitHub Check: lint-back

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/backend/messages/settings.py (1)

90-94: Document the purpose, expected values, and monitoring strategy for DATA_UPLOAD_MAX_MEMORY_SIZE.

Django's DATA_UPLOAD_MAX_MEMORY_SIZE limits the total request body size before raising RequestDataTooBig, and inbound MTA email delivery sends the entire email (including attachments) as the request body. The current default of 2.5 MB is appropriate only if emails consistently stay below that threshold.

The review comment appropriately flags that this setting affects all HTTP requests globally. Django documentation recommends measuring real request sizes first before increasing limits, then adjusting to 5–20 MB if needed.

To improve the implementation, consider:

  1. Add inline documentation explaining that this controls inbound email size (particularly with attachments)
  2. Recommend measuring actual email sizes before choosing a production value
  3. Note reverse proxy enforcement: Enforce upload size limits at the reverse proxy/load balancer level as well to prevent expensive processing in Django
  4. Add monitoring: Log and monitor RequestDataTooBig exceptions to detect when emails exceed the limit

Example improvement:

    # Upload limits
+   # Controls maximum request body size (form data + email ingest).
+   # Inbound emails are delivered as raw RFC822 in request.body; measure typical email sizes with attachments
+   # before adjusting. Enforce limits at reverse proxy/load balancer level as well.
     DATA_UPLOAD_MAX_MEMORY_SIZE = values.PositiveIntegerValue(
-        2621440, environ_name="DATA_UPLOAD_MAX_MEMORY_SIZE", environ_prefix=None
-    )  # Default 2.5MB, can be overridden via environment variable
+        2621440,  # 2.5 MB default; increase to 5–20 MB if emails with attachments exceed this
+        environ_name="DATA_UPLOAD_MAX_MEMORY_SIZE",
+        environ_prefix=None,
+    )
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6cf8ce3 and cf9cc5a.

📒 Files selected for processing (1)
  • src/backend/messages/settings.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
src/backend/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

src/backend/**/*.py: Follow Django/PEP 8 style with a 100-character line limit
Use descriptive, snake_case names for variables and functions
Use Django ORM for database access; avoid raw SQL unless necessary for performance
Use Django’s built-in user model and authentication framework
Prefer try-except blocks to handle exceptions in business logic and views
Log expected and unexpected actions with appropriate log levels
Capture and report exceptions to Sentry; use capture_exception() for custom errors
Do not log sensitive information (tokens, passwords, financial/health data, PII)

Files:

  • src/backend/messages/settings.py
src/backend/**/{settings.py,middleware.py}

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

Use middleware judiciously for cross-cutting concerns (authentication, logging, caching)

Files:

  • src/backend/messages/settings.py
src/backend/**/settings.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

src/backend/**/settings.py: Leverage Django’s caching framework (e.g., Redis/Memcached) where appropriate
Use Django’s cache framework with a backend like Redis or Memcached to reduce DB load
Optimize static file handling using Django’s staticfiles pipeline (e.g., WhiteNoise)

Files:

  • src/backend/messages/settings.py
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: docker-publish-socks-proxy / docker-build-push
  • GitHub Check: build-front
  • GitHub Check: check-api-state
  • GitHub Check: test-back
  • GitHub Check: lint-back

@sylvinus
Copy link
Member

sylvinus commented Nov 1, 2025

Good catch and I can confirm we have the issue on our instance too with medium-size incoming attachments. However I think it would be cleaner to have a single setting for the max attachment size (or 2, for incoming + outgoing?) and set that limit in accordance?

@mosa-riel
Copy link
Contributor Author

mosa-riel commented Nov 1, 2025

Yes,

I suggest we are going to use MAX_INCOMING_EMAIL_SIZE and MAX_OUTGOING_EMAIL_SIZE because I suppose we want to use the email body limited as well (gmail, for example, can do 50Mb email, and max 25mb attachment size).

On incoming mail, it is quite easy to get the MAX_INCOMING_EMAIL_SIZE and return 413 when the message is too large.

For outgoing mail, it is a UX question: I think it is best when creating a draft. But it is hard to determine the message body size (because it is not yet a raw email and updates very often when typing or pasting images). We can easy calculate the total attachment size when uploading, so when an attachment exceeds that total size, we can show a message.

When sending the final email, we can check the total size INCLUDING the body, but it can happen your email body is too large then. Do we want to check this also when drafting your email (harder to code) or is a message 'your total message is too large' when you try to send it is good enough?

Replace generic DATA_UPLOAD_MAX_MEMORY_SIZE with two semantic settings:
- MAX_INCOMING_EMAIL_SIZE: Validates total incoming email size (default 10MB)
- MAX_OUTGOING_EMAIL_SIZE: Validates total outgoing message size (default 10MB)

Validation points:
1. Incoming emails: Validates raw email size at MTA delivery (HTTP 413)
2. Draft attachments: Validates cumulative attachment size when adding files
3. Outbound send: Validates total MIME message size before sending

All user-facing errors are translatable using gettext_lazy.
Includes comprehensive tests for all validation scenarios.

Addresses review feedback from PR suitenumerique#394 to use semantic, unified settings
instead of exposing Django's low-level upload configuration.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
src/backend/core/mda/draft.py (1)

297-327: Validate attachment size correctly with proper exclusion logic.

The validation correctly excludes attachments being removed and includes only new attachments, using select_related("blob") to optimize database queries. The error message is clear and localized.

However, consider that this validation checks raw attachment sizes, but the final MIME message will be larger due to:

  • Base64 encoding overhead (~33% increase)
  • MIME headers, boundaries, and message body
  • DKIM signature

The validation in outbound.py (lines 201-221) catches oversized messages after composition, but users might benefit from a more conservative limit here (e.g., 80-90% of MAX_OUTGOING_EMAIL_SIZE) to provide earlier feedback during draft editing rather than at send time.

Consider applying a safety margin:

-                if total_size > settings.MAX_OUTGOING_EMAIL_SIZE:
+                # Reserve ~30% for MIME encoding overhead, headers, body, and signatures
+                attachment_limit = int(settings.MAX_OUTGOING_EMAIL_SIZE * 0.7)
+                if total_size > attachment_limit:
                     raise drf.exceptions.ValidationError(
                         {
                             "attachments": _(
-                                "Total attachment size (%(total_size)s bytes) exceeds maximum "
-                                "allowed size of %(max_size)s bytes. Current attachments: "
-                                "%(current_size)s bytes."
+                                "Total attachment size (%(total_size)s bytes) exceeds attachment "
+                                "limit of %(max_size)s bytes (reserved space for message encoding). "
+                                "Current attachments: %(current_size)s bytes."
                             )
                             % {
                                 "total_size": total_size,
-                                "max_size": settings.MAX_OUTGOING_EMAIL_SIZE,
+                                "max_size": attachment_limit,
                                 "current_size": current_total_size,
                             }
                         }
                     )
src/backend/core/mda/outbound.py (1)

156-161: Remove unused variable total_attachment_size.

The variable total_attachment_size is initialized and accumulated but never used. The actual size validation at line 202 correctly uses len(raw_mime_signed), which includes all attachments plus MIME encoding overhead.

Apply this diff to remove the unused variable:

     # Add attachments if present
     if message.attachments.exists():
         attachments = []
-        total_attachment_size = 0

         for attachment in message.attachments.select_related("blob").all():
             # Get the blob data
             blob = attachment.blob
-            total_attachment_size += blob.size

             # Add the attachment to the MIME data
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cf9cc5a and 5c6bece.

📒 Files selected for processing (7)
  • src/backend/core/api/viewsets/inbound/mta.py (1 hunks)
  • src/backend/core/api/viewsets/send.py (1 hunks)
  • src/backend/core/mda/draft.py (4 hunks)
  • src/backend/core/mda/outbound.py (4 hunks)
  • src/backend/core/tests/api/test_attachments.py (1 hunks)
  • src/backend/core/tests/api/test_inbound_mta.py (1 hunks)
  • src/backend/messages/settings.py (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/backend/messages/settings.py
🧰 Additional context used
📓 Path-based instructions (2)
src/backend/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

src/backend/**/*.py: Follow Django/PEP 8 style with a 100-character line limit
Use descriptive, snake_case names for variables and functions
Use Django ORM for database access; avoid raw SQL unless necessary for performance
Use Django’s built-in user model and authentication framework
Prefer try-except blocks to handle exceptions in business logic and views
Log expected and unexpected actions with appropriate log levels
Capture and report exceptions to Sentry; use capture_exception() for custom errors
Do not log sensitive information (tokens, passwords, financial/health data, PII)

Files:

  • src/backend/core/tests/api/test_inbound_mta.py
  • src/backend/core/api/viewsets/inbound/mta.py
  • src/backend/core/api/viewsets/send.py
  • src/backend/core/tests/api/test_attachments.py
  • src/backend/core/mda/outbound.py
  • src/backend/core/mda/draft.py
src/backend/**/{tests.py,tests/**/*.py}

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

src/backend/**/{tests.py,tests/**/*.py}: Use Django’s testing tools (pytest-django) to ensure code quality and reliability
Unit tests should focus on a single use case, keep assertions minimal, and cover all possible cases

Files:

  • src/backend/core/tests/api/test_inbound_mta.py
  • src/backend/core/tests/api/test_attachments.py
🧠 Learnings (2)
📚 Learning: 2025-09-02T10:12:12.835Z
Learnt from: CR
Repo: suitenumerique/messages PR: 0
File: .cursor/rules/django-python.mdc:0-0
Timestamp: 2025-09-02T10:12:12.835Z
Learning: Applies to src/backend/**/settings.py : Use Django’s cache framework with a backend like Redis or Memcached to reduce DB load

Applied to files:

  • src/backend/core/mda/outbound.py
📚 Learning: 2025-09-02T10:12:12.835Z
Learnt from: CR
Repo: suitenumerique/messages PR: 0
File: .cursor/rules/django-python.mdc:0-0
Timestamp: 2025-09-02T10:12:12.835Z
Learning: Applies to src/backend/**/settings.py : Leverage Django’s caching framework (e.g., Redis/Memcached) where appropriate

Applied to files:

  • src/backend/core/mda/outbound.py
🧬 Code graph analysis (5)
src/backend/core/tests/api/test_inbound_mta.py (2)
src/backend/core/tests/api/test_attachments.py (2)
  • api_client (25-30)
  • api_client (140-145)
src/backend/core/factories.py (1)
  • MailboxFactory (60-96)
src/backend/core/api/viewsets/send.py (1)
src/backend/core/mda/outbound.py (1)
  • prepare_outbound_message (45-256)
src/backend/core/tests/api/test_attachments.py (4)
src/backend/core/tests/api/test_labels.py (2)
  • api_client (34-38)
  • mailbox (26-30)
src/backend/core/models.py (4)
  • create_blob (548-574)
  • create_blob (1358-1422)
  • content_type (1562-1564)
  • Attachment (1523-1574)
src/backend/core/api/viewsets/send.py (1)
  • post (77-129)
src/backend/core/api/viewsets/draft.py (2)
  • post (197-231)
  • put (234-281)
src/backend/core/mda/outbound.py (3)
src/backend/core/tests/api/test_attachments.py (2)
  • attachment (169-173)
  • blob (160-166)
src/backend/core/factories.py (1)
  • blob (233-236)
src/backend/core/models.py (1)
  • size (1567-1569)
src/backend/core/mda/draft.py (1)
src/backend/core/models.py (3)
  • Contact (1126-1149)
  • size (1567-1569)
  • Attachment (1523-1574)
🪛 Ruff (0.14.2)
src/backend/core/tests/api/test_attachments.py

342-342: import should be at the top-level of a file

(PLC0415)


381-381: import should be at the top-level of a file

(PLC0415)


453-453: import should be at the top-level of a file

(PLC0415)


505-505: import should be at the top-level of a file

(PLC0415)


574-574: import should be at the top-level of a file

(PLC0415)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: docker-publish-socks-proxy / docker-build-push
  • GitHub Check: build-front
  • GitHub Check: check-api-state
  • GitHub Check: test-back
  • GitHub Check: lint-back
🔇 Additional comments (14)
src/backend/core/mda/draft.py (2)

7-9: LGTM! Clean import additions for settings and i18n.

These imports are properly used later in the validation logic.


53-53: LGTM! Consistent unpacking of get_or_create results.

The change from _ to _created improves code readability, even though the created flag isn't used downstream.

Also applies to: 200-200

src/backend/core/api/viewsets/inbound/mta.py (1)

142-155: LGTM! Early size validation with appropriate HTTP status.

The validation correctly rejects oversized incoming emails with HTTP 413 before any parsing occurs, which is efficient. The error message is clear and includes the size limit. The logging provides good debugging information with both actual and limit values.

src/backend/core/api/viewsets/send.py (1)

109-116: LGTM! Clean exception-driven flow for outbound preparation.

The change to rely on prepare_outbound_message raising ValidationError (instead of checking a return value) is more idiomatic for DRF. The comment clearly documents this behavior. Exceptions will be properly handled by DRF's exception handler and converted to appropriate HTTP responses.

src/backend/core/mda/outbound.py (3)

10-12: LGTM! Standard imports for i18n and DRF exceptions.

Both imports are properly utilized in the exception handling logic below.


187-189: LGTM! Proper exception handling for MIME composition failure.

The change to raise drf.exceptions.APIException instead of returning False is consistent with the new exception-driven flow. The error is logged for debugging and the user-facing message is clear and localized.


201-221: LGTM! Comprehensive size validation after DKIM signing.

The validation correctly checks the final message size after DKIM signing, which captures the true size including all MIME encoding overhead, attachments, headers, and the signature. The error message is clear, localized, and includes both actual and limit sizes.

Note that this validation uses {"message": ...} as the error key, while the draft validation (lines 314-327 in draft.py) uses {"attachments": ...}. This difference appears intentional: draft validation is field-specific, while this is a message-level validation at send time. Both patterns are valid in DRF.

src/backend/core/tests/api/test_attachments.py (5)

340-378: LGTM! Clear test for attachment size limit enforcement.

The test correctly validates that creating a draft with an attachment exceeding MAX_OUTGOING_EMAIL_SIZE returns a 400 error with the "attachments" key in the response.

Regarding the static analysis hint about import at the top level: importing override_settings inside the test method is a common pattern in Django tests for clarity and test isolation. This is acceptable despite the linter warning.


379-449: LGTM! Comprehensive test for cumulative size validation.

The test correctly validates that adding a second attachment that would push the total size over the limit (1 KB + 1.5 KB = 2.5 KB > 2 KB limit) is rejected with a 400 error. The test properly exercises the update flow by including both attachments in the PUT request.


451-499: LGTM! Good coverage of the success path.

The test validates that attachments within the size limit (8 KB < 10 KB) are accepted and correctly stored. This complements the error case tests and ensures the validation doesn't incorrectly reject valid requests.


501-568: LGTM! Important edge case for attachment replacement.

This test validates that the size validation correctly excludes attachments being removed when calculating the total. Replacing a 1.5 KB attachment with another 1.5 KB attachment should succeed even though each is close to the 2 KB limit. This aligns with the validation logic in draft.py (lines 300-303) that uses exclude(id__in=to_remove).


570-627: LGTM! Critical test for send-time size validation.

This test validates the defense-in-depth approach by bypassing draft validation and testing the send-time validation in outbound.py. This is important because:

  • Drafts might have been created before the limit was introduced
  • The limit might have been changed to a lower value
  • There could be bugs in draft validation

The test correctly expects a 400 error with the "message" key in the response, matching the validation in outbound.py (line 212).

src/backend/core/tests/api/test_inbound_mta.py (2)

950-978: LGTM! Validates inbound size limit enforcement.

The test correctly validates that incoming emails exceeding MAX_INCOMING_EMAIL_SIZE are rejected with HTTP 413 and an appropriate error message. The test properly uses a valid JWT token and correct Content-Type to isolate the size validation behavior.


980-1009: LGTM! Good coverage of the success path for inbound size validation.

The test validates that incoming emails within the size limit (5 KB < 10 KB) are accepted and delivered successfully. The test properly creates a mailbox to ensure delivery succeeds after passing size validation. This complements the error case test and ensures the validation doesn't incorrectly reject valid emails.

- Replace MAX_OUTGOING_EMAIL_SIZE with MAX_OUTGOING_ATTACHMENT_SIZE and MAX_OUTGOING_BODY_SIZE
- Add validation for body size (5MB default) and attachment size (20MB default) separately
- Update DATA_UPLOAD_MAX_MEMORY_SIZE calculation to accommodate both limits together
- Validate raw content sizes (before MIME encoding) to ignore implementation overhead
- Add body size validation in draft updates and send operations
- Expose both limits via config API for frontend
- Update frontend to use MAX_OUTGOING_ATTACHMENT_SIZE from config
- Add frontend cumulative attachment size validation
- Update error messages to show sizes in MB instead of bytes
- Update tests to use new MAX_OUTGOING_ATTACHMENT_SIZE setting

Validation points:
- Draft: Body size and cumulative attachment size
- Send: Body size and cumulative attachment size (before MIME encoding)
- Incoming: Total email size

Resolves feedback on PR suitenumerique#394
…ions

- Replace Field error state with toast.error() for cleaner UX
- Use AttachmentHelper.getFormattedSize() for consistent size formatting
- Keep attachment area clean (no red state) when files exceed limit
- Add translation string for attachment size error message
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/backend/core/mda/outbound.py (1)

184-247: Critical breaking change: selfcheck.py will crash when exceptions are raised instead of returning False.

The function signature still declares -> bool, but the implementation now raises APIException instead of returning False. This breaks the existing calling pattern in src/backend/core/mda/selfcheck.py:77, which expects a boolean return:

if not prepare_outbound_message(from_mailbox, message, text_body, html_body):
    raise SelfCheckError("Failed to prepare outbound message")

This code is not in a DRF view context and has no exception handler. When an APIException is raised, it will propagate unhandled and crash the selfcheck flow.

While src/backend/core/api/viewsets/send.py:110 is prepared to handle exceptions (as indicated by the comment), selfcheck.py is not. The function cannot have two conflicting behaviors.

Fix required: Either (1) keep the -> bool return pattern across all callers, or (2) update selfcheck.py:77 to catch the exceptions instead of checking return values, and update the function signature to reflect the change.

🧹 Nitpick comments (2)
src/backend/core/api/openapi.json (2)

201-210: Document scope and add basic constraints for new size settings.

Clarify whether MAX_OUTGOING_ATTACHMENT_SIZE is per‑attachment or total attachments, and ensure both have a non‑negative constraint.

Apply minimal schema polish:

 "MAX_OUTGOING_ATTACHMENT_SIZE": {
-  "type": "integer",
-  "description": "Maximum size in bytes for outgoing email attachments",
+  "type": "integer",
+  "minimum": 0,
+  "description": "Maximum size in bytes for outgoing email attachments (bytes, PER ATTACHMENT or TOTAL — clarify)",
   "readOnly": true
 },
 "MAX_OUTGOING_BODY_SIZE": {
-  "type": "integer",
-  "description": "Maximum size in bytes for outgoing email body (text + HTML)",
+  "type": "integer",
+  "minimum": 0,
+  "description": "Maximum size in bytes for outgoing email body (text + HTML, bytes)",
   "readOnly": true
 }

2399-2449: Image proxy endpoint: add formats and binary response schema; note SSRF considerations.

  • mailbox_id should be UUID formatted; url should use URI format.
  • Define 200 response content as image binary (image/*).
  • Optionally document allowed URL schemes (http/https) and max fetched size trigger for 413.

Apply schema refinements:

         {
           "in": "path",
           "name": "mailbox_id",
           "schema": {
-            "type": "string"
+            "type": "string",
+            "format": "uuid"
           },
           "description": "ID of the mailbox",
           "required": true
         },
         {
           "in": "query",
           "name": "url",
           "schema": {
-            "type": "string"
+            "type": "string",
+            "format": "uri"
           },
-          "description": "The external image URL to proxy",
+          "description": "The external image URL to proxy (http/https)",
           "required": true
         }
...
-        "responses": {
-          "200": {
-            "description": "Image content"
-          },
+        "responses": {
+          "200": {
+            "description": "Image content",
+            "content": {
+              "image/*": {
+                "schema": { "type": "string", "format": "binary" }
+              }
+            }
+          },
           "400": { "description": "Invalid request" },
           "403": { "description": "Forbidden" },
           "413": { "description": "Image too large" },
           "502": { "description": "Failed to fetch external image" }
         }

Please also confirm the implementation enforces:

  • Allowed schemes (http/https only), size/time limits, blocked private IP ranges (SSRF), and DNS re‑resolution on connect. If desired, I can add a short “Security notes” paragraph to the endpoint description.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5c6bece and 2d5bba5.

⛔ Files ignored due to path filters (5)
  • src/frontend/package-lock.json is excluded by !**/package-lock.json
  • src/frontend/src/features/api/gen/mailboxes/mailboxes.ts is excluded by !**/gen/**
  • src/frontend/src/features/api/gen/models/config_retrieve200.ts is excluded by !**/gen/**
  • src/frontend/src/features/api/gen/models/index.ts is excluded by !**/gen/**
  • src/frontend/src/features/api/gen/models/mailboxes_image_proxy_list_params.ts is excluded by !**/gen/**
📒 Files selected for processing (9)
  • src/backend/core/api/openapi.json (3 hunks)
  • src/backend/core/api/viewsets/config.py (3 hunks)
  • src/backend/core/mda/draft.py (5 hunks)
  • src/backend/core/mda/outbound.py (4 hunks)
  • src/backend/core/tests/api/test_attachments.py (1 hunks)
  • src/backend/messages/settings.py (2 hunks)
  • src/frontend/public/locales/common/en-US.json (1 hunks)
  • src/frontend/src/features/forms/components/message-form/attachment-uploader.tsx (4 hunks)
  • src/frontend/src/features/providers/config.tsx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
src/backend/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

src/backend/**/*.py: Follow Django/PEP 8 style with a 100-character line limit
Use descriptive, snake_case names for variables and functions
Use Django ORM for database access; avoid raw SQL unless necessary for performance
Use Django’s built-in user model and authentication framework
Prefer try-except blocks to handle exceptions in business logic and views
Log expected and unexpected actions with appropriate log levels
Capture and report exceptions to Sentry; use capture_exception() for custom errors
Do not log sensitive information (tokens, passwords, financial/health data, PII)

Files:

  • src/backend/core/api/viewsets/config.py
  • src/backend/core/mda/draft.py
  • src/backend/messages/settings.py
  • src/backend/core/mda/outbound.py
  • src/backend/core/tests/api/test_attachments.py
src/backend/**/{settings.py,middleware.py}

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

Use middleware judiciously for cross-cutting concerns (authentication, logging, caching)

Files:

  • src/backend/messages/settings.py
src/backend/**/settings.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

src/backend/**/settings.py: Leverage Django’s caching framework (e.g., Redis/Memcached) where appropriate
Use Django’s cache framework with a backend like Redis or Memcached to reduce DB load
Optimize static file handling using Django’s staticfiles pipeline (e.g., WhiteNoise)

Files:

  • src/backend/messages/settings.py
src/backend/**/{tests.py,tests/**/*.py}

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

src/backend/**/{tests.py,tests/**/*.py}: Use Django’s testing tools (pytest-django) to ensure code quality and reliability
Unit tests should focus on a single use case, keep assertions minimal, and cover all possible cases

Files:

  • src/backend/core/tests/api/test_attachments.py
🧠 Learnings (3)
📚 Learning: 2025-09-02T12:20:22.006Z
Learnt from: Nastaliss
Repo: suitenumerique/messages PR: 313
File: src/frontend/src/features/layouts/components/admin/modal-create-domain/index.tsx:32-37
Timestamp: 2025-09-02T12:20:22.006Z
Learning: In the messages application, `SCHEMA_CUSTOM_ATTRIBUTES_MAILDOMAIN` from the config defaults to an empty object `{}` rather than being potentially undefined, so null checks are not needed when using this configuration value.

Applied to files:

  • src/backend/core/api/openapi.json
  • src/frontend/src/features/providers/config.tsx
📚 Learning: 2025-09-02T10:12:12.835Z
Learnt from: CR
Repo: suitenumerique/messages PR: 0
File: .cursor/rules/django-python.mdc:0-0
Timestamp: 2025-09-02T10:12:12.835Z
Learning: Applies to src/backend/**/settings.py : Use Django’s cache framework with a backend like Redis or Memcached to reduce DB load

Applied to files:

  • src/backend/core/mda/outbound.py
📚 Learning: 2025-09-02T10:12:12.835Z
Learnt from: CR
Repo: suitenumerique/messages PR: 0
File: .cursor/rules/django-python.mdc:0-0
Timestamp: 2025-09-02T10:12:12.835Z
Learning: Applies to src/backend/**/settings.py : Leverage Django’s caching framework (e.g., Redis/Memcached) where appropriate

Applied to files:

  • src/backend/core/mda/outbound.py
🧬 Code graph analysis (4)
src/frontend/src/features/forms/components/message-form/attachment-uploader.tsx (4)
src/frontend/src/features/providers/config.tsx (1)
  • useConfig (50-56)
src/frontend/src/features/forms/components/message-form/drive-attachment-picker.tsx (1)
  • DriveFile (11-11)
src/frontend/src/hooks/use-debounce-callback.ts (1)
  • useDebounceCallback (7-23)
src/frontend/src/features/utils/attachment-helper/index.ts (1)
  • AttachmentHelper (11-84)
src/backend/core/mda/draft.py (3)
src/backend/core/models.py (5)
  • Contact (1126-1149)
  • create_blob (548-574)
  • create_blob (1358-1422)
  • size (1567-1569)
  • Attachment (1523-1574)
src/backend/core/tests/api/test_attachments.py (1)
  • blob (160-166)
src/backend/core/factories.py (1)
  • blob (233-236)
src/backend/core/mda/outbound.py (2)
src/backend/core/models.py (3)
  • text_body (1802-1810)
  • html_body (1791-1799)
  • size (1567-1569)
src/backend/core/factories.py (1)
  • blob (233-236)
src/backend/core/tests/api/test_attachments.py (5)
src/backend/core/tests/api/test_labels.py (2)
  • api_client (34-38)
  • mailbox (26-30)
src/backend/core/factories.py (5)
  • blob (233-236)
  • ThreadFactory (125-132)
  • ThreadAccessFactory (135-145)
  • ContactFactory (148-157)
  • MessageFactory (160-186)
src/backend/core/models.py (4)
  • create_blob (548-574)
  • create_blob (1358-1422)
  • content_type (1562-1564)
  • Attachment (1523-1574)
src/backend/core/api/viewsets/send.py (1)
  • post (77-129)
src/backend/core/api/viewsets/draft.py (2)
  • post (197-231)
  • put (234-281)
🪛 Ruff (0.14.2)
src/backend/core/tests/api/test_attachments.py

342-342: import should be at the top-level of a file

(PLC0415)


381-381: import should be at the top-level of a file

(PLC0415)


453-453: import should be at the top-level of a file

(PLC0415)


505-505: import should be at the top-level of a file

(PLC0415)


574-574: import should be at the top-level of a file

(PLC0415)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test-back
🔇 Additional comments (21)
src/frontend/public/locales/common/en-US.json (1)

55-55: LGTM!

The translation string is well-structured with clear placeholder names that align with the frontend usage in attachment-uploader.tsx. The message provides comprehensive information about the size limit violation.

src/backend/core/mda/outbound.py (2)

10-12: LGTM!

The new imports support localization and DRF exception handling, which are necessary for the size validation features.


153-182: Body size validation approach is sound.

The validation correctly calculates raw UTF-8 byte size before MIME encoding, which is consistent with the intent to limit email body size. The error message is localized and provides clear feedback with sizes in MB.

src/frontend/src/features/providers/config.tsx (1)

14-15: LGTM!

The default values align with the backend settings (20MB for attachments, 5MB for body). These defaults provide graceful degradation if the backend config endpoint fails or hasn't loaded yet.

src/backend/core/mda/draft.py (4)

7-9: LGTM!

The new imports support the size validation and localization features.


53-53: Better variable naming.

Changing the unpacking from _ to _created improves code clarity by explicitly showing that the second value is the boolean "created" flag from get_or_create().

Also applies to: 200-200


225-243: Body size validation is correctly implemented.

The validation occurs before blob creation, uses consistent UTF-8 encoding for size calculation, and provides clear localized error messages.


317-354: Comprehensive attachment size validation.

The validation logic correctly handles the attachment replacement scenario by excluding attachments being removed from the current total. The error message provides all relevant size information (current, new, total, limit) in a user-friendly format.

src/frontend/src/features/forms/components/message-form/attachment-uploader.tsx (3)

5-5: LGTM!

The component correctly uses the config provider to retrieve the dynamic size limit, replacing the previous hardcoded value.

Also applies to: 17-17, 33-34


41-60: Client-side validation improves UX.

The cumulative size validation prevents uploads before they occur, providing immediate feedback to users. The error message includes all relevant size information formatted using the localized translation string.


151-157: Simplified field state management.

The component now shows a consistent informational message about the size limit instead of conditional error states, providing clearer guidance to users.

src/backend/core/api/viewsets/config.py (2)

65-74: OpenAPI schema properly updated.

The schema correctly documents the new size limit fields as read-only integers with clear descriptions and marks them as required in the response.

Also applies to: 85-86


115-117: LGTM!

The response handler correctly exposes the size limit settings from the Django configuration.

src/backend/core/tests/api/test_attachments.py (6)

340-378: Test coverage for single large attachment.

The test correctly verifies that a single oversized attachment is rejected during draft creation with a 400 error containing attachments in the response.


379-449: Test coverage for cumulative size limit.

The test validates that adding multiple attachments respects the cumulative size limit by first creating a draft with one attachment, then attempting to add another that would exceed the total limit.


451-499: Test coverage for valid attachments.

The test confirms that multiple attachments within the limit are accepted successfully.


501-568: Test coverage for attachment replacement.

The test verifies that removing an attachment allows adding a new one within the limit, confirming the replacement logic works correctly.


570-627: Test coverage for send-time validation.

The test confirms that size limits are enforced during the send operation, not just during draft creation, by manually adding an oversized attachment and attempting to send.


342-342: Static analysis hint can be safely ignored.

The import statements inside test methods are intentional and necessary when using override_settings as a context manager. Moving these imports to the top level would break the test isolation.

Also applies to: 381-381, 453-453, 505-505, 574-574

src/backend/messages/settings.py (1)

90-112: Well-structured size limit settings.

The new settings provide comprehensive control over email size limits with reasonable defaults:

  • DATA_UPLOAD_MAX_MEMORY_SIZE: 2.5 MB (Django default)
  • MAX_INCOMING_EMAIL_SIZE: 10 MB
  • MAX_OUTGOING_ATTACHMENT_SIZE: 20 MB
  • MAX_OUTGOING_BODY_SIZE: 5 MB

All settings are configurable via environment variables, addressing the core issue described in the PR objectives.

src/backend/core/api/openapi.json (1)

220-223: Review comment is incorrect; fields are already properly integrated.

The backend already defines MAX_OUTGOING_ATTACHMENT_SIZE and MAX_OUTGOING_BODY_SIZE with defaults (20MB and 5MB respectively), and the config endpoint explicitly returns these fields. These are not new undeployed fields—they're active parts of the system used for email validation. Marking them as required poses no deployment risk; the proposed diff should be ignored.

Likely an incorrect or invalid review comment.

Comment on lines +772 to +779
# Ensure Django's upload limit accommodates the larger of the email size limits
# For outgoing, we need to accommodate both body + attachments together
max_outgoing_total = self.MAX_OUTGOING_ATTACHMENT_SIZE + self.MAX_OUTGOING_BODY_SIZE
self.DATA_UPLOAD_MAX_MEMORY_SIZE = max(
self.DATA_UPLOAD_MAX_MEMORY_SIZE,
self.MAX_INCOMING_EMAIL_SIZE,
max_outgoing_total,
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify DATA_UPLOAD_MAX_MEMORY_SIZE computation logic.

The computation sets Django's upload limit to the maximum of:

  1. Configured DATA_UPLOAD_MAX_MEMORY_SIZE
  2. MAX_INCOMING_EMAIL_SIZE
  3. MAX_OUTGOING_ATTACHMENT_SIZE + MAX_OUTGOING_BODY_SIZE

This ensures Django accepts the largest possible upload scenario. However, consider whether the outgoing total (body + attachments) is the right comparison, since:

  • Incoming emails come as a single payload
  • Outgoing drafts may be updated incrementally (body, then attachments)

The logic appears sound for the MTA intake use case (where the entire email arrives as one payload), but verify it doesn't inadvertently allow uploads larger than intended in other scenarios.


🏁 Script executed:

#!/bin/bash
# Description: Check how DATA_UPLOAD_MAX_MEMORY_SIZE is used in request handling

# Find references to DATA_UPLOAD_MAX_MEMORY_SIZE
rg -n 'DATA_UPLOAD_MAX_MEMORY_SIZE' --type=py -A3 -B3

# Find request parsing and upload handling
rg -n 'RequestDataTooBig|request\.body|request\.FILES' --type=py -C2

Length of output: 4530



---

🏁 Script executed:

```shell
#!/bin/bash
# Find outgoing email/draft creation and handling
rg -n 'MAX_OUTGOING_BODY_SIZE|MAX_OUTGOING_ATTACHMENT_SIZE' --type=py -B5 -A5

# Find endpoints/views for creating/uploading outgoing emails
rg -n 'outgoing|draft|compose' --type=py -i | grep -i 'def \|class ' | head -20

# Check if multipart/form-data is used for draft/outgoing handling
rg -n 'MultiPartParser|FormParser|multipart' --type=py -B2 -A2

Length of output: 76111


🏁 Script executed:

#!/bin/bash
# Same as above but runs without repository
echo "Need repository context - cannot skip cloning for this search"

Length of output: 61


🏁 Script executed:

#!/bin/bash
# Find DraftMessageView create/update implementations
rg -n 'class DraftMessageView' --type=py -A30

# Check how draft requests are structured - look for serializer fields
rg -n 'class.*DraftSerializer\|class.*MessageSerializer' --type=py -A10 | head -60

# Find what parsers are used by draft endpoints
rg -n 'parser_classes.*=|DraftMessageView|draft.*endpoint' --type=py -B2 -A2 | grep -A5 -B5 'parser_classes'

Length of output: 4184


Reconsider the computation of outgoing body + attachments sum.

The code sets DATA_UPLOAD_MAX_MEMORY_SIZE = max(current, MAX_INCOMING_EMAIL_SIZE, MAX_OUTGOING_ATTACHMENT_SIZE + MAX_OUTGOING_BODY_SIZE), but the architecture never sends body and attachments together in a single HTTP request:

  • Draft creation/update: draftBody comes as JSON text + attachmentIds are UUID references (not binary attachment data).
  • Blob uploads: Handled separately via the blob endpoint with MultiPartParser.
  • Inbound MTA: Full RFC822 in request.body (single payload, already covered by MAX_INCOMING_EMAIL_SIZE).

Since outgoing email body and attachments are uploaded separately (body in draft JSON, attachments pre-uploaded as blobs), the sum is unnecessary. Consider using max(MAX_OUTGOING_BODY_SIZE, MAX_OUTGOING_ATTACHMENT_SIZE) instead, or remove the outgoing sum entirely if MAX_INCOMING_EMAIL_SIZE already covers all scenarios.

🤖 Prompt for AI Agents
In src/backend/messages/settings.py around lines 772 to 779, the code sums
MAX_OUTGOING_ATTACHMENT_SIZE + MAX_OUTGOING_BODY_SIZE into
DATA_UPLOAD_MAX_MEMORY_SIZE even though body and attachments are never uploaded
together; change the logic to not add them together — either use
max(MAX_OUTGOING_BODY_SIZE, MAX_OUTGOING_ATTACHMENT_SIZE) or drop the outgoing
sizes entirely if MAX_INCOMING_EMAIL_SIZE already covers all cases — update the
DATA_UPLOAD_MAX_MEMORY_SIZE assignment accordingly to use the chosen alternative
and remove the unnecessary sum.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/frontend/src/features/forms/components/message-form/attachment-uploader.tsx (1)

144-146: Enforce the limit for drive attachments too.

Drive picks skip the new total-size validation, so users can exceed MAX_OUTGOING_ATTACHMENT_SIZE by pulling files from Drive. Mirror the drop handler’s guard (with the same toast) before calling appendToAttachments.

Apply this diff:

-    const handleDriveAttachmentPick = (attachments: DriveFile[]) => {
-        appendToAttachments(attachments);
-    }
+    const handleDriveAttachmentPick = (newAttachments: DriveFile[]) => {
+        const newAttachmentsSize = newAttachments.reduce((acc, attachment) => acc + attachment.size, 0);
+        const newTotalSize = currentTotalSize + newAttachmentsSize;
+
+        if (newTotalSize > MAX_ATTACHMENT_SIZE) {
+            toast.error(
+                t("Cannot add attachment(s) ({{newSize}}). Total would be {{totalSize}}, exceeding the {{maxSize}} limit. Current attachments: {{currentSize}}.", {
+                    newSize: AttachmentHelper.getFormattedSize(newAttachmentsSize, i18n.language),
+                    totalSize: AttachmentHelper.getFormattedSize(newTotalSize, i18n.language),
+                    maxSize: AttachmentHelper.getFormattedSize(MAX_ATTACHMENT_SIZE, i18n.language),
+                    currentSize: AttachmentHelper.getFormattedSize(currentTotalSize, i18n.language),
+                })
+            );
+            return;
+        }
+
+        appendToAttachments(newAttachments);
+    }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2d5bba5 and a27c317.

📒 Files selected for processing (1)
  • src/frontend/src/features/forms/components/message-form/attachment-uploader.tsx (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/frontend/src/features/forms/components/message-form/attachment-uploader.tsx (4)
src/frontend/src/features/providers/config.tsx (1)
  • useConfig (50-56)
src/frontend/src/features/forms/components/message-form/drive-attachment-picker.tsx (1)
  • DriveFile (11-11)
src/frontend/src/hooks/use-debounce-callback.ts (1)
  • useDebounceCallback (7-23)
src/frontend/src/features/utils/attachment-helper/index.ts (1)
  • AttachmentHelper (11-84)

Comment on lines 41 to 60
// Calculate current total size of attachments
const currentTotalSize = attachments.reduce((acc, attachment) => acc + attachment.size, 0);

const { getRootProps, getInputProps, isDragActive, fileRejections } = useDropzone({
onDrop: async (acceptedFiles) => {
// Check cumulative size before uploading
const newFilesSize = acceptedFiles.reduce((acc, file) => acc + file.size, 0);
const totalSize = currentTotalSize + newFilesSize;

if (totalSize > MAX_ATTACHMENT_SIZE) {
toast.error(
t("Cannot add attachment(s) ({{newSize}}). Total would be {{totalSize}}, exceeding the {{maxSize}} limit. Current attachments: {{currentSize}}.", {
newSize: AttachmentHelper.getFormattedSize(newFilesSize, i18n.language),
totalSize: AttachmentHelper.getFormattedSize(totalSize, i18n.language),
maxSize: AttachmentHelper.getFormattedSize(MAX_ATTACHMENT_SIZE, i18n.language),
currentSize: AttachmentHelper.getFormattedSize(currentTotalSize, i18n.language)
})
);
return;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Include pending uploads in the size guard.

The cumulative limit only sums attachments, so users can enqueue multiple batches while uploads are pending and blow past MAX_OUTGOING_ATTACHMENT_SIZE once everything finishes uploading. Please fold the uploadingQueue sizes into the running total before accepting more files.

Apply this diff:

-    const currentTotalSize = attachments.reduce((acc, attachment) => acc + attachment.size, 0);
+    const attachmentsSize = attachments.reduce((acc, attachment) => acc + attachment.size, 0);
+    const uploadingQueueSize = uploadingQueue.reduce((acc, file) => acc + file.size, 0);
+    const currentTotalSize = attachmentsSize + uploadingQueueSize;

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
src/frontend/src/features/forms/components/message-form/attachment-uploader.tsx
around lines 41 to 60, the size check only sums existing attachments and omits
pending uploads, allowing users to enqueue more files and exceed
MAX_ATTACHMENT_SIZE; update the total-size calculation to include sizes of files
in uploadingQueue (sum uploadingQueue.map(q=>q.file.size) or equivalent) when
computing totalSize and currentTotalSize used in the guard, then use that
combined total in the conditional and in the error message formatting so pending
uploads are counted before accepting additional files.

- Replace Intl.NumberFormat with binary (MiB) formatter for consistent sizes
- Shows 5MB, 10MB, 20MB instead of 5.2MB, 10.5MB, 20.97MB
- Fixes locale-specific 'mln. byte' text issue
- Use ToasterItem instead of toast.error for attachment size errors
- Matches file system conventions users expect
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/frontend/src/features/forms/components/message-form/attachment-uploader.tsx (1)

41-62: Count in-flight uploads in the size guard.

currentTotalSize ignores uploadingQueue, so users can stage multiple batches while earlier files are uploading and push the eventual total past MAX_OUTGOING_ATTACHMENT_SIZE. The toast also under-reports the current total in that situation.

-    const currentTotalSize = attachments.reduce((acc, attachment) => acc + attachment.size, 0);
+    const attachmentsSize = attachments.reduce((acc, attachment) => acc + attachment.size, 0);
+    const uploadingQueueSize = uploadingQueue.reduce((acc, file) => acc + file.size, 0);
+    const currentTotalSize = attachmentsSize + uploadingQueueSize;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a27c317 and 5ba002a.

📒 Files selected for processing (3)
  • src/frontend/src/features/forms/components/message-form/attachment-uploader.tsx (4 hunks)
  • src/frontend/src/features/utils/attachment-helper/index.test.ts (1 hunks)
  • src/frontend/src/features/utils/attachment-helper/index.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/frontend/src/features/utils/attachment-helper/index.ts (1)
src/backend/core/models.py (1)
  • size (1567-1569)
src/frontend/src/features/utils/attachment-helper/index.test.ts (4)
src/frontend/src/features/utils/attachment-helper/index.ts (1)
  • AttachmentHelper (11-91)
src/frontend/src/features/api/utils.ts (1)
  • getRequestUrl (39-50)
src/frontend/src/features/api/gen/blob/blob.ts (1)
  • getBlobDownloadRetrieveUrl (49-51)
src/frontend/src/features/forms/components/message-form/drive-attachment-picker.tsx (1)
  • DriveFile (11-11)
src/frontend/src/features/forms/components/message-form/attachment-uploader.tsx (6)
src/frontend/src/features/providers/config.tsx (1)
  • useConfig (50-56)
src/frontend/src/features/forms/components/message-form/drive-attachment-picker.tsx (1)
  • DriveFile (11-11)
src/frontend/src/features/api/gen/blob/blob.ts (1)
  • useBlobUploadCreate (304-324)
src/frontend/src/hooks/use-debounce-callback.ts (1)
  • useDebounceCallback (7-23)
src/frontend/src/features/ui/components/toaster/index.tsx (2)
  • addToast (63-76)
  • ToasterItem (16-61)
src/frontend/src/features/utils/attachment-helper/index.ts (1)
  • AttachmentHelper (11-91)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: docker-publish-socks-proxy / docker-build-push
  • GitHub Check: check-api-state
  • GitHub Check: test-back
  • GitHub Check: lint-back

Comment on lines +70 to +84
// Use binary (1024) conversion for user-friendly display
// This matches file system conventions and gives clean numbers
const MB = 1024 * 1024;
const KB = 1024;

return formatter.format(size);
if (size >= MB) {
const mb = size / MB;
// Show whole number if it's exact, otherwise 1 decimal
return mb % 1 === 0 ? `${mb} MB` : `${mb.toFixed(1)} MB`;
} else if (size >= KB) {
const kb = size / KB;
return kb % 1 === 0 ? `${kb} KB` : `${kb.toFixed(1)} KB`;
} else {
return `${size} B`;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Restore locale-aware size formatting.

By dropping the Intl formatter we now ignore the language argument and always emit English-style decimals (e.g. 1.5 MB in French instead of 1,5 MB). That is a regression for localized UIs. Please keep the binary threshold logic but format the numeric part through Intl.NumberFormat so separators and decimal symbols follow the active locale.

     static getFormattedSize(size: number, language: string = 'en') {
-        const MB = 1024 * 1024;
-        const KB = 1024;
-
-        if (size >= MB) {
-            const mb = size / MB;
-            return mb % 1 === 0 ? `${mb} MB` : `${mb.toFixed(1)} MB`;
-        } else if (size >= KB) {
-            const kb = size / KB;
-            return kb % 1 === 0 ? `${kb} KB` : `${kb.toFixed(1)} KB`;
-        } else {
-            return `${size} B`;
-        }
+        const MB = 1024 * 1024;
+        const KB = 1024;
+        const formatNumber = (value: number) => {
+            return new Intl.NumberFormat(language, {
+                minimumFractionDigits: Number.isInteger(value) ? 0 : 1,
+                maximumFractionDigits: 1,
+            }).format(Number.isInteger(value) ? value : Number(value.toFixed(1)));
+        };
+
+        if (size >= MB) {
+            return `${formatNumber(size / MB)} MB`;
+        }
+        if (size >= KB) {
+            return `${formatNumber(size / KB)} KB`;
+        }
+        return `${formatNumber(size)} B`;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Use binary (1024) conversion for user-friendly display
// This matches file system conventions and gives clean numbers
const MB = 1024 * 1024;
const KB = 1024;
return formatter.format(size);
if (size >= MB) {
const mb = size / MB;
// Show whole number if it's exact, otherwise 1 decimal
return mb % 1 === 0 ? `${mb} MB` : `${mb.toFixed(1)} MB`;
} else if (size >= KB) {
const kb = size / KB;
return kb % 1 === 0 ? `${kb} KB` : `${kb.toFixed(1)} KB`;
} else {
return `${size} B`;
}
// Use binary (1024) conversion for user-friendly display
// This matches file system conventions and gives clean numbers
const MB = 1024 * 1024;
const KB = 1024;
const formatNumber = (value: number) => {
return new Intl.NumberFormat(language, {
minimumFractionDigits: Number.isInteger(value) ? 0 : 1,
maximumFractionDigits: 1,
}).format(Number.isInteger(value) ? value : Number(value.toFixed(1)));
};
if (size >= MB) {
return `${formatNumber(size / MB)} MB`;
}
if (size >= KB) {
return `${formatNumber(size / KB)} KB`;
}
return `${formatNumber(size)} B`;
🤖 Prompt for AI Agents
In src/frontend/src/features/utils/attachment-helper/index.ts around lines 70 to
84, restore locale-aware numeric formatting: keep the binary thresholds (KB =
1024, MB = 1024*1024) and compute mb/kb as before, but pass the numeric part
through Intl.NumberFormat using the existing language argument (or navigator
language fallback) so decimal separators and grouping follow the active locale;
format whole numbers with no fraction digits and non-whole values with one
decimal place (e.g. use Intl.NumberFormat(language, { maximumFractionDigits: 1,
minimumFractionDigits: 0 }) and conditionally call with 0 or 1 fraction digits
or use Number.isInteger to decide), then inject the formatted string into the
unit (e.g. "… MB", "… KB", "… B").

@mosa-riel
Copy link
Contributor Author

WIP ;)

Long story somewhat short:

init

  • attachment limits set on the backend .env
  • backend communicates these to frontend (/api/v1.0/config)

frontend

  • when uploading attachment frontend calculates side to prevent uploading the file to server
  • when sending draft updates, backend checks message body size (works, pasted 5+ mb of text) and if exceeds, sends error and message to reduce body size

backend

  • server also checks message and attechment size on send (for future API purposes or someone circumvents frontend)
  • size communication is in MB (bytes / 1024 / 1024) because the shown size did not always correspond with the size my filesystem showed. The Intl comment of Coderabbit I understand.

@sylvinus
Copy link
Member

sylvinus commented Nov 3, 2025

I'm wondering if the draft updates won't be larger (due to JSON serialization and the blocknote data format) than the final body size ? If so we could add a 1.x factor for DATA_UPLOAD_MAX_MEMORY_SIZE. BTW the comment from coderabbit saying that including attachment sizes in the max is useless seems relevant ?

@mosa-riel
Copy link
Contributor Author

mosa-riel commented Nov 3, 2025

Ah, true. I was already in the users' context where it should be clear that the max 10Mb, that the user actually can use 10Mb (and not be limited tot 7Mb due to this overhead).

But wé should take care for specifically this DATA_UPLOAD_MAX_MEMORY_SIZE to have indeed a factor above it, you are right. So if we communicate (and set in .env) 10Mb, the data_upload_max should be a factor 1.x. The server size check itself is not adding this overhead (I already tested this by pasting 5Mb of body text on a 5Mb limit).

About coderabbits comment that is true - I was under the impression that we send ONE upload that has to deal with that DATA_UPLOAD_MAX_MEMORY_SIZE, so adding them is useless. It should be the max of the 3, max body, max incoming, max outgoing, correct.

Also, I chose to have only the popup on the bottom-left for UI messages. We now have 3 options: below the attachment upload (small text), as a toast popup at the top right, or messages you get bottom-left (e.g. server errors).

If we attach an attachment too big, where would we want to show the message? For me bottom-left seems the 'standard'?

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.

3 participants