refactor(mail): address PR #935 round-1 review

- models/mail.py: lowercase `list` generics per CLAUDE.md convention.
- client/mail.py: _ocs_get now inspects ocs.meta.statuscode (re-raises >=400
  as HTTPStatusError carrying the OCS code so callers' 404/403 handling
  applies) and guards response.json() against non-JSON bodies (RequestError).
- Extract the duplicated _format_addresses + content reconstruction into
  vector/mail_content.py, used by both processor.py and context.py (fixes the
  SonarCloud new_duplicated_lines_density gate).
- processor.py: add the missing mail_message Qdrant payload block so the
  computed mail metadata (subject/from/to/cc/date_int/has_attachments/
  account_id/mailbox_id) is actually stored, not dropped.
- Rename the list_messages `filter` param to `search_filter` (avoid shadowing
  builtins.filter); still maps to the OCS `filter` query param.
- Docstring notes: has_more heuristic, attachment content size.
- Tests: OCS meta-failure + non-JSON client paths; initial-sync scanner tests
  (tests/unit/vector/test_scanner_mail.py).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-06-20 12:31:32 +02:00
co-authored by Claude Opus 4.8
parent 3074622455
commit 62ee3e9f32
8 changed files with 303 additions and 94 deletions
+11 -13
View File
@@ -1,7 +1,5 @@
"""Pydantic models for Nextcloud Mail app responses (read-only)."""
from typing import List
from pydantic import BaseModel, ConfigDict, Field
from .base import BaseResponse
@@ -41,7 +39,7 @@ class MailMailbox(BaseModel):
None, alias="displayName", description="Human-readable mailbox name"
)
account_id: int = Field(alias="accountId", description="Parent account ID")
special_use: List[str] = Field(
special_use: list[str] = Field(
default_factory=list,
alias="specialUse",
description="Special-use roles (e.g. inbox, sent, trash)",
@@ -94,10 +92,10 @@ class MailMessageSummary(BaseModel):
date_int: int | None = Field(
None, alias="dateInt", description="Sent date as a Unix timestamp (seconds)"
)
from_: List[MailAddress] = Field(
from_: list[MailAddress] = Field(
default_factory=list, alias="from", description="Sender addresses"
)
to: List[MailAddress] = Field(
to: list[MailAddress] = Field(
default_factory=list, description="Recipient addresses"
)
mailbox_id: int | None = Field(
@@ -123,14 +121,14 @@ class MailMessage(BaseModel):
date_int: int | None = Field(
None, alias="dateInt", description="Sent date as a Unix timestamp (seconds)"
)
from_: List[MailAddress] = Field(
from_: list[MailAddress] = Field(
default_factory=list, alias="from", description="Sender addresses"
)
to: List[MailAddress] = Field(
to: list[MailAddress] = Field(
default_factory=list, description="Recipient addresses"
)
cc: List[MailAddress] = Field(default_factory=list, description="CC addresses")
bcc: List[MailAddress] = Field(default_factory=list, description="BCC addresses")
cc: list[MailAddress] = Field(default_factory=list, description="CC addresses")
bcc: list[MailAddress] = Field(default_factory=list, description="BCC addresses")
has_html_body: bool = Field(
False, alias="hasHtmlBody", description="Whether the body is HTML"
)
@@ -138,7 +136,7 @@ class MailMessage(BaseModel):
None,
description="Rendered body (sanitized HTML if hasHtmlBody, else plain text)",
)
attachments: List[MailAttachment] = Field(
attachments: list[MailAttachment] = Field(
default_factory=list, description="Message attachments"
)
@@ -149,21 +147,21 @@ class MailMessage(BaseModel):
class ListAccountsResponse(BaseResponse):
"""Response model for listing mail accounts."""
results: List[MailAccount] = Field(description="List of mail accounts")
results: list[MailAccount] = Field(description="List of mail accounts")
total_count: int = Field(description="Total number of accounts")
class ListMailboxesResponse(BaseResponse):
"""Response model for listing mailboxes."""
results: List[MailMailbox] = Field(description="List of mailboxes")
results: list[MailMailbox] = Field(description="List of mailboxes")
total_count: int = Field(description="Total number of mailboxes")
class ListMessagesResponse(BaseResponse):
"""Response model for listing message envelopes."""
results: List[MailMessageSummary] = Field(description="List of message summaries")
results: list[MailMessageSummary] = Field(description="List of message summaries")
total_count: int = Field(description="Number of messages returned")
has_more: bool = Field(False, description="Whether more messages may exist")