perf(mail): batch verify-on-read; test build_mail_content; addr-recall

Address PR #935 round-3 review:

- search/verification.py: rewrite _verify_mail_messages to batch by mailbox.
  get_message triggers a server-side IMAP body fetch, so per-result verify
  issued one IMAP FETCH per hit; now it calls the DB-cached list_messages once
  per mailbox (mailbox_id comes from the Qdrant payload via result.metadata)
  and intersects — O(unique mailboxes) light calls instead of O(results) IMAP.
- vector/mail_content.py: include Cc/Bcc in the indexed text so recipient
  queries match; move MAIL_SCAN_MAX_PER_MAILBOX here (shared by scanner index
  window + verifier presence window) with a note that it equals the Mail OCS
  per-request max (100), so it's a fixed constant not a config knob.
- client/mail.py: clamp list_messages limit to 1..100 at the client layer.
- tests: add test_mail_content.py (exact-layout contract for build_mail_content);
  rewrite the mail verifier tests for the batch-per-mailbox shape.

Left as-is: ValidationError isn't caught in the list-endpoint tools — consistent
with nc_notes_*/nc_deck_* and not a regression.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-06-20 13:01:21 +02:00
co-authored by Claude Opus 4.8
parent c62ccf3d0d
commit 891d07db12
6 changed files with 244 additions and 69 deletions
+8 -7
View File
@@ -28,6 +28,7 @@ from nextcloud_mcp_server.server.tag_exclusion import (
is_path_excluded,
)
from nextcloud_mcp_server.vector.dead_letter import is_dead_lettered
from nextcloud_mcp_server.vector.mail_content import MAIL_SCAN_MAX_PER_MAILBOX
from nextcloud_mcp_server.vector.placeholder import (
query_document_metadata,
write_placeholder_point,
@@ -1355,13 +1356,13 @@ async def scan_news_items(
return queued
# Newest-N messages indexed per mailbox. The Mail app paginates by recency, so
# this bounds the index to recent mail; older messages age out of the index as
# newer ones arrive (and the deletion-tracking pass below evicts them, the same
# way it handles actually-deleted messages). Raise this if deeper history is
# wanted, at the cost of more embedding work.
MAIL_SCAN_MAX_PER_MAILBOX = 100
# Newest-N messages indexed per mailbox (= the Mail OCS per-request maximum;
# imported from mail_content so the scanner index window and the search-time
# verifier presence window stay identical). Older messages age out of the index
# as newer ones arrive — the deletion-tracking pass below evicts them, the same
# way it handles actually-deleted messages. Going beyond 100 would require
# cursor pagination, so the value is fixed rather than configurable.
#
# Per-process record of (user_id, mailbox_id) for which the newest-N cap has
# already been logged, so the "older mail not indexed" notice is emitted once at
# info level (discoverable) rather than on every scan tick (which would flood