Skip to content

✨(channels) encrypt channel settings + add new provisioning routes#599

Open
sylvinus wants to merge 9 commits intomainfrom
encrypt_channels
Open

✨(channels) encrypt channel settings + add new provisioning routes#599
sylvinus wants to merge 9 commits intomainfrom
encrypt_channels

Conversation

@sylvinus
Copy link
Copy Markdown
Member

@sylvinus sylvinus commented Mar 31, 2026

Summary by CodeRabbit

  • New Features

    • Raw RFC‑822 email submission endpoint (POST .../submit/) with async delivery and acceptance response
    • Provisioning lookup endpoints for mailboxes and users with role labels
    • Channel encrypted settings storage and optional channel creator association; channel creation now returns any generated password in responses
    • Message responses include a mime_id field
  • Bug Fixes

    • More accurate inbound delivery success detection
  • Configuration

    • Added CALENDARS_API_KEY and header-based service auth for provisioning/calendar APIs
  • Tests & Migrations

    • Expanded tests for submit, provisioning, and channel encrypted settings; DB migration to add encrypted settings and channel user field

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 31, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds header-based Calendars API key auth and a raw RFC‑822 submit endpoint; adds two provisioning lookup endpoints; introduces encrypted per-channel settings and channel user FK with migration; refactors outbound MIME signing/size validation; adjusts inbound/outbound creation logic; tightens signal guards; expands tests.

Changes

Cohort / File(s) Summary
Permissions & Settings
src/backend/core/api/permissions.py, src/backend/messages/settings.py
Added HasCalendarsApiKey permission (constant-time X-Service-Auth Bearer check) and CALENDARS_API_KEY setting.
Serializers, Models & Migration
src/backend/core/api/serializers.py, src/backend/core/models.py, src/backend/core/migrations/0023_channel_encrypted_settings_and_user.py
Added Channel.encrypted_settings (EncryptedJSONField) and Channel.user FK; ChannelSerializer now moves configured sensitive keys from settingsencrypted_settings; MessageSerializer exposes mime_id.
API Views & Routing
src/backend/core/api/viewsets/submit.py, src/backend/core/api/viewsets/provisioning.py, src/backend/core/api/viewsets/channel.py, src/backend/core/urls.py
Added SubmitRawEmailView (POST /submit/) requiring Calendars API key and processing raw RFC‑822; added ProvisioningMailboxView and ProvisioningUserView; channel create injects generated password into response; routes registered.
MDA: Outbound & Inbound
src/backend/core/mda/outbound.py, src/backend/core/mda/inbound_create.py, src/backend/core/mda/inbound.py
Added validate_mime_size() and _sign_and_store(); prepare_outbound_message accepts raw_mime and can bypass composition; _create_message_from_inbound gains is_outbound altering blob/sender/draft behavior; inbound success check changed to bool(result).
Signals
src/backend/core/signals.py
Replaced getattr(..., False) guards with direct settings.OPENSEARCH_INDEX_THREADS access in signal handlers.
Tests
src/backend/core/tests/api/test_submit.py, src/backend/core/tests/api/test_provisioning_mailbox.py, src/backend/core/tests/api/test_channels.py, src/backend/core/tests/mda/test_autoreply.py
Added comprehensive tests for submit and provisioning endpoints and encrypted channel settings; updated channel tests and minor whitespace cleanup.
OpenAPI
src/backend/core/api/openapi.json
Added read-only nullable mime_id to Message schema and included in required fields.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant API as SubmitRawEmailView
    participant DB as Database
    participant Parser as EmailParser
    participant Outbound as prepare_outbound_message
    participant Signer as DKIMSigner
    participant Blob as BlobStorage
    participant Queue as SMTPQueue

    Client->>API: POST /api/.../submit/ (X-Mail-From, X-Rcpt-To, RFC822 body)
    API->>API: Validate headers & CALENDARS API key
    API->>DB: Resolve mailbox by UUID (X-Mail-From)
    API->>Parser: Parse raw MIME
    Parser-->>API: Parsed email
    API->>API: Verify From matches mailbox
    API->>DB: Create Message (is_outbound=True)
    DB-->>API: Message instance
    API->>Outbound: prepare_outbound_message(message, raw_mime)
    Outbound->>Signer: DKIM sign raw bytes
    Signer-->>Outbound: Signed MIME
    Outbound->>Blob: Store signed MIME as message/rfc822 blob
    Blob-->>Outbound: Blob reference
    Outbound->>DB: Finalize message (mime_id, flags, timestamps)
    Outbound-->>API: Success
    API->>Queue: send_message_task.delay(message.id)
    Queue-->>API: Enqueued
    API-->>Client: 202 Accepted {message_id, status: "accepted"}
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • sdemagny
  • NathanVss
  • jbpenrath

"🐰 I sniff the headers, hop through keys with care,
I tuck secrets away where only rabbits dare,
I parse the RFC and sign the bytes so dear,
Persist, enqueue, then watch them hop out of here! ✨"

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 49.44% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main changes: adding encrypted settings for channels and new provisioning API routes, which are the primary features introduced across the changeset.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch encrypt_channels

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.

@sylvinus sylvinus changed the title ✨(channels) encrypt channel settings + add new read-only provisioning… ✨(channels) encrypt channel settings + add new provisioning routes Mar 31, 2026
Copy link
Copy Markdown

@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: 5

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)

355-373: ⚠️ Potential issue | 🟠 Major

This validation now leaks a persisted blob on rejection.

compose_and_store_mime() has already created and stored message.blob by the time validate_mime_size() runs here. If the size check raises, the request fails but that new blob row remains orphaned in storage. The final MIME should be validated before create_blob(), or explicitly cleaned up on failure.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/backend/core/mda/outbound.py` around lines 355 - 373,
compose_and_store_mime currently creates and persists message.blob before
validate_mime_size runs, which can leave an orphaned blob if validation fails;
update the flow in the outbound send path so that MIME size is validated before
any persistent blob is created (i.e. validate the composed MIME bytes/size prior
to calling create_blob/create_and_store_blob inside compose_and_store_mime), or
if you prefer minimal change, catch validation failures around
validate_mime_size and explicitly delete the persisted blob (use the same blob
deletion routine used elsewhere) in the except block to ensure cleanup;
reference compose_and_store_mime, validate_mime_size,
create_blob/create_and_store_blob and message.blob to locate the spots to move
the check or add deletion logic.
🧹 Nitpick comments (1)
src/backend/core/api/viewsets/channel.py (1)

68-76: Consider a more explicit contract for passing the generated password.

Using a private attribute _generated_password on the model instance creates implicit coupling between the viewset and serializer. The context snippet shows ChannelSerializer.create() doesn't currently set this attribute.

When implementing the password generation feature, consider alternatives:

  • Return a tuple or dict from the serializer's create() method via a custom mixin
  • Use a response serializer that includes the password field explicitly
  • Add a documented public attribute or method on the Channel model

For now, the getattr(..., None) fallback makes this forward-compatible and safe.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/backend/core/api/viewsets/channel.py` around lines 68 - 76, The view
currently reads a private attribute _generated_password from the created Channel
instance which couples the view to serializer internals; change the contract so
ChannelSerializer.create/save returns the generated password explicitly (e.g.,
return a dict or tuple like {"instance": instance, "generated_password": pwd} or
(instance, generated_password)) or have the serializer set a documented public
attribute (e.g., instance.generated_password) before returning the instance;
then update the viewset code that calls serializer.save(mailbox=self.mailbox) to
unpack the returned value or read the public attribute (prefer unpacking a
dict/tuple from ChannelSerializer.save/create to find the created instance and
password) and include that password in the Response.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/backend/core/api/serializers.py`:
- Around line 1524-1555: The _move_sensitive_settings helper only migrates
incoming payloads, leaving existing plaintext secrets in settings and preventing
clearing of previously encrypted keys; update the serializer and model access so
reads never expose plaintext by filtering settings keys listed in
ENCRYPTED_SETTINGS_KEYS for the channel type and returning values only from
encrypted_settings, implement a backfill/migration to move legacy plaintext
values from settings into encrypted_settings, and adjust create/update semantics
so that a full update can remove an encrypted setting when the key is explicitly
set to null (and the merge-path respects explicit nulls vs omitted keys) to
ensure secrets are either encrypted, cleared, or never returned in plaintext.

In `@src/backend/core/api/viewsets/submit.py`:
- Around line 67-70: recipient_emails parsed from rcpt_to_header is validated
but never used, so envelope-only recipients (e.g. Bcc) are not created and
send_message_task never delivers to them; update the message creation flow in
_create_message_from_inbound to merge the parsed recipient_emails into the set
of recipients used to create MessageRecipient rows (ensuring duplicates are
deduped and that Bcc/envelope-only addresses are included even if absent from
MIME headers), and apply the same fix to the analogous logic referenced around
lines 107-116 so all envelope recipients are persisted for send_message_task to
process.

In `@src/backend/core/mda/outbound.py`:
- Around line 309-313: The code validates len(raw_mime) before calling
_sign_and_store, but _sign_and_store prepends a DKIM header which can push the
final payload over the size limit; update the raw-MIME path(s) to re-check the
total size after signing by either (a) having _sign_and_store return the signed
bytes and then call validate_mime_size(len(signed_bytes), message.id) before
persisting, or (b) add a validate_mime_size call inside _sign_and_store
immediately after prepending the DKIM header; apply the same change for the
similar branch around lines 430-438 so messages are size-validated post-signing.
- Around line 375-380: The line overwriting message.has_attachments with
len(attachments) > 0 discards attachments added during compose_and_store_mime()
(like inline signature images); remove that override (or set
message.has_attachments based on the post-composition list such as
all_attachments) so that compose_and_store_mime()’s result is preserved; look
for message.has_attachments, compose_and_store_mime(), attachments and any
all_attachments variable and either delete the reset or replace it with a check
against the final attachment list.

In `@src/backend/core/models.py`:
- Around line 467-474: The ForeignKey field "user" on the Channel model
currently uses on_delete=models.CASCADE which will remove channels if the
creator account is deleted; change its on_delete behavior to models.SET_NULL so
the creator link becomes NULL instead of deleting the Channel (update the user
ForeignKey definition in the Channel model and run a migration to apply the
change). Ensure the field remains null=True, blank=True and that any references
to the creator handle a None value safely.

---

Outside diff comments:
In `@src/backend/core/mda/outbound.py`:
- Around line 355-373: compose_and_store_mime currently creates and persists
message.blob before validate_mime_size runs, which can leave an orphaned blob if
validation fails; update the flow in the outbound send path so that MIME size is
validated before any persistent blob is created (i.e. validate the composed MIME
bytes/size prior to calling create_blob/create_and_store_blob inside
compose_and_store_mime), or if you prefer minimal change, catch validation
failures around validate_mime_size and explicitly delete the persisted blob (use
the same blob deletion routine used elsewhere) in the except block to ensure
cleanup; reference compose_and_store_mime, validate_mime_size,
create_blob/create_and_store_blob and message.blob to locate the spots to move
the check or add deletion logic.

---

Nitpick comments:
In `@src/backend/core/api/viewsets/channel.py`:
- Around line 68-76: The view currently reads a private attribute
_generated_password from the created Channel instance which couples the view to
serializer internals; change the contract so ChannelSerializer.create/save
returns the generated password explicitly (e.g., return a dict or tuple like
{"instance": instance, "generated_password": pwd} or (instance,
generated_password)) or have the serializer set a documented public attribute
(e.g., instance.generated_password) before returning the instance; then update
the viewset code that calls serializer.save(mailbox=self.mailbox) to unpack the
returned value or read the public attribute (prefer unpacking a dict/tuple from
ChannelSerializer.save/create to find the created instance and password) and
include that password in the Response.
🪄 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

Run ID: 4081d7f8-4228-46a3-a993-eb1f4fb88b4e

📥 Commits

Reviewing files that changed from the base of the PR and between 3461981 and 38bbc90.

📒 Files selected for processing (17)
  • src/backend/core/api/permissions.py
  • src/backend/core/api/serializers.py
  • src/backend/core/api/viewsets/channel.py
  • src/backend/core/api/viewsets/provisioning.py
  • src/backend/core/api/viewsets/submit.py
  • src/backend/core/mda/inbound.py
  • src/backend/core/mda/inbound_create.py
  • src/backend/core/mda/outbound.py
  • src/backend/core/migrations/0023_channel_encrypted_settings_and_user.py
  • src/backend/core/models.py
  • src/backend/core/signals.py
  • src/backend/core/tests/api/test_channels.py
  • src/backend/core/tests/api/test_provisioning_mailbox.py
  • src/backend/core/tests/api/test_submit.py
  • src/backend/core/tests/mda/test_autoreply.py
  • src/backend/core/urls.py
  • src/backend/messages/settings.py
💤 Files with no reviewable changes (1)
  • src/backend/core/tests/mda/test_autoreply.py

Copy link
Copy Markdown

@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.

🧹 Nitpick comments (3)
src/backend/core/tests/api/test_submit.py (2)

150-159: Tighten the status code assertion.

The assertion response.status_code in (400, 404) is imprecise. Looking at the implementation in submit.py lines 59-65, an invalid UUID raises ValueError which is caught alongside DoesNotExist, returning 404. The test should assert the specific expected behavior.

♻️ Suggested fix
     def test_invalid_uuid_in_x_mail_from_returns_error(self, client, auth_header):
         response = client.post(
             SUBMIT_URL,
             data=MINIMAL_MIME,
             content_type="message/rfc822",
             HTTP_X_MAIL_FROM="not-a-uuid",
             HTTP_X_RCPT_TO="attendee@example.com",
             **auth_header,
         )
-        assert response.status_code in (400, 404)
+        assert response.status_code == 404
+        assert "not found" in response.json()["detail"].lower()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/backend/core/tests/api/test_submit.py` around lines 150 - 159, The test
test_invalid_uuid_in_x_mail_from_returns_error currently allows 400 or 404 but
the handler in submit.py catches ValueError alongside DoesNotExist and returns
404; update the assertion to expect a single specific status
(response.status_code == 404) so the test matches the submit.py behavior for
invalid UUIDs (leave SUBMIT_URL and MINIMAL_MIME usage unchanged).

208-212: Consider using Mock for clearer mock object creation.

The type("Message", (), {"id": uuid.uuid4()})() pattern works but is unconventional. Using unittest.mock.Mock would be more idiomatic and self-documenting.

♻️ Alternative approach
+from unittest.mock import Mock, patch
-from unittest.mock import patch
 ...
     def test_accepted(
         self, mock_create, mock_prepare, mock_task, client, auth_header, mailbox
     ):
-        fake_message = type("Message", (), {"id": uuid.uuid4()})()
+        fake_message = Mock(id=uuid.uuid4())
         mock_create.return_value = fake_message
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/backend/core/tests/api/test_submit.py` around lines 208 - 212, In
test_accepted, replace the unconventional dynamic type used for fake_message
with an idiomatic unittest.mock.Mock: create a Mock (e.g., fake_message =
Mock()) and set its id attribute (fake_message.id = uuid.uuid4()) or use
Mock(spec=MessageClass) if a real Message class exists; update imports to
include Mock if needed and ensure mock_create.return_value = fake_message
remains the same so the test behavior is unchanged.
src/backend/core/api/viewsets/submit.py (1)

119-137: Consider logging error conditions for observability.

The logger is imported but unused. When message creation or preparation fails (returning 500), logging these failures would aid debugging and align with the coding guideline to "log expected and unexpected actions with appropriate log levels."

📝 Suggested logging additions
         if not message:
+            logger.error(
+                "Failed to create message for mailbox %s from raw submission",
+                mailbox_id,
+            )
             return Response(
                 {"detail": "Failed to create message."},
                 status=status.HTTP_500_INTERNAL_SERVER_ERROR,
             )
         ...
         if not prepared:
+            logger.error(
+                "Failed to prepare outbound message %s for mailbox %s",
+                message.id,
+                mailbox_id,
+            )
             return Response(
                 {"detail": "Failed to prepare message for sending."},
                 status=status.HTTP_500_INTERNAL_SERVER_ERROR,
             )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/backend/core/api/viewsets/submit.py` around lines 119 - 137, The code
currently returns 500 responses when "message" is falsy or "prepared" is falsy
without logging; use the imported logger to record these error conditions before
returning so failures are observable. Specifically, in the block that checks "if
not message:" add a logger.error (or logger.warning) that includes context such
as mailbox id/identifier and any relevant exception or details available about
the failed create message operation, and likewise before the "if not prepared:"
return add a logger.error referencing prepare_outbound_message and include
mailbox, message id (or a digest), and raw_mime presence; keep messages concise
and consistent with existing logging conventions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/backend/core/api/viewsets/submit.py`:
- Around line 119-137: The code currently returns 500 responses when "message"
is falsy or "prepared" is falsy without logging; use the imported logger to
record these error conditions before returning so failures are observable.
Specifically, in the block that checks "if not message:" add a logger.error (or
logger.warning) that includes context such as mailbox id/identifier and any
relevant exception or details available about the failed create message
operation, and likewise before the "if not prepared:" return add a logger.error
referencing prepare_outbound_message and include mailbox, message id (or a
digest), and raw_mime presence; keep messages concise and consistent with
existing logging conventions.

In `@src/backend/core/tests/api/test_submit.py`:
- Around line 150-159: The test test_invalid_uuid_in_x_mail_from_returns_error
currently allows 400 or 404 but the handler in submit.py catches ValueError
alongside DoesNotExist and returns 404; update the assertion to expect a single
specific status (response.status_code == 404) so the test matches the submit.py
behavior for invalid UUIDs (leave SUBMIT_URL and MINIMAL_MIME usage unchanged).
- Around line 208-212: In test_accepted, replace the unconventional dynamic type
used for fake_message with an idiomatic unittest.mock.Mock: create a Mock (e.g.,
fake_message = Mock()) and set its id attribute (fake_message.id = uuid.uuid4())
or use Mock(spec=MessageClass) if a real Message class exists; update imports to
include Mock if needed and ensure mock_create.return_value = fake_message
remains the same so the test behavior is unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fff35496-b21e-45ae-83d2-c31b5e91ddf6

📥 Commits

Reviewing files that changed from the base of the PR and between 38bbc90 and ca06ac3.

📒 Files selected for processing (5)
  • src/backend/core/api/viewsets/submit.py
  • src/backend/core/mda/outbound.py
  • src/backend/core/migrations/0023_channel_encrypted_settings_and_user.py
  • src/backend/core/models.py
  • src/backend/core/tests/api/test_submit.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/backend/core/models.py
  • src/backend/core/migrations/0023_channel_encrypted_settings_and_user.py
  • src/backend/core/mda/outbound.py

Copy link
Copy Markdown

@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.

🧹 Nitpick comments (5)
src/backend/core/api/viewsets/submit.py (4)

129-146: Pre-compute lowercase recipient set to avoid O(n×m) complexity.

Line 133 rebuilds {e.lower() for e in mime_recipients} on every loop iteration, resulting in O(n×m) time complexity where n = envelope recipients and m = MIME recipients. Pre-compute the lowercase set once before the loop.

♻️ Proposed fix
         mime_recipients = set(
             message.recipients.values_list("contact__email", flat=True)
         )
+        mime_recipients_lower = {e.lower() for e in mime_recipients}
         for addr in recipient_emails:
-            if addr.lower() not in {e.lower() for e in mime_recipients}:
+            if addr.lower() not in mime_recipients_lower:
                 try:
                     contact, _ = models.Contact.objects.get_or_create(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/backend/core/api/viewsets/submit.py` around lines 129 - 146, The loop
rebuilds {e.lower() for e in mime_recipients} on every iteration causing O(n×m)
complexity; precompute a lowercase set (e.g., mime_recipients_lower = {e.lower()
for e in mime_recipients}) before iterating recipient_emails and use that set in
the membership test, leaving the try/except that creates models.Contact and
models.MessageRecipient (and uses models.MessageRecipientTypeChoices.BCC and
logger.warning) unchanged.

156-160: Outbound preparation failure lacks diagnostic logging.

Similar to message creation, prepare_outbound_message returning False produces a 500 response without server-side logging, hindering troubleshooting.

♻️ Proposed fix
         if not prepared:
+            logger.error(
+                "Failed to prepare outbound message %s for mailbox %s",
+                message.id,
+                mailbox.id,
+            )
             return Response(
                 {"detail": "Failed to prepare message for sending."},
                 status=status.HTTP_500_INTERNAL_SERVER_ERROR,
             )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/backend/core/api/viewsets/submit.py` around lines 156 - 160, The
prepare_outbound_message failure branch currently returns a 500 response without
any server-side logging; update the branch where prepare_outbound_message is
called (the code that checks "if not prepared") to log a detailed error via the
existing logger (or process logger) before returning the Response—include
identifying context such as the message id, conversation_id or payload summary
and any exception or error info available from prepare_outbound_message so
operators can diagnose why preparation failed (reference the
prepare_outbound_message call and the Response-returning block to make the
change).

145-146: Broad exception catch may hide database or integrity errors.

Swallowing all exceptions with only a warning log could mask serious issues like database connection failures or constraint violations. Consider catching more specific exceptions (e.g., IntegrityError, DatabaseError) or at minimum logging at exception level to capture the traceback.

♻️ Suggested improvement
-                except Exception:  # pylint: disable=broad-exception-caught
-                    logger.warning("Failed to add BCC recipient %s", addr)
+                except Exception:  # pylint: disable=broad-exception-caught
+                    logger.exception("Failed to add BCC recipient %s", addr)

Using logger.exception() will capture the full traceback, making debugging easier while still allowing the submission to proceed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/backend/core/api/viewsets/submit.py` around lines 145 - 146, Replace the
broad "except Exception" that currently logs via logger.warning("Failed to add
BCC recipient %s", addr) with a narrower catch for database-related errors
(e.g., except (IntegrityError, DatabaseError):) and log with logger.exception to
capture the traceback; if other non-DB errors should still be tolerated, add a
second generic except that logs at exception level before continuing. Make sure
to import the specific exceptions (for example from django.db import
IntegrityError, DatabaseError) and keep the existing context (addr) in the log
message so the failure point in the BCC-addition code is clear.

109-123: Message creation failure lacks diagnostic logging.

When _create_message_from_inbound returns None, a 500 response is returned without any server-side logging. This could make production debugging difficult.

♻️ Proposed fix
         if not message:
+            logger.error(
+                "Failed to create message for mailbox %s from raw MIME submission",
+                mailbox.id,
+            )
             return Response(
                 {"detail": "Failed to create message."},
                 status=status.HTTP_500_INTERNAL_SERVER_ERROR,
             )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/backend/core/api/viewsets/submit.py` around lines 109 - 123, The code
returns a 500 when _create_message_from_inbound(...) yields None but does not
log any diagnostic details; update the submit view to log a clear error before
returning by calling the existing logger (e.g., logger or process_logger) with
context including the mailbox identifier (mailbox or mailbox.email),
recipient_email (mailbox_email), and a short summary of parsed_email (e.g.,
subject or message-id) so operators can diagnose why
_create_message_from_inbound failed; keep sensitive raw_mime out of logs or
truncate it if absolutely necessary.
src/backend/core/tests/api/test_submit.py (1)

205-210: Mock message factory could be more realistic.

The values_list mock returns a plain list. While this works because the production code wraps it in set(), consider returning a mock QuerySet or at least documenting this coupling in a comment.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/backend/core/tests/api/test_submit.py` around lines 205 - 210, The test
factory _fake_message returns msg.recipients.values_list as a plain list which
hides the fact production code expects an iterable/QuerySet; update
_fake_message so msg.recipients.values_list.return_value is a QuerySet-like mock
(e.g., a MagicMock with an __iter__ implementation or a Mock(spec=['__iter__'])
that yields "attendee@example.com") or add a short comment in the _fake_message
docstring noting the coupling to the production code that wraps values_list in
set(); this ensures tests better mirror real behavior used by the submit logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/backend/core/api/viewsets/submit.py`:
- Around line 129-146: The loop rebuilds {e.lower() for e in mime_recipients} on
every iteration causing O(n×m) complexity; precompute a lowercase set (e.g.,
mime_recipients_lower = {e.lower() for e in mime_recipients}) before iterating
recipient_emails and use that set in the membership test, leaving the try/except
that creates models.Contact and models.MessageRecipient (and uses
models.MessageRecipientTypeChoices.BCC and logger.warning) unchanged.
- Around line 156-160: The prepare_outbound_message failure branch currently
returns a 500 response without any server-side logging; update the branch where
prepare_outbound_message is called (the code that checks "if not prepared") to
log a detailed error via the existing logger (or process logger) before
returning the Response—include identifying context such as the message id,
conversation_id or payload summary and any exception or error info available
from prepare_outbound_message so operators can diagnose why preparation failed
(reference the prepare_outbound_message call and the Response-returning block to
make the change).
- Around line 145-146: Replace the broad "except Exception" that currently logs
via logger.warning("Failed to add BCC recipient %s", addr) with a narrower catch
for database-related errors (e.g., except (IntegrityError, DatabaseError):) and
log with logger.exception to capture the traceback; if other non-DB errors
should still be tolerated, add a second generic except that logs at exception
level before continuing. Make sure to import the specific exceptions (for
example from django.db import IntegrityError, DatabaseError) and keep the
existing context (addr) in the log message so the failure point in the
BCC-addition code is clear.
- Around line 109-123: The code returns a 500 when
_create_message_from_inbound(...) yields None but does not log any diagnostic
details; update the submit view to log a clear error before returning by calling
the existing logger (e.g., logger or process_logger) with context including the
mailbox identifier (mailbox or mailbox.email), recipient_email (mailbox_email),
and a short summary of parsed_email (e.g., subject or message-id) so operators
can diagnose why _create_message_from_inbound failed; keep sensitive raw_mime
out of logs or truncate it if absolutely necessary.

In `@src/backend/core/tests/api/test_submit.py`:
- Around line 205-210: The test factory _fake_message returns
msg.recipients.values_list as a plain list which hides the fact production code
expects an iterable/QuerySet; update _fake_message so
msg.recipients.values_list.return_value is a QuerySet-like mock (e.g., a
MagicMock with an __iter__ implementation or a Mock(spec=['__iter__']) that
yields "attendee@example.com") or add a short comment in the _fake_message
docstring noting the coupling to the production code that wraps values_list in
set(); this ensures tests better mirror real behavior used by the submit logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ee05df2f-34ca-4279-b6e0-b915a000ed65

📥 Commits

Reviewing files that changed from the base of the PR and between b91fbab and 37085b9.

⛔ Files ignored due to path filters (1)
  • src/frontend/src/features/api/gen/models/message.ts is excluded by !**/gen/**
📒 Files selected for processing (2)
  • src/backend/core/api/viewsets/submit.py
  • src/backend/core/tests/api/test_submit.py

Copy link
Copy Markdown

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/backend/core/api/viewsets/submit.py`:
- Around line 147-148: The warning currently logs PII by including the raw email
variable addr in logger.warning("Failed to add BCC recipient %s", addr); update
this to avoid logging full email — either log a masked representation (e.g., use
a small helper like mask_email(addr) that preserves only non-PII parts such as
the domain or first character) or log a non-PII message (e.g., "Failed to add
BCC recipient (masked)" or "Failed to add BCC recipient; masked address
omitted"); change the logger call in the except block that references addr to
use the masked value or a generic message and add a small helper function
(mask_email) if you choose masking, keeping the variable name addr as the input
to that helper.
🪄 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

Run ID: aeeaeb70-6f1b-4ed8-9c08-3d908ca841f5

📥 Commits

Reviewing files that changed from the base of the PR and between 37085b9 and c40bdd4.

📒 Files selected for processing (1)
  • src/backend/core/api/viewsets/submit.py

Comment on lines +147 to +148
except Exception: # pylint: disable=broad-exception-caught
logger.warning("Failed to add BCC recipient %s", addr)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid logging PII (email address).

The email address addr is logged in the warning message. Per coding guidelines, PII should not be logged. Consider logging only a count or a masked representation.

Proposed fix
             except Exception:  # pylint: disable=broad-exception-caught
-                logger.warning("Failed to add BCC recipient %s", addr)
+                logger.warning("Failed to add BCC recipient")
📝 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
except Exception: # pylint: disable=broad-exception-caught
logger.warning("Failed to add BCC recipient %s", addr)
except Exception: # pylint: disable=broad-exception-caught
logger.warning("Failed to add BCC recipient")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/backend/core/api/viewsets/submit.py` around lines 147 - 148, The warning
currently logs PII by including the raw email variable addr in
logger.warning("Failed to add BCC recipient %s", addr); update this to avoid
logging full email — either log a masked representation (e.g., use a small
helper like mask_email(addr) that preserves only non-PII parts such as the
domain or first character) or log a non-PII message (e.g., "Failed to add BCC
recipient (masked)" or "Failed to add BCC recipient; masked address omitted");
change the logger call in the except block that references addr to use the
masked value or a generic message and add a small helper function (mask_email)
if you choose masking, keeping the variable name addr as the input to that
helper.

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.

1 participant