German umlaute in iOS contacts app are not rendered correct

Issue Description

When using the CardDav account all german umlauts (äöüß) are not rendered correct & displayed. For e.g. Bökelberg becomse Bökelberg etc.

Furthermore not all contacts are synced in total (508 in adress book, only 489 are synced)

Expected Behavior

Umlauts are displayed correct & all contacts are synced

Actual Behavior

Umlauts are not displayed correct & contacts are missing.

Reproduction Steps

  1. Create a contact with a german umlaut
  2. Sync the CardDav address book with this contact to your iPhone

Stalwart Version

v0.16.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 server is already correctly setting Content-Type: text/vcard; charset=utf-8 in its CardDAV GET response. This means the garbled display (Bökelberg instead of Bökelberg) is a classic double-encoding or charset mismatch issue happening on the iOS side or during the initial import into Stalwart, not in the server’s wire response.

Here are the two likely causes and what to check:

1. Corrupted data at import time
The garbling pattern ö for ö is the classic signature of UTF-8 bytes being interpreted as Latin-1/ISO-8859-1 at some point. If you imported your contacts from a .vcf file that was saved as Latin-1 (or had no explicit charset declaration), the non-ASCII characters may have been stored incorrectly in Stalwart’s database from the start. To verify: check one of the affected contacts in the Stalwart WebUI directly. If the umlaut looks wrong there too, the problem is in the stored data, not in sync.

2. The source .vcf file or export encoding
If you exported contacts from another app/server as vCard 2.1, those files sometimes use CHARSET=ISO-8859-1 parameters on individual fields (which vCard 2.1 allowed). When re-imported into a server that treats everything as UTF-8, this can produce the exact garbling you describe.

For the missing contacts (508 vs 489): This is a separate issue. It would help to know whether those contacts are visible in the Stalwart WebUI or if they are entirely absent, to distinguish between a sync/pagination issue and an import-loss issue.

Recommended next steps:

  1. Open one affected contact in the Stalwart WebUI – if the umlaut is already wrong there, the fix is to re-import your contacts from a clean UTF-8 encoded .vcf export.
  2. Use a tool like file -i yourcontacts.vcf or open it in a hex editor to confirm the source file’s encoding before re-importing.
  3. For the missing 19 contacts, check whether they appear in the WebUI address book. If they do appear there but not on iOS, that may be a separate CardDAV sync/pagination bug worth reporting with HTTP logs.

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

Checked the Web-UI, the characters are display correct there as they are also displayed correct in my email client (eM Client).

The number of contacts in the WebUI address book matches the number of contacts in the email client too.

This has been fixed. The fix will be included in v0.16.7.

Thanks for the reply. I’ve analyzed this further & the main issue is that iOS can only parse vCard v3.0 & the serialized vCard that Stalwart is sending is a v4.0

grep -aoE ‘VERSION:[0-9.]+’ /tmp/all.xml | sort | uniq -c
508 VERSION:4.0

Can you confirm this behavior?

I’ve tested the 0.16.7 & sadly the error is not fixed. One issue is that Stalwart is always delivering VERSION:4.0 vCards even if we explicitly ask for a v3 version. This can be easily tested with the following curl call:

curl -su ‘yourmail:app_password’ -X REPORT -H “Depth: 1”
-H ‘Content-Type: application/xml’
–data ‘<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>’
“``https://stalwart.cloud-fuchs.eu/dav/card/yourmail/default/”`` -o /tmp/v3.xml

grep -aoE ‘VERSION:[0-9.]+’ /tmp/v3.xml | sort | uniq -c
508 VERSION:4.0

As per RFC 6352 chapter 5.1.1 the server has to respect the negotiation of the client:

"*Servers might support more than one primary media type for address
object resources, for example, vCard v3.0 and vCard v4.0. In such
cases, servers have to accept all media types that they advertise via
the CARDDAV:supported-address-data WebDAV property (see
Section 6.2.2).

However, clients can use standard HTTP content negotiation behavior
(the Accept request header defined in Section 14.1 of [RFC2616]) to
request that an address object resource’s data be returned in a
specific media type format. For example, a client merely capable of
handling vCard v3.0 would only want to have address object resources
returned in v3.0 format.*"

Source: RFC 6352 - 5.1.1

Same problem here on 0.16.14.

697 contacts, all stored as VERSION:4.0. On my iPhone the Cyrillic and
German names are unreadable (Дама shows as Ðама). Thunderbird shows
them fine, and the same contacts from my Nextcloud show fine on the same
iPhone. So I don’t think it’s the phone.

I checked the server and the data is correct - clean UTF-8, correct
Content-Type, same result with and without my proxy.

Then I tried one card twice: same phone, same address book, same Cyrillic
name. Stored as 3.0 it looks right. Stored as 4.0 it’s garbled. That was
the only difference.

So I converted all 697 cards to 3.0 and put them back. Removed and re-added
the account on the phone, and now everything is correct.

But it doesn’t stay fixed. New contacts from webmail are stored as 4.0, and
so are new ones from Thunderbird. Both come out broken on the phone again,
so I’d have to keep converting.

Could Stalwart serve 3.0 to clients that ask for it, or let me pick the
stored version per address book? Happy to test anything on my setup.

0.16.14, binary on Linux, RocksDB.

Fixed in v0.16.17 (yet to be released). Make sure your client is requesting the correct vCard version in the GET request headers.

Thanks! I upgraded to 0.16.17 and the Accept header negotiation works — a GET with Accept: text/vcard; version=3.0 now correctly returns 3.0.

The problem is that iOS never sends a GET. I captured the sync with tcpdump: it only uses sync-collection REPORT, sends Accept: /, and the request body contains a bare with no version attribute. So the server falls back to 4.0 and Apple Contacts shows mojibake for non-ASCII names.

As far as I can tell there’s no setting on the iOS side to change this. Would a fallback to 3.0 when no version is requested be an option, or a configurable default per address book?

VCard 3.0 is defined in a obsoleted RFC from September 1998. Stalwart is a modern server, and we can’t default to a 30 year old format that’s also obsolete. Unfortunately, you have two options: switch to a modern contacts client, or, if you want to keep using Apple clients, enter names with the ASCII substitutes for umlauts. For example, instead of Jürgen Müller enter Juergen Mueller so it survives Apple’s beloved 1998 format intact.

Q: which version of vcard 3.0 or 4.0 is the most common? show me which common client supports what.

Answer from Claude Opus 5:

vCard 3.0 is the de facto standard. It’s what you should emit if you don’t control both ends of the exchange. 4.0 (RFC 6350, 2011) is the newer and technically better spec, but adoption is uneven — most applications still default to exporting 3.0 for compatibility, and some older systems only understand 2.1. vCardify

A big structural reason for this: CardDAV (RFC 6352) mandates vCard 3.0 support and makes 4.0 optional, so essentially every sync server speaks 3.0 as its guaranteed common denominator.

Client / platform Exports Imports
Apple Contacts, iCloud 3.0 (macOS has a 2.1/3.0 toggle in prefs) 2.1/3.0 fine; 4.0 parses but drops unmapped props
Google Contacts 3.0 (the “vCard for iOS Contacts” option) 2.1/3.0/4.0
Outlook classic (Windows) 2.1 by default, with quoted-printable encoding for non-ASCII 2.1/3.0
New Outlook / OWA no VCF export at all — CSV only limited
Thunderbird 128+ stores contacts as vCard 4.0 internally and handles 4.0 imports well all three; 102 and earlier had limited 4.0 support
Android (AOSP/Samsung export) 2.1 2.1/3.0
Nextcloud / SabreDAV, Radicale stores 3.0 or 4.0; plain HTTP GET without an explicit Accept: text/vcard; version=4.0 returns 3.0 both
CRMs (Salesforce, HubSpot, etc.) usually CSV; VCF where offered is 3.0 3.0

Practical rules: write 3.0 for anything user-facing or cross-platform. Reach for 4.0 only when you need properties that don’t exist in 3.0 (KIND, GENDER, RELATED, MEMBER) or when you want jCard/xCard for an API. Always read the VERSION property per-card rather than assuming — a multi-contact .vcf can legally mix versions, and 2.1’s quoted-printable encoding will produce mojibake if you parse it as 3.0.

One data point before I drop this: Nextcloud handles the same address book both ways. A REPORT with a bare <address-data/> returns 691× VERSION:3.0, the identical request with version="4.0" returns 691× VERSION:4.0.

So it’s not really a matter of defaulting to an obsolete format — it serves 4.0 to anyone who asks, and only falls back to 3.0 when the client states no preference. That’s the behaviour I was hoping for, but I understand if you’d rather not go that route.

Thanks again for the quick replies and for looking into this.