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.

A fix has been committed to read the version tag from address-data. It will be included in v0.16.9.

Thanking you in anticipation

Thank you for your fix, I did some investigations & we are one step further now but there are still issues. (did the analysis with the help of Claude).

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

Repository: stalwartlabs/calcard
Verified against: calcard 0.3.5 (CHANGELOG: 0.3.4 added CHARSET=UTF-8, 0.3.5 added ENCODING= for vCard ≤3.0), via Stalwart CardDAV with Accept: text/vcard; version=3.0.

Summary

When a card is serialized for target version 3.0, vCard-4.0-only parameters are written unchanged. PROP-ID (RFC 9554) and JSCOMPS (RFC 9555) are not valid in vCard 3.0, yet they appear in 3.0 output. This is a conformance defect: the produced text declares VERSION:3.0 but contains 4.0-only syntax.

The CHARSET/ENCODING additions in 0.3.4/0.3.5 touched the trailing !is_v4 block after the parameter loop; they do not address this, which is in the parameter loop itself.

Reproduction

A contact fetched with Accept: text/vcard; version=3.0:

VERSION:3.0
N;JSCOMPS=";1;0":Web;ABTest;;;;;
EMAIL;PROP-ID=e0:[email protected]
TEL;TYPE=WORK,VOICE;PROP-ID=p0:+49 ...
ADR;TYPE=WORK;PROP-ID=a0;JSCOMPS="s,\, ;11;3;5;6";CHARSET=UTF-8:;;...

The same card fetched with version=4.0 carries the identical parameters under VERSION:4.0. The 3.0 path changes only the VERSION line; it does not strip the 4.0-only parameters.

Root cause (current main)

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

  1. ArchivedVCard::write_to iterates all entries, skipping only Begin/End/Version — no property filtering by target version. v4-only properties (CREATED, GRAMGENDER, PRONOUNS, SOCIALPROFILE, LANGUAGE, JSPROP, …) would likewise be emitted into 3.0.
  2. The parameter loop in ArchivedVCardEntry::write_to has no is_v4 guard: PROP-ID, JSCOMPS, DERIVED, AUTHOR, PHONETIC, etc. are written unconditionally for every target version.
  3. Version handling exists only in the trailing !is_v4 block (ENCODING=b, CHARSET=UTF-8, date formats, the data: URI prefix), i.e. after the parameter loop — which is why a card serializes as ...;JSCOMPS=...;CHARSET=UTF-8:... (the v4-only parameter precedes the correctly-added CHARSET).

Suggested direction

  • Add version-aware filtering inside the writers: when targeting ≤ 3.0, drop v4-only parameters (PROP-ID, JSCOMPS, DERIVED, AUTHOR, AUTHOR-NAME, CREATED, PHONETIC, SCRIPT, SERVICE-TYPE, USERNAME, JSPTR, …) and v4-only properties. JSCOMPS can simply be omitted — N preserves structured-name semantics on its own.
  • Keep writer.rs and rkyv_writer.rs aligned; the existing archived_v3_emits_charset_for_non_ascii test asserts owned == archived for CHARSET — a sibling test covering v4-param stripping under 3.0 would lock this down.
  • Secondary observation: a migrated PHOTO line was emitted ending in a stray carriage return (\r\r\n instead of \r\n), worth a separate look.

Scope of impact — what this is and is NOT

This is a conformance defect (3.0 output containing 4.0-only syntax).

It is worth being precise about what this does not establish. I initially suspected these v4-only parameters caused Apple Contacts to drop cards, but testing on a 508-contact address book disproved that: every card carried JSCOMPS on N, yet cards synced fine. The actual cause of dropped contacts in my case was a missing FN property (mandatory per RFC 6350), unrelated to this issue and filed separately. So this report makes no claim that the emitted v4 parameters cause client-side data loss; it is reported purely as a spec-conformance issue in the 3.0 serializer.

Note on claims

That these v4-only constructs are emitted under VERSION:3.0 is code- and wire-verified. No client-behavior claim is attached.

See my last comment.

Sorry I won’t read your AI analysis. Explain in your own words what the issue is.

Two vCard output issues observed when syncing to Apple Contacts

1. Missing FN when a contact has no full name set

If a contact only has name components (given/surname) but no full name, the exported vCard contains N but no FN:

VERSION:3.0
N;JSCOMPS=";1;0":Web;ABTest;;;;;

FN is mandatory in vCard (RFC 6350), so Apple Contacts silently drops these cards on CardDAV sync. It would help if FN were derived from the name components when no full name is present, so the output is always valid regardless of what the client stored.

2. vCard 4.0-only parameters appear in 3.0 output

When I request version=3.0, the cards still contain 4.0-only parameters like JSCOMPS and PROP-ID:

VERSION:3.0
N;JSCOMPS=";1;0":...
EMAIL;PROP-ID=e0:...
ADR;PROP-ID=a0;JSCOMPS="s,\, ;11;3;5;6":...

These aren’t valid in 3.0. The VERSION line switches to 3.0 but the 4.0 parameters stay.

(To be clear, this is just a conformance thing — it’s not what caused my dropped contacts; that was the missing FN above.)