fix(contacts): warn on unsupported dict/list email/tel update inputs

Surface the text-merge update path's limitation rather than silently no-op:
when contact_data['email'] or ['tel'] arrives as a dict/list on update, log a
warning at the top of _merge_vcard_properties pointing callers at plain str
or create_contact. Existing EMAIL/TEL lines are still preserved unchanged.

Bring nc_contacts_update_contact docstring into parity with create — the
update tool now documents the same keys plus the explicit single-string
limitation for email/tel and the BDAY validation / URL first-only behaviours.

Three new TestMergeVcardProperties cases pin the warning: dict email warns,
list tel warns, plain str email is silent (no false positives).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-04-26 16:03:29 +02:00
co-authored by Claude Opus 4.7
parent 125ab40121
commit c91be45374
3 changed files with 88 additions and 2 deletions
+23 -1
View File
@@ -482,7 +482,29 @@ class ContactsClient(BaseNextcloudClient):
def _merge_vcard_properties(
self, raw_vcard: str, contact_data: dict[str, Any], uid: str
) -> str:
"""Merge new contact data into existing raw vCard while preserving all properties."""
"""Merge new contact data into existing raw vCard while preserving all properties.
Limitation: dict / list-form ``email`` and ``tel`` inputs are not applied
by this text-merge path. Existing EMAIL/TEL lines are preserved unchanged,
and no new lines are written for the dict/list inputs. Pass plain strings
to update EMAIL/TEL here, or recreate via ``create_contact`` for full
multi-entry support with TYPE annotations.
"""
# Surface dict/list email/tel up front rather than silently no-op in the
# add-new loop below (where the isinstance(value, str) guard skips them).
for _key in ("email", "tel"):
_value = contact_data.get(_key)
if _value is not None and not isinstance(_value, str):
logger.warning(
"update_contact: %s=%r (dict/list shape) is not applied via "
"the text-merge update path; existing %s lines are preserved "
"unchanged. Use a plain string to update %s here, or recreate "
"via create_contact for multi-entry support.",
_key,
_value,
_key.upper(),
_key.upper(),
)
try:
# Instead of using pythonvCard4 which has formatting issues,
# let's do a simple text-based merge to preserve exact formatting
+24 -1
View File
@@ -250,7 +250,30 @@ def configure_contacts_tools(mcp: FastMCP):
not the display name. Use nc_contacts_list_addressbooks to
find available URI slugs.
uid: The unique ID of the contact to update.
contact_data: A dictionary with the contact's updated details, e.g. {"fn": "Jane Doe", "email": "jane.doe@example.com"}.
contact_data: A dictionary with the contact's updated details. Supported
keys mirror nc_contacts_create_contact:
- ``fn`` (str): Formatted full name.
- ``email`` (str): Email address. **Update path supports plain
strings only**; dict / list-form inputs are not applied — the
existing EMAIL line is preserved unchanged and a warning is
logged. Use create_contact for multi-entry support with TYPE
annotations.
- ``tel`` / ``phone`` (str): Phone number. Same single-string
limitation as ``email`` above.
- ``org`` / ``organization`` (str or list of str): Organization.
Lists become semicolon-separated ORG components per RFC 6350.
- ``title`` (str): Job title.
- ``note`` (str): Free-form note.
- ``nickname`` (str or list of str).
- ``bday`` (ISO date str ``"YYYY-MM-DD"`` or ``datetime.date``).
Non-ISO strings are rejected with a warning; the existing
BDAY line is preserved.
- ``categories`` (list of str, or comma-separated str).
- ``url`` (str or list of str). Only the first URL is written
on update; multi-URL contacts should use create_contact.
Example: ``{"fn": "Jane Doe", "email": "jane.doe@example.com"}``.
etag: Optional ETag for optimistic concurrency control.
"""
client = await get_client(ctx)