iOS contacts and Mac Neo

Issue Description

Mac Neo does not sync all carddav contacts

Expected Behavior

All devices should have the same list of contacts.

Actual Behavior

Using a iPad, iPhone and Mac Neo. Only a subset of contacts appear on the Mac Neo.

Reproduction Steps

Create a list of contacts, I used Thunderbird on Windows.
Link to apple devices
Only some of the contacts appear in Mac Neo
You can make them appear by ‘editing’ the contact on the iPad and saving it.

Stalwart Version

v0.15.x

Installation Method

Binary (Linux)

Database Backend

RocksDB

Blob Storage

RocksDB

Search Engine

Internal

Directory Backend

Internal

I have reviewed the documentation and FAQ and confirm that my issue is NOT addressed there.

on

I have searched this support forum (open and closed topics) and confirm this is not a duplicate.

on

I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I’d prefer a human-only reply, I’ll add the no-ai tag to my topic.

on

The documentation doesn’t cover this specific partial-sync symptom. However, based on what you’ve described, there are a few important points to note:

You are running v0.15.x, but CardDAV/CalDAV (the collaboration stack) was introduced in a later version. The v0.15 branch does not include built-in CardDAV support. It’s possible your setup is using an older version of Stalwart that may have had incomplete or different DAV handling compared to what the current docs describe.

Beyond the version concern, the behavior you describe – contacts appearing after you “edit and save” on the iPad – is a classic ETag/sync-token refresh pattern: the client (Mac Contacts / “Mac Neo”) fetched the address book once, cached a sync state, and never picked up the contacts that were already present at the time of first sync. Editing a contact triggers a change notification that forces a re-fetch.

Some things worth trying:

  1. Remove and re-add the CardDAV account on Mac Contacts. This forces a full re-sync from scratch instead of relying on a cached sync token.
  2. Check whether the contacts imported from Thunderbird are in the same address book as the one Mac Contacts is subscribed to. If Thunderbird created a separate address book, the Mac may only be showing the default one.
  3. Upgrade to the current release. CardDAV sync-collection and reporting support has been significantly improved since v0.15, and the issue may already be resolved upstream.

The relevant CardDAV documentation (for the current version) is at: Contacts | Stalwart

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong.

There currently exists an issue with CardDAV & iOS/MacOS.

There should have been a fix in 0.16.7 but it currently does not work. I’m facing the same issues as you on iOS & MacOS.

The “edit and re-save makes it show up” behavior usually means Apple’s client is silently rejecting those cards on the initial sync, often over a vCard field it does not like, rather than the server failing to serve them. I would like to pin down what the missing contacts have in common.

Could you grab one contact that does not sync and check whether it shares a trait with the others (a PHOTO, non-ASCII characters, a missing UID or FN, or vCard 3.0 vs 4.0)? A raw copy of one failing card would help a lot. Also, what exact Stalwart version are you on now, and does removing and re-adding the account on the Mac (a full fresh sync) bring all contacts in, or only the same subset? Server logs for the CardDAV PROPFIND/REPORT during that initial sync would tell me whether the server is sending them and the client is dropping them.

I don’t know how to find;

I am using version 15.5

On the Mac Neo I deleted the contact account and recreated it. I had 32 contacts but on the iPad, iPhone and Thunderbird I have 141. I was able to increase the Neo contacts to 33 by editing a contact on the iPad. There was an apostrophe in a surname that had been converted to â and I changed that back. There are no obvious other differences.

I was unable to find anything about CardDAV in server logs, maybe looking in the wrong place for the wrong thing?

/opt/stalwart-mail/logs# less stalwart.log.2026-06-07 | grep -i CardDAV
root@mail:/opt/stalwart-mail/logs#

Please advise how to proceed.

A little more testing. I can’t see any difference in the ‘cards’ or patterns, other than there is some weird characters in places. The spaces in phone numbers sometimes have a A instead. The language on everything is English.

I created a contact on each of four devices and the problem seems to be isolated to the Neo (which is a new product);

  • iPhone – All contacts were present
  • iPad – All contacts were present
  • Thunderbird – All contacts were present
  • Mac Neo – missing the contact created on Thunderbird

Originally the contacts were created on Thunderbird. 140.11 ESR

Ok, I’ve digged a little bit deeper (with the help of Claude & the currently published Git code base). It seems that we’ve to deal with 2 issues here:

CardDAV: version attribute of CARDDAV:address-data is discarded during XML parsing (RFC 6352 §8.6)

Repository: stalwartlabs/stalwart

Environment

  • Stalwart 0.16.8 (originally observed on 0.16.6)
  • CardDAV via addressbook-query / addressbook-multiget

Summary

The version (and content-type) attributes of the <C:address-data> element in REPORT bodies are silently dropped. A client requesting <C:address-data content-type="text/vcard" version="3.0"/> receives vCard 4.0 if the stored card is 4.0.

Per RFC 6352 §8.6, these attributes specify the media type to be returned; if unsupported, the server MUST fail with the CARDDAV:supported-address-data precondition. Silently returning a different version is neither.

Reproduction

REPORT /dav/card/<account>/<book>/ with body:

<C:addressbook-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:carddav">
  <D:prop><C:address-data content-type="text/vcard" version="3.0"/></D:prop>
  <C:filter><C:prop-filter name="UID"/></C:filter>
</C:addressbook-query>

→ All returned cards contain VERSION:4.0 (the stored version).

The same request with an HTTP header Accept: text/vcard; version=3.0 correctly returns 3.0 — so the conversion machinery exists; only the RFC-mandated XML path never reaches it.

Root cause (current main)

  • In crates/dav-proto/src/parser/property.rs, collect_properties(), the AddressData match arm destructures Token::ElementStart { name, .. }, discarding the raw field that carries the element attributes (contrast with the Comp / Prop arms, which call raw.attributes::<T>()).
  • collect_address_data() only processes child elements (<C:allprop>, <C:prop name=…>).
  • The data model cannot carry the version either: CardDavProperty::AddressData(Vec<CardDavPropertyName>) with CardDavPropertyName { name, group, no_value }.
  • Serialization version selection in crates/dav/src/common/propfind.rs is query.max_vcard_version.or_else(|| card.version()), where max_vcard_version is populated exclusively from the Accept header (crates/dav-proto/src/parser/header.rs).

Side findings

  1. The CalendarData arm in the same function discards raw identically, so the content-type / version attributes from RFC 4791 §9.6 are equally unsupported on the CalDAV side.
  2. Accept parsing (parser/header.rs) matches version= via split_once and VCardVersion::try_parse, which only accepts the exact strings 4.0 | 3.0 | 2.1 | 2.0 — a MIME-quoted parameter (version="3.0") is silently ignored.
  3. When multiple text/vcard entries appear in Accept, the highest version wins; arguably the lowest common version would be the safer negotiation result.

Impact

Apple iOS/macOS Contacts receive vCard 4.0 regardless of what they request, leading to mojibake (UTF-8 bytes read as Latin-1, e.g. BökelbergBökelberg) and silently dropped contacts. Observed: a 508-contact address book → 489 contacts on iOS, fewer on macOS Tahoe 26.5.1.

Note: even when 3.0 is negotiated via Accept, the produced 3.0 output is invalid — see the companion issue in stalwartlabs/calcard (vCard 3.0 serialization emits v4-only parameters such as JSCOMPS). Both fixes together are required for Apple-compatible output.

vCard 3.0 serialization emits vCard 4.0-only parameters and properties (JSCOMPS, RFC 9554/9555)

Repository: stalwartlabs/calcard

Empirical evidence

Verified against Stalwart 0.16.8: a contact created via JSContact (webmail) and fetched over CardDAV with Accept: text/vcard; version=3.0 returns:

VERSION:3.0
N;JSCOMPS=";1;0":Kömps;Jstest;;;;;

The same card fetched with Accept: text/vcard; version=4.0 returns the identical N line under VERSION:4.0 — i.e. the 3.0 downgrade changes the VERSION line but does not remove version-incompatible content.

Summary

JSCOMPS is defined by RFC 9555 for vCard 4.0 only; emitting it in a card declared as 3.0 produces invalid vCard 3.0 and is a plausible trigger for strict parsers (Apple Contacts on iOS/macOS) to drop cards. (The dropping itself is observed client behavior, not code-verified.)

Root cause (current main)

In src/vcard/rkyv_writer.rs — the writer used by Stalwart’s DAV path:

  1. ArchivedVCard::write_to iterates all entries, skipping only Begin / End / Version. There is no property filtering by target version: RFC 9554/9555 and v4-only properties (CREATED, GRAMGENDER, PRONOUNS, SOCIALPROFILE, LANGUAGE, JSPROP, KIND, MEMBER, …) are written into 3.0 output unchanged.
  2. The parameter loop in ArchivedVCardEntry::write_to has no is_v4 check at all: JSCOMPS, PROP-ID, DERIVED, AUTHOR, PHONETIC, etc. are written unconditionally for every target version.
  3. Version-dependent handling exists only for:
    • ;ENCODING=b on binary values (non-v4),
    • date formats (format_as_vcard vs. format_as_legacy_vcard),
    • the data: URI prefix for binary values (v4 only).

Additional divergence between the two writers

The owned writer (src/vcard/writer.rs, VCardEntry::write_to) appends ;CHARSET=UTF-8 for non-ASCII text values when !is_v4. This block is entirely absent from the archived writer (rkyv_writer.rs), so the two writers produce different non-v4 output for the same card. The archived writer is the one used on the DAV serving path.

Suggested direction

  • Version-aware filtering/translation in the writers: drop or translate v4-only parameters and properties when targeting ≤ 3.0. JSCOMPS can simply be omitted — the structured-name semantics are preserved by the N property itself.
  • Align the owned and archived writers so both produce identical output for the same card and target version.

Impact

Stalwart can negotiate vCard 3.0 via the Accept header (and, once fixed, via the CARDDAV:address-data version attribute — see companion issue in stalwartlabs/stalwart), but the resulting “3.0” cards still carry 4.0 syntax. Apple clients exhibit mojibake and silently dropped contacts when consuming such cards; both issues need to be fixed for Apple-compatible CardDAV output.