Conversation
This is the first step in our standards-compliant JMAP support. It implements just enough to be able to list recent threads.
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the
📝 WalkthroughWalkthroughThis PR introduces a complete JMAP (RFC 8620) API implementation for the backend, including custom exception definitions, method handlers with back-reference resolution and property filtering, Django REST views for session and method-call processing, URL routing integration, and comprehensive test infrastructure. Changes
Sequence DiagramsequenceDiagram
participant Client
participant JMAPAPIView
participant MethodRegistry
participant BaseMethod
participant Database
Client->>JMAPAPIView: POST /jmap/ (methodCalls)
JMAPAPIView->>JMAPAPIView: Validate request structure & capabilities
loop For each methodCall
JMAPAPIView->>JMAPAPIView: resolve_args (back-references)
JMAPAPIView->>MethodRegistry: get_handler(method_name)
MethodRegistry-->>JMAPAPIView: Handler class
JMAPAPIView->>BaseMethod: execute(args, context)
alt Account validation
BaseMethod->>BaseMethod: _get_account_id(accountId)
end
alt Back-reference resolution
BaseMethod->>BaseMethod: resolve_value(value, context)
BaseMethod->>BaseMethod: _resolve_reference(ref)
BaseMethod->>BaseMethod: _navigate_path(obj, path)
end
BaseMethod->>Database: Query (filter, sort, paginate)
Database-->>BaseMethod: Results
BaseMethod->>BaseMethod: Serialize results (mailbox/email)
BaseMethod-->>JMAPAPIView: Method response
JMAPAPIView->>JMAPAPIView: Accumulate in results[callId]
end
JMAPAPIView-->>Client: Response (methodResponses, sessionState)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes The changes introduce substantial new functionality with high logic density: multiple JMAP method implementations with overlapping but distinct business logic (filtering, sorting, pagination, property selection), intricate back-reference resolution with nested path traversal, and comprehensive error handling. The heterogeneous nature of individual method handlers—each with unique database queries and serialization—combined with the scope of new public APIs and supporting infrastructure (views, registry, test client) requires careful, multi-faceted review. Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
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. Comment |
| "sessionState": datetime.now(dt_timezone.utc).isoformat(), | ||
| } | ||
|
|
||
| return Response(response) |
Check warning
Code scanning / CodeQL
Information exposure through an exception Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 2 months ago
In general, the fix is to avoid returning raw exception messages in responses. Instead, log the full exception (including stack trace) on the server side and send back a generic error description to the client, following the JMAP spec’s serverFail semantics or a similar pattern.
Concretely, in JMAPAPIView.post, update the generic except Exception as e: handler to:
- Stop including
str(e)in thedescriptionfield. - Replace it with a generic message such as
"An internal server error occurred."or omitdescriptionaltogether if that’s acceptable for your client. - Optionally, log the exception server-side (e.g., via Python’s
loggingmodule) so developers can still see the details while users cannot.
To implement this with minimal functional change:
- Add an import for Python’s
loggingmodule at the top ofsrc/backend/core/api/jmap/views.py. - Create a module-level logger (e.g.,
logger = logging.getLogger(__name__)). - In the
except Exception as e:block, calllogger.exception("Unhandled exception while processing JMAP method %s", method_name)to log the full stack trace. - Replace
{"type": "serverFail", "description": str(e)}with{"type": "serverFail", "description": "An internal server error occurred."}(or similarly generic text).
All changes are confined to src/backend/core/api/jmap/views.py within the shown snippets: adding the logging import and logger definition near the top, and modifying the except Exception block in JMAPAPIView.post.
| @@ -3,6 +3,7 @@ | ||
| from datetime import datetime | ||
| from datetime import timezone as dt_timezone | ||
| from urllib.parse import urlencode, urlparse | ||
| import logging | ||
|
|
||
| from django.conf import settings | ||
| from django.http import HttpResponse, HttpResponseRedirect | ||
| @@ -20,6 +21,8 @@ | ||
| ) | ||
| from .methods import JMAPContext, MethodRegistry, resolve_args | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| # JMAP capabilities we support | ||
| JMAP_CAPABILITIES = { | ||
| "urn:ietf:params:jmap:core": { | ||
| @@ -246,10 +249,16 @@ | ||
| method_responses.append(e.to_response(call_id)) | ||
| except Exception as e: | ||
| # Catch any unexpected errors | ||
| logger.exception( | ||
| "Unhandled exception while processing JMAP method %s", method_name | ||
| ) | ||
| method_responses.append( | ||
| [ | ||
| "error", | ||
| {"type": "serverFail", "description": str(e)}, | ||
| { | ||
| "type": "serverFail", | ||
| "description": "An internal server error occurred.", | ||
| }, | ||
| call_id, | ||
| ] | ||
| ) |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Fix all issues with AI agents
In @src/backend/core/api/jmap/methods.py:
- Around line 179-195: The returned "total" is computed after pagination;
compute total_count = len(mailbox_ids) immediately after mailbox_ids =
list(mailboxes.values_list("id", flat=True)) and before applying the
position/limit slice, then use total_count in the returned dict's "total" field
while still slicing mailbox_ids for "ids"; adjust references in the method
(mailbox_ids, position, limit, and the returned dict) so "ids" contains the
paginated IDs but "total" reflects the full unpaged match count.
- Around line 105-119: Refactor the loop in _navigate_path to enumerate parts
(for idx, part in enumerate(parts)) so when part == "*" you compute
remaining_parts = parts[idx + 1:] (not parts.index(part)) to handle multiple
wildcards correctly; also restructure the control flow so there is no elif after
a return (e.g., use an if/return and then continue or fallthrough for other
cases) to satisfy the no-else-return lint rule, and build remaining_path = "/" +
"/".join(remaining_parts) and return [self._navigate_path(item, remaining_path)
for item in current] or current as before.
In @src/backend/core/api/jmap/views.py:
- Around line 181-189: The current except block in views.py appends str(e) into
method_responses which can leak internal details; change the handler in the
except Exception block (where method_responses.append([...]) is called) to stop
returning str(e) to the client—replace the response description with a generic
message like "Internal server error" or "serverFail" and record the real
exception internally by calling capture_exception() (from sentry_sdk) or logging
it via your logger before appending the generic error response; ensure call_id
and error type remain unchanged so clients can correlate errors without seeing
internal details.
In @src/backend/core/tests/api/jmap/test_jmap_methods.py:
- Around line 31-54: The test contains a local import "import uuid" inside
test_mailbox_query_filters_by_name; move that import to the module top-level
(add "import uuid" to the file imports) and remove the inline import from the
test function so test_mailbox_query_filters_by_name uses the top-level uuid
import instead of importing inside the function.
🧹 Nitpick comments (8)
src/backend/core/urls.py (1)
228-238: Consider adding trailing slash to session endpoint for consistency.The
/jmap/sessionendpoint lacks a trailing slash while/jmap/has one. While this might be intentional per JMAP spec conventions, Django typically expects trailing slashes. Consider either:
- Adding trailing slash:
jmap/session/- Ensuring
APPEND_SLASHsetting handles this appropriatelyThis is a minor consistency observation - if the current pattern matches JMAP client expectations, it's fine to keep as-is.
src/backend/core/tests/api/jmap/test_jmap_session.py (1)
26-51: Address unusedmailboxargument warnings.The
mailboxfixture is required to create the mailbox in the database (side effect), but Pylint warns about unused arguments. Use underscore prefix or explicit annotation to indicate intentional side-effect usage.♻️ Suggested fix
- def test_session_returns_account(self, api_client, user, mailbox): + def test_session_returns_account(self, api_client, user, mailbox): # noqa: ARG002 """Test that the session includes the user's account."""Or rename to underscore prefix:
- def test_session_returns_account(self, api_client, user, mailbox): + def test_session_returns_account(self, api_client, user, _mailbox): """Test that the session includes the user's account."""Apply the same pattern to
test_session_returns_primary_accountat line 40.src/backend/core/api/jmap/views.py (1)
61-68: Simplify first mailbox retrieval.The for-loop pattern to get the first item is unnecessarily verbose.
♻️ Proposed simplification
# Use the first mailbox email as the account name, or user email - primary_email = None - for mailbox in mailboxes[:1]: - primary_email = f"{mailbox.local_part}@{mailbox.domain.name}" - break + first_mailbox = mailboxes.first() + primary_email = ( + f"{first_mailbox.local_part}@{first_mailbox.domain.name}" + if first_mailbox + else None + )src/backend/core/tests/api/jmap/test_jmap_workflow.py (1)
144-150: Move import to top of file.The
Messageimport inside the test function triggers linting warnings. Move it to the top-level imports for consistency and linter compliance.♻️ Proposed fix
At the top of the file (around line 21):
from core import enums, factories +from core.models import MessageThen remove line 145:
- from core.models import Message - Message.objects.filter(id=recent_msg.id).update(src/backend/core/tests/api/jmap/conftest.py (1)
317-321: Minor: Remove unnecessarypassstatement.Exception classes with docstrings don't need an explicit
pass.♻️ Proposed fix
class JMAPTestError(Exception): """Exception raised when a JMAP method call fails.""" - - passsrc/backend/core/api/jmap/methods.py (3)
242-253: N+1 query problem: 4 COUNT queries per mailbox.Each call to
_serialize_mailboxexecutes 4 separate database queries for counts. For M mailboxes, this results in 4M additional queries, which will degrade performance significantly with many mailboxes.Consider annotating the queryset with counts using Django's
CountandCase/When:from django.db.models import Count, Case, When, IntegerField mailboxes = models.Mailbox.objects.filter( accesses__user=self.context.user ).annotate( total_emails=Count('accesses__thread__messages', distinct=True), unread_emails=Count( Case(When(accesses__thread__messages__is_unread=True, then=1)), output_field=IntegerField() ), # ... similar for threads )This would reduce the query count from O(4M) to O(1).
406-411: N+1 query: ThreadAccess query inside serialization loop.Each email serialization executes a separate query for
mailbox_ids. With N emails, this adds N extra queries.Consider prefetching or batching
You could prefetch
thread__accessesin the main query:messages = ( models.Message.objects.filter(id__in=ids) .filter(thread__accesses__mailbox__accesses__user=self.context.user) - .select_related("sender", "thread", "blob") - .prefetch_related("recipients__contact") + .select_related("sender", "thread", "blob") + .prefetch_related("recipients__contact", "thread__accesses") )Then in
_serialize_email:mailbox_ids = [access.mailbox_id for access in message.thread.accesses.all()]
481-491: Potential prefetch inefficiency:.order_by()may bypass prefetch cache.Calling
.order_by("created_at")onthread.messagesinside the loop may issue a new database query per thread, bypassing theprefetch_related("messages")optimization.Use Prefetch with ordering or sort in Python
Option 1 - Use
Prefetchwith ordering:from django.db.models import Prefetch threads = models.Thread.objects.filter( id__in=ids, accesses__mailbox__accesses__user=self.context.user ).prefetch_related( Prefetch("messages", queryset=models.Message.objects.order_by("created_at")) )Then remove the
.order_by():"emailIds": [str(m.id) for m in thread.messages.all()],Option 2 - Sort in Python:
"emailIds": [str(m.id) for m in sorted(thread.messages.all(), key=lambda m: m.created_at)],
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
src/backend/poetry.lockis excluded by!**/*.lock
📒 Files selected for processing (11)
src/backend/core/api/jmap/__init__.pysrc/backend/core/api/jmap/errors.pysrc/backend/core/api/jmap/methods.pysrc/backend/core/api/jmap/views.pysrc/backend/core/tests/api/jmap/__init__.pysrc/backend/core/tests/api/jmap/conftest.pysrc/backend/core/tests/api/jmap/test_jmap_methods.pysrc/backend/core/tests/api/jmap/test_jmap_session.pysrc/backend/core/tests/api/jmap/test_jmap_workflow.pysrc/backend/core/urls.pysrc/backend/pyproject.toml
🧰 Additional context used
📓 Path-based instructions (6)
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/jmap/__init__.pysrc/backend/core/tests/api/jmap/conftest.pysrc/backend/core/tests/api/jmap/__init__.pysrc/backend/core/urls.pysrc/backend/core/api/jmap/errors.pysrc/backend/core/tests/api/jmap/test_jmap_workflow.pysrc/backend/core/tests/api/jmap/test_jmap_session.pysrc/backend/core/tests/api/jmap/test_jmap_methods.pysrc/backend/core/api/jmap/views.pysrc/backend/core/api/jmap/methods.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/jmap/conftest.pysrc/backend/core/tests/api/jmap/__init__.pysrc/backend/core/tests/api/jmap/test_jmap_workflow.pysrc/backend/core/tests/api/jmap/test_jmap_session.pysrc/backend/core/tests/api/jmap/test_jmap_methods.py
src/backend/**/urls.py
📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)
Define clear, RESTful URL patterns using Django’s URL dispatcher
Files:
src/backend/core/urls.py
src/backend/**/views.py
📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)
src/backend/**/views.py: Use Django REST Framework viewsets for API endpoints
Implement error handling at the view level using Django’s built-in mechanisms
Optimize related object fetching with select_related and prefetch_related
Files:
src/backend/core/api/jmap/views.py
src/backend/**/{models.py,forms.py,views.py}
📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)
Keep business logic in models and forms; keep views thin and focused on request handling
Files:
src/backend/core/api/jmap/views.py
src/backend/**/{views.py,tasks.py}
📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)
Use asynchronous views and Celery tasks for I/O-bound or long-running operations
Files:
src/backend/core/api/jmap/views.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/**/views.py : Use Django REST Framework viewsets for API endpoints
Applied to files:
src/backend/core/urls.pysrc/backend/core/api/jmap/views.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/**/urls.py : Define clear, RESTful URL patterns using Django’s URL dispatcher
Applied to files:
src/backend/core/urls.py
🧬 Code graph analysis (6)
src/backend/core/tests/api/jmap/conftest.py (3)
src/backend/core/factories.py (7)
UserFactory(20-31)MailboxFactory(60-96)users_read(71-81)ThreadFactory(125-132)ThreadAccessFactory(135-145)ContactFactory(148-157)MessageFactory(160-186)src/backend/core/models.py (1)
update_stats(798-927)src/backend/core/api/jmap/views.py (2)
get(52-100)post(113-196)
src/backend/core/urls.py (1)
src/backend/core/api/jmap/views.py (2)
JMAPAPIView(103-196)JMAPSessionView(43-100)
src/backend/core/tests/api/jmap/test_jmap_workflow.py (4)
src/backend/core/api/jmap/methods.py (4)
EmailGet(369-464)EmailQuery(277-365)MailboxGet(199-270)ThreadGet(471-501)src/backend/core/tests/api/jmap/conftest.py (5)
jmap_client(324-326)user(22-24)mailbox(28-30)request(88-159)ids(299-301)src/backend/core/factories.py (7)
MailboxFactory(60-96)users_read(71-81)ThreadFactory(125-132)ThreadAccessFactory(135-145)ContactFactory(148-157)MessageFactory(160-186)MailDomainFactory(51-57)src/backend/core/models.py (1)
update_stats(798-927)
src/backend/core/tests/api/jmap/test_jmap_session.py (3)
src/backend/core/tests/api/jmap/conftest.py (3)
api_client(16-18)user(22-24)mailbox(28-30)src/backend/core/api/jmap/views.py (1)
get(52-100)src/backend/core/factories.py (2)
MailboxFactory(60-96)users_read(71-81)
src/backend/core/tests/api/jmap/test_jmap_methods.py (4)
src/backend/core/api/jmap/methods.py (5)
EmailGet(369-464)EmailQuery(277-365)MailboxGet(199-270)MailboxQuery(160-195)ThreadGet(471-501)src/backend/core/tests/api/jmap/conftest.py (6)
jmap_client(324-326)mailbox(28-30)request(88-159)ids(299-301)user(22-24)not_found(309-311)src/backend/core/factories.py (7)
MailDomainFactory(51-57)MailboxFactory(60-96)users_read(71-81)ThreadFactory(125-132)ThreadAccessFactory(135-145)ContactFactory(148-157)MessageFactory(160-186)src/backend/core/tests/api/conftest.py (1)
message(77-81)
src/backend/core/api/jmap/methods.py (3)
src/backend/core/api/jmap/errors.py (3)
InvalidArgumentsError(34-37)InvalidResultReferenceError(40-43)UnknownMethodError(28-31)src/backend/core/enums.py (1)
MessageRecipientTypeChoices(31-36)src/backend/core/models.py (1)
size(1634-1636)
🪛 GitHub Actions: Lint and tests
src/backend/core/tests/api/jmap/conftest.py
[warning] 28-28: Pylint: W0621 Redefining name 'user' from outer scope (line 22).
[warning] 34-34: Pylint: W0621 Redefining name 'user' from outer scope (line 22).
[warning] 36-36: Pylint: W0621 Redefining name 'mailbox' from outer scope (line 28).
src/backend/core/tests/api/jmap/test_jmap_workflow.py
[warning] 145-145: Pylint: C0415 Import outside toplevel (core.models.Message).
src/backend/core/tests/api/jmap/test_jmap_session.py
[warning] 26-26: Pylint: W0613 Unused argument 'mailbox'.
[warning] 40-40: Pylint: W0613 Unused argument 'mailbox'.
src/backend/core/tests/api/jmap/test_jmap_methods.py
[warning] 33-33: Pylint: C0415 Import outside toplevel (uuid).
src/backend/core/api/jmap/views.py
[warning] 181-181: Pylint: W0718 Catching too general exception Exception (broad-exception-caught).
src/backend/core/api/jmap/methods.py
[error] 106-106: Pylint: R1705 Unnecessary "elif" after "return". Remove the leading "el" from "elif" (no-else-return).
🪛 GitHub Check: CodeQL
src/backend/core/api/jmap/views.py
[warning] 196-196: Information exposure through an exception
Stack trace information flows to this location and may be exposed to an external user.
🪛 Ruff (0.14.11)
src/backend/core/tests/api/jmap/test_jmap_workflow.py
145-145: import should be at the top-level of a file
(PLC0415)
src/backend/core/tests/api/jmap/test_jmap_methods.py
33-33: import should be at the top-level of a file
(PLC0415)
🔇 Additional comments (22)
src/backend/core/tests/api/jmap/__init__.py (1)
1-1: LGTM - standard test package initialization.src/backend/core/urls.py (1)
8-8: LGTM - JMAP view imports follow existing conventions.src/backend/core/api/jmap/__init__.py (1)
1-1: LGTM - standard package initialization.src/backend/core/tests/api/jmap/test_jmap_methods.py (6)
1-19: LGTM - well-organized imports and test setup.Good use of
pytestmarkto applydjango_dbmarker to all tests in the module.
56-67: Good coverage for access control.Testing that inaccessible mailboxes are properly excluded is important for security validation.
69-108: LGTM - comprehensive Mailbox/get coverage.Good coverage including the back-reference test (Line 92) which validates an important JMAP protocol feature.
110-194: LGTM - thorough Email/query testing.Good coverage of JMAP-specific features including
collapseThreadsbehavior and sort ordering. The verification loop at Lines 163-169 properly validates that thread collapsing works correctly.
196-266: LGTM - solid Email/get test coverage.Good testing of partial property selection (Lines 234-242) and keyword flag serialization (Lines 244-265).
268-308: LGTM - Thread/get tests validate ordering correctly.Good verification that
emailIdsare returned in chronological order (oldest to newest) at Line 299.src/backend/pyproject.toml (1)
82-82: LGTM - dev dependency for JMAP testing.The
jmapc==0.2.23library is appropriately added as a dev dependency (correctly alphabetized), supporting the JMAP endpoint testing implemented in the codebase.src/backend/core/api/jmap/errors.py (1)
1-61: LGTM!Clean implementation of JMAP error types per RFC 8620. The class hierarchy is well-structured with appropriate
error_typevalues and theto_response()method correctly formats the error response tuple.src/backend/core/tests/api/jmap/test_jmap_session.py (1)
62-81: LGTM!The authentication test and mailbox email name test are well-structured. The inline mailbox creation in
test_session_uses_mailbox_email_as_namewith explicitlocal_partanddomain__nameis appropriate for testing specific email formatting.src/backend/core/api/jmap/views.py (1)
21-40: LGTM!The JMAP capabilities constant and session response structure follow RFC 8620 correctly. The session view properly handles user mailboxes, builds appropriate URLs, and includes all required session properties.
Also applies to: 43-100
src/backend/core/tests/api/jmap/test_jmap_workflow.py (2)
26-121: LGTM!The
test_recent_threads_full_workflowtest is well-structured, following the three-step JMAP workflow pattern. Good use of unique domain names via UUID to prevent test isolation issues, and proper setup of threads with messages.
186-254: LGTM!The empty mailbox and multiple mailboxes tests provide good edge case coverage. The assertions are focused and verify the expected behavior correctly.
src/backend/core/tests/api/jmap/conftest.py (3)
27-31: Pylint warnings are false positives for pytest fixture pattern.The warnings about redefining
userandmailboxfrom outer scope are expected in pytest. Fixtures that depend on other fixtures naturally reuse parameter names. No changes needed.Also applies to: 33-61
64-159: LGTM!The
JMAPTestClientclass is well-designed to bridge DRF's APIClient with jmapc method objects. The two-pass approach for handling call IDs and Ref resolution is correct. Good documentation and error handling with theraise_errorsparameter.
213-288: LGTM!The
_process_refsmethod correctly handles multiple Ref reference types (previous method via -1, integer index, or method name string) and properly converts them to JMAP back-reference format. The recursive handling of nested dicts is appropriate.src/backend/core/api/jmap/methods.py (4)
19-46: LGTM!The registry pattern is well-implemented with clean decorator syntax and proper error handling for unknown methods.
48-54: LGTM!Simple and effective context container for passing user and results between method calls.
140-153: LGTM!The function correctly resolves
#-prefixed back-reference keys per JMAP spec.
276-344: LGTM!The query logic is well-structured: filters are applied correctly,
totalis computed before pagination, and thecollapseThreadsimplementation using subqueries is efficient.
| for part in parts: | ||
| if part == "*": | ||
| # Wildcard: extract from all items in list | ||
| if not isinstance(current, list): | ||
| raise InvalidResultReferenceError( | ||
| f"Cannot use '*' on non-list: {type(current)}" | ||
| ) | ||
| # Return the remaining path applied to each item | ||
| remaining_parts = parts[parts.index(part) + 1 :] | ||
| if remaining_parts: | ||
| remaining_path = "/" + "/".join(remaining_parts) | ||
| return [ | ||
| self._navigate_path(item, remaining_path) for item in current | ||
| ] | ||
| return current |
There was a problem hiding this comment.
Fix elif after return to satisfy linter, and address potential bug with multiple wildcards.
Two issues in this block:
-
Linter violation (Line 106): The
elifafter thereturnon line 119 triggers Pylint'sno-else-returnrule. -
Potential bug (Line 113): Using
parts.index(part)whenpart == "*"will always find the first occurrence of"*"inparts, not the current position. If a path contains multiple wildcards (e.g.,/list/*/nested/*/id), this will produce incorrect remaining paths.
Proposed fix using enumeration to track position
- for part in parts:
+ for i, part in enumerate(parts):
if part == "*":
# Wildcard: extract from all items in list
if not isinstance(current, list):
raise InvalidResultReferenceError(
f"Cannot use '*' on non-list: {type(current)}"
)
# Return the remaining path applied to each item
- remaining_parts = parts[parts.index(part) + 1 :]
+ remaining_parts = parts[i + 1 :]
if remaining_parts:
remaining_path = "/" + "/".join(remaining_parts)
return [
self._navigate_path(item, remaining_path) for item in current
]
return current
- elif isinstance(current, dict):
+ if isinstance(current, dict):🧰 Tools
🪛 GitHub Actions: Lint and tests
[error] 106-106: Pylint: R1705 Unnecessary "elif" after "return". Remove the leading "el" from "elif" (no-else-return).
🤖 Prompt for AI Agents
In @src/backend/core/api/jmap/methods.py around lines 105 - 119, Refactor the
loop in _navigate_path to enumerate parts (for idx, part in enumerate(parts)) so
when part == "*" you compute remaining_parts = parts[idx + 1:] (not
parts.index(part)) to handle multiple wildcards correctly; also restructure the
control flow so there is no elif after a return (e.g., use an if/return and then
continue or fallthrough for other cases) to satisfy the no-else-return lint
rule, and build remaining_path = "/" + "/".join(remaining_parts) and return
[self._navigate_path(item, remaining_path) for item in current] or current as
before.
| # Get IDs | ||
| mailbox_ids = list(mailboxes.values_list("id", flat=True)) | ||
|
|
||
| # Apply pagination | ||
| if limit is not None: | ||
| mailbox_ids = mailbox_ids[position : position + limit] | ||
| else: | ||
| mailbox_ids = mailbox_ids[position:] | ||
|
|
||
| return { | ||
| "accountId": account_id, | ||
| "queryState": self._get_state(), | ||
| "canCalculateChanges": False, | ||
| "position": position, | ||
| "ids": [str(mid) for mid in mailbox_ids], | ||
| "total": len(mailbox_ids), | ||
| } |
There was a problem hiding this comment.
total returns paginated count instead of total matching count.
Per JMAP spec, total should represent the total number of results in the query before pagination is applied. Currently, total is computed from len(mailbox_ids) after slicing.
Proposed fix
# Get IDs
- mailbox_ids = list(mailboxes.values_list("id", flat=True))
+ all_mailbox_ids = list(mailboxes.values_list("id", flat=True))
+ total_count = len(all_mailbox_ids)
# Apply pagination
if limit is not None:
- mailbox_ids = mailbox_ids[position : position + limit]
+ mailbox_ids = all_mailbox_ids[position : position + limit]
else:
- mailbox_ids = mailbox_ids[position:]
+ mailbox_ids = all_mailbox_ids[position:]
return {
"accountId": account_id,
"queryState": self._get_state(),
"canCalculateChanges": False,
"position": position,
"ids": [str(mid) for mid in mailbox_ids],
- "total": len(mailbox_ids),
+ "total": total_count,
}🤖 Prompt for AI Agents
In @src/backend/core/api/jmap/methods.py around lines 179 - 195, The returned
"total" is computed after pagination; compute total_count = len(mailbox_ids)
immediately after mailbox_ids = list(mailboxes.values_list("id", flat=True)) and
before applying the position/limit slice, then use total_count in the returned
dict's "total" field while still slicing mailbox_ids for "ids"; adjust
references in the method (mailbox_ids, position, limit, and the returned dict)
so "ids" contains the paginated IDs but "total" reflects the full unpaged match
count.
| except Exception as e: | ||
| # Catch any unexpected errors | ||
| method_responses.append( | ||
| [ | ||
| "error", | ||
| {"type": "serverFail", "description": str(e)}, | ||
| call_id, | ||
| ] | ||
| ) |
There was a problem hiding this comment.
Security: Avoid exposing internal exception details to clients.
The generic exception handler exposes str(e) directly in the response, which can leak internal implementation details, file paths, or stack traces. This is flagged by CodeQL as an information disclosure risk.
🔒 Proposed fix
except JMAPError as e:
method_responses.append(e.to_response(call_id))
- except Exception as e:
+ except Exception:
# Catch any unexpected errors
+ # Log the actual exception for debugging, but return generic message
+ import logging
+ logger = logging.getLogger(__name__)
+ logger.exception("Unexpected error processing JMAP method %s", method_name)
method_responses.append(
[
"error",
- {"type": "serverFail", "description": str(e)},
+ {"type": "serverFail", "description": "An unexpected error occurred"},
call_id,
]
)Consider also reporting to Sentry per coding guidelines:
from sentry_sdk import capture_exception
# ...
except Exception:
capture_exception()
# ...📝 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.
| except Exception as e: | |
| # Catch any unexpected errors | |
| method_responses.append( | |
| [ | |
| "error", | |
| {"type": "serverFail", "description": str(e)}, | |
| call_id, | |
| ] | |
| ) | |
| except Exception: | |
| # Catch any unexpected errors | |
| # Log the actual exception for debugging, but return generic message | |
| import logging | |
| logger = logging.getLogger(__name__) | |
| logger.exception("Unexpected error processing JMAP method %s", method_name) | |
| method_responses.append( | |
| [ | |
| "error", | |
| {"type": "serverFail", "description": "An unexpected error occurred"}, | |
| call_id, | |
| ] | |
| ) |
🧰 Tools
🪛 GitHub Actions: Lint and tests
[warning] 181-181: Pylint: W0718 Catching too general exception Exception (broad-exception-caught).
🤖 Prompt for AI Agents
In @src/backend/core/api/jmap/views.py around lines 181 - 189, The current
except block in views.py appends str(e) into method_responses which can leak
internal details; change the handler in the except Exception block (where
method_responses.append([...]) is called) to stop returning str(e) to the
client—replace the response description with a generic message like "Internal
server error" or "serverFail" and record the real exception internally by
calling capture_exception() (from sentry_sdk) or logging it via your logger
before appending the generic error response; ensure call_id and error type
remain unchanged so clients can correlate errors without seeing internal
details.
| def test_mailbox_query_filters_by_name(self, jmap_client, user): | ||
| """Test that Mailbox/query can filter by name.""" | ||
| import uuid | ||
|
|
||
| # Create mailboxes with specific names using unique domain | ||
| domain_name = f"filter-{uuid.uuid4().hex[:8]}.com" | ||
| domain = factories.MailDomainFactory(name=domain_name) | ||
| mailbox1 = factories.MailboxFactory( | ||
| local_part="inbox", | ||
| domain=domain, | ||
| users_read=[user], | ||
| ) | ||
| factories.MailboxFactory( | ||
| local_part="other", | ||
| domain=domain, | ||
| users_read=[user], | ||
| ) | ||
|
|
||
| result = jmap_client.request( | ||
| MailboxQuery(filter={"name": f"inbox@{domain_name}"}) | ||
| ) | ||
|
|
||
| assert str(mailbox1.id) in result.ids | ||
| assert len(result.ids) == 1 |
There was a problem hiding this comment.
Move uuid import to top-level.
The pipeline flagged this: import uuid should be at the top of the file, not inside the test function.
Proposed fix
Add to imports at the top of the file (around line 7):
import uuidThen remove line 33:
def test_mailbox_query_filters_by_name(self, jmap_client, user):
"""Test that Mailbox/query can filter by name."""
- import uuid
-
# Create mailboxes with specific names using unique domain🧰 Tools
🪛 GitHub Actions: Lint and tests
[warning] 33-33: Pylint: C0415 Import outside toplevel (uuid).
🪛 Ruff (0.14.11)
33-33: import should be at the top-level of a file
(PLC0415)
🤖 Prompt for AI Agents
In @src/backend/core/tests/api/jmap/test_jmap_methods.py around lines 31 - 54,
The test contains a local import "import uuid" inside
test_mailbox_query_filters_by_name; move that import to the module top-level
(add "import uuid" to the file imports) and remove the inline import from the
test function so test_mailbox_query_filters_by_name uses the top-level uuid
import instead of importing inside the function.
Please follow what's done by stallwart (and not followed by Twake Mail Flutter currently): expose (and redirect) Twake mail uses a more complex mechanism atm (OpenID discovery and webfinger) but retrospectively we would be better served by a simpler setup.
|
This makes the JMAP Webmail demo work with light patches
| ) | ||
|
|
||
| # Redirect back to the demo with the token in the URL fragment | ||
| return HttpResponseRedirect(f"{stored_redirect}#access_token={access_token}") |
Check warning
Code scanning / CodeQL
URL redirection from remote source Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 2 months ago
In general, to fix untrusted URL redirection you must not redirect directly to arbitrary user-provided URLs. Instead, either (a) maintain a whitelist of allowed redirect targets (domains or full URLs), or (b) only allow relative paths on the same host, using Django’s url_has_allowed_host_and_scheme helper to enforce this. This code is already using Django, so the best low-impact fix is to validate redirect_uri with url_has_allowed_host_and_scheme, restricting it to allowed hosts and safe schemes before putting it into the session and later redirecting to it.
Concretely for JMAPOIDCLoginView.get in src/backend/core/api/jmap/views.py:
- Import
url_has_allowed_host_and_schemefromdjango.utils.http. - Replace the custom
urlparse-based validation (lines 124–128) with a call tourl_has_allowed_host_and_scheme. Useallowed_hosts={request.get_host()}(orsettings.ALLOWED_HOSTSif you want more flexibility) and restrict schemes to["http", "https"]. - Only store
redirect_uriin the session if it passes that check; otherwise, return a 400 error as currently done. - Optionally, to make CodeQL’s reasoning clearer, keep using
stored_redirectbut add a comment that it has already been validated using Django’s helper.
This preserves existing behaviour for legitimate same-origin redirects while blocking redirects to arbitrary external domains, and requires only a small code change.
| @@ -6,6 +6,7 @@ | ||
|
|
||
| from django.conf import settings | ||
| from django.http import HttpResponse, HttpResponseRedirect | ||
| from django.utils.http import url_has_allowed_host_and_scheme | ||
| from django.views import View | ||
|
|
||
| from rest_framework.parsers import JSONParser | ||
| @@ -122,9 +123,13 @@ | ||
|
|
||
| # Store redirect_uri in session on first visit | ||
| if redirect_uri: | ||
| # Validate redirect_uri to prevent open redirects | ||
| parsed = urlparse(redirect_uri) | ||
| if not parsed.scheme or not parsed.netloc: | ||
| # Validate redirect_uri to prevent open redirects. | ||
| # Only allow redirects to the current host (or its aliases) over http/https. | ||
| if not url_has_allowed_host_and_scheme( | ||
| url=redirect_uri, | ||
| allowed_hosts={request.get_host()}, | ||
| require_https=request.is_secure(), | ||
| ): | ||
| return HttpResponse("Invalid redirect_uri", status=400) | ||
| request.session["jmap_oidc_redirect_uri"] = redirect_uri | ||
|
|
This is the first step in our standards-compliant JMAP support.
For now it implements just enough to be able to list recent threads and send emails.
We should:
Summary by CodeRabbit
New Features
Tests
✏️ Tip: You can customize this high-level summary in your review settings.