# vCard 3.0 ↔ schema mapping

Source: SQLite (single source of truth). Target: vCard 3.0 (RFC 2426) for CardDAV `GET` and `addressbook-multiget` responses.

Why vCard 3.0 and not 4.0:

- vCard 3.0 is what EDS, KAddressBook, GNOME Contacts and Apple Contacts reliably emit and consume. vCard 4.0 (RFC 6350) is well-defined but unevenly supported by Linux desktop clients in 2026.
- The exotic features 4.0 brings (rich `KIND`, gender, language tags) are not used by our schema yet.
- We can revisit when EDS is reliably 4.0-fluent. Until then, 3.0 maximizes interop.

## Encoding rules

| Concern        | Rule                                                                                                |
| -------------- | --------------------------------------------------------------------------------------------------- |
| Line ending    | CRLF.                                                                                                |
| Charset        | UTF-8 (`CHARSET=UTF-8` is **not** emitted; vCard 3.0 implies UTF-8 when the file is UTF-8).         |
| Line folding   | Lines longer than 75 octets are folded by inserting `CRLF` followed by a single space. Folding happens between any two octets, but not in the middle of a UTF-8 multibyte sequence. |
| Field escaping | `\` → `\\`, `,` → `\,`, `;` → `\;`, newline → `\n`. Quoting is not used.                            |
| Required props | `BEGIN:VCARD`, `VERSION:3.0`, `FN:<display_name>`, `UID:<urn:uuid:...>`, `END:VCARD`. `FN` falls back to a placeholder `"Unknown"` if no name is set; `UID` uses the raw_contact's CardDAV UUID. |
| `PRODID`       | `-//Liberux//contacts-server 0.1//EN`. Emitted on every vCard.                                      |
| `REV`          | `raw_contacts.updated_at` formatted as `YYYYMMDDTHHMMSSZ` (UTC).                                    |

The vCard for a raw_contact is generated by walking its `data` rows in mimetype-priority order (`name`, `phone_v2`, `email_v2`, `postal-address_v2`, `organization`, `note`, `photo`, the rest), so the output is deterministic and the ETag stable across reads.

## Mimetype → vCard property table

### `…/name`

| Source field        | vCard                                       |
| ------------------- | ------------------------------------------- |
| `data1` (full)      | `FN:<value>`                                |
| `data3,2,5,4,6`     | `N:<family>;<given>;<middle>;<prefix>;<suffix>` |
| `data7..9` (phonetic) | `X-PHONETIC-LAST-NAME`, `X-PHONETIC-FIRST-NAME`, `X-PHONETIC-MIDDLE-NAME` (Apple-compatible) |

If `data1` is empty but the structured parts are populated, the server synthesizes `FN` as `"<given> <family>"` trimmed.

### `…/phone_v2`

| `data2` (`Phone.TYPE_*`) | vCard `TEL` `TYPE=`                                       |
| ------------------------ | --------------------------------------------------------- |
| 1 HOME                   | `HOME,VOICE`                                              |
| 2 MOBILE                 | `CELL,VOICE`                                              |
| 3 WORK                   | `WORK,VOICE`                                              |
| 4 FAX_WORK               | `WORK,FAX`                                                |
| 5 FAX_HOME               | `HOME,FAX`                                                |
| 6 PAGER                  | `PAGER`                                                   |
| 7 OTHER                  | `VOICE` (plain)                                           |
| 9 CALLBACK               | `VOICE,X-CALLBACK`                                        |
| 10 CAR                   | `CAR,VOICE`                                               |
| 11 COMPANY_MAIN          | `WORK,VOICE,X-COMPANY-MAIN`                               |
| 12 ISDN                  | `ISDN`                                                    |
| 13 MAIN                  | `VOICE,PREF`                                              |
| 14 OTHER_FAX             | `FAX`                                                     |
| 15 RADIO                 | `VOICE,X-RADIO`                                           |
| 17 TTY_TDD               | `VOICE,X-TTY`                                             |
| 18 WORK_MOBILE           | `WORK,CELL`                                               |
| 19 WORK_PAGER            | `WORK,PAGER`                                              |
| 20 ASSISTANT             | `VOICE,X-ASSISTANT`                                       |
| 21 MMS                   | `MSG,X-MMS`                                               |
| 0 CUSTOM                 | `X-CUSTOM-LABEL`; in addition `X-ABLABEL` group prefix carries `data3` (Apple convention). |

`is_super_primary = 1` adds `PREF` to the `TYPE=` parameter.

The body of the property is `data1` (display form). `data4` (E.164 normalized form) is emitted as `X-LIBERUX-NORMALIZED-NUMBER` immediately after the corresponding `TEL` for round-tripping; clients ignore unknown `X-` properties.

Custom-label encoding (Apple "labels" convention):

```
item1.TEL;TYPE=VOICE:+34 600 123 456
item1.X-ABLabel:Boss
```

Each custom-labeled property gets a unique `itemN.` group prefix.

### `…/email_v2`

| `data2` (`Email.TYPE_*`) | vCard `EMAIL` `TYPE=`            |
| ------------------------ | -------------------------------- |
| 1 HOME                   | `HOME,INTERNET`                  |
| 2 WORK                   | `WORK,INTERNET`                  |
| 3 OTHER                  | `INTERNET`                       |
| 4 MOBILE                 | `INTERNET,X-MOBILE`              |
| 0 CUSTOM                 | `INTERNET` + `X-ABLabel` group   |

Body: `data1` (the address).

### `…/postal-address_v2`

| `data2` (`StructuredPostal.TYPE_*`) | vCard `ADR` `TYPE=`             |
| ----------------------------------- | -------------------------------- |
| 1 HOME                              | `HOME`                           |
| 2 WORK                              | `WORK`                           |
| 3 OTHER                             | (none)                           |
| 0 CUSTOM                            | `X-ABLabel` group                |

Body: `ADR:<pobox>;<extended>;<street>;<city>;<region>;<postcode>;<country>` where `<extended>` is left empty (we have no `data` column for it). If a `formatted_address` (`data1`) is available it is emitted as `LABEL;TYPE=<same>:` after the `ADR`.

### `…/photo`

```
PHOTO;ENCODING=b;TYPE=<JPEG|PNG>:<base64 payload, folded>
```

We emit `ENCODING=b` (single-letter form, the RFC 2426 canonical token); some clients also accept the long form `BASE64` but `b` round-trips with EDS, KAddressBook, GNOME Contacts and Apple Contacts. The base64 payload follows the folded-line rule like every other property — there is no required blank line preceding it.

The `TYPE=` parameter is detected from the first bytes of the BLOB:
- `FF D8 FF` → `JPEG`
- `89 50 4E 47 0D 0A 1A 0A` → `PNG`
- otherwise omit `TYPE=`.

A blank line precedes the payload per RFC 2426 §4 and the payload is folded to 75-octet lines.

### `…/organization`

```
ORG:<company>;<department>
TITLE:<title>
ROLE:<job_description>            -- only if data6 is set
X-LIBERUX-OFFICE-LOCATION:<office_location>   -- only if data9 is set
```

### `…/note`

```
NOTE:<data1, escaped>
```

Multi-line notes are encoded with `\n` escapes per the rules above.

### `…/contact_event`

| `data2` (event type)  | vCard property                                |
| --------------------- | --------------------------------------------- |
| 3 BIRTHDAY            | `BDAY:<YYYY-MM-DD>` (or `BDAY;X-APPLE-OMIT-YEAR=1604:1604-MM-DD` for unknown-year). |
| 1 ANNIVERSARY         | `X-ANNIVERSARY:<YYYY-MM-DD>`                  |
| 2 OTHER               | `X-LIBERUX-EVENT;TYPE=OTHER:<date>`           |
| 0 CUSTOM              | `X-LIBERUX-EVENT;TYPE=<data3>:<date>`         |

Rule for unknown year (Android allows `--MM-DD`): emit using Apple's `X-APPLE-OMIT-YEAR=1604` convention with the year placeholder `1604`. EDS, KAddressBook and Apple Contacts all understand this.

### `…/im`

vCard 3.0 has `IMPP` (RFC 4770). We emit one line per IM handle:

| `data5` (`Im.PROTOCOL_*`) | vCard `IMPP`                                                        |
| ------------------------- | ------------------------------------------------------------------- |
| 0 AIM                     | `IMPP;X-SERVICE-TYPE=AIM:aim:<handle>`                              |
| 1 MSN                     | `IMPP;X-SERVICE-TYPE=MSN:msnim:<handle>`                            |
| 2 YAHOO                   | `IMPP;X-SERVICE-TYPE=Yahoo:ymsgr:<handle>`                          |
| 3 SKYPE                   | `IMPP;X-SERVICE-TYPE=Skype:skype:<handle>`                          |
| 4 QQ                      | `IMPP;X-SERVICE-TYPE=QQ:x-apple:<handle>`                           |
| 5 GTALK                   | `IMPP;X-SERVICE-TYPE=GoogleTalk:xmpp:<handle>`                      |
| 6 ICQ                     | `IMPP;X-SERVICE-TYPE=ICQ:aim:<handle>`                              |
| 7 JABBER                  | `IMPP;X-SERVICE-TYPE=Jabber:xmpp:<handle>`                          |
| 8 NETMEETING              | `IMPP;X-SERVICE-TYPE=NetMeeting:<handle>`                           |
| -1 CUSTOM                 | `IMPP;X-SERVICE-TYPE=<data6>:<handle>`                              |

For round-trip safety, the original `data5` integer is also emitted as `X-LIBERUX-IM-PROTOCOL:<n>`.

### `…/website`

```
URL:<data1>
```

If `data2` ≠ OTHER and ≠ HOMEPAGE, append a `TYPE=` parameter (`HOME`, `WORK`, `BLOG` → `X-BLOG`, `PROFILE` → `X-PROFILE`, `FTP` → `X-FTP`). For CUSTOM, use the `X-ABLabel` group.

### `…/nickname`

```
NICKNAME:<data1>
```

`data2` (DEFAULT/OTHER_NAME/MAIDEN_NAME/SHORT_NAME/INITIALS) is preserved as `X-LIBERUX-NICKNAME-TYPE:<n>` immediately after the `NICKNAME` line, since vCard 3.0 has no standardized type for nicknames.

## Unknown / custom mimetypes

For any `data.mimetype` not in the supported list above, we serialize as:

```
X-LIBERUX-<UPPER_NAME>:<data1>
```

where `<UPPER_NAME>` is derived from the mimetype tail (`vnd.android.cursor.item/foo_bar` → `FOO_BAR`). If the mimetype's data uses more than `data1`, additional fields are emitted as separate `X-LIBERUX-<UPPER_NAME>-<N>:<value>` lines (`<N>` = 2..15). This guarantees lossless round-trip through the vCard layer for any mimetype Android invents that we have not yet first-classed.

## TYPE / LABEL handling (data2 / data3)

A pattern repeats across `phone_v2`, `email_v2`, `postal-address_v2`, `website`, `im`, `nickname`, `organization`, `contact_event`:

1. `data2` is an integer constant from `ContactsContract.CommonDataKinds.<X>.TYPE_*`.
2. `data3` is a custom label string, used **only** when `data2 == 0` (TYPE_CUSTOM).

vCard mapping:

- For each non-CUSTOM constant we use a fixed `TYPE=` value as listed in the per-mimetype tables above.
- For CUSTOM, we use Apple's grouped-property convention:

  ```
  item1.TEL;TYPE=VOICE:+34 600 123 456
  item1.X-ABLabel:Manager
  ```

  This lets EDS/KAddressBook/Apple Contacts surface "Manager" as the displayed label without inventing a non-standard `TYPE=` value. Group prefixes (`item1`, `item2`, …) are allocated server-side and reset per vCard.

For mimetypes whose `TYPE_*` constants have no clean vCard equivalent (Phone's `RADIO`, `CALLBACK`, `ASSISTANT`, etc.), we use `X-` parameter values rather than the grouped `X-ABLabel` approach. These survive round-trips because we also keep the original integer `data2` in `X-LIBERUX-<MIMETYPE>-TYPE-CODE`. For example a Phone with `data2 = 9` (CALLBACK) emits:

```
TEL;TYPE=VOICE,X-CALLBACK:+34 600 555 888
X-LIBERUX-PHONE-V2-TYPE-CODE:9
```

The `X-LIBERUX-…-TYPE-CODE` line is informational; v1 readers only need to consume it when re-importing a vCard we exported (round-trip).

## Determinism

For a stable ETag, vCard generation is deterministic:

1. Properties are emitted in a fixed order: `BEGIN`, `VERSION`, `PRODID`, `UID`, `FN`, `N`, `NICKNAME`, `ORG`, `TITLE`, `ROLE`, `EMAIL*`, `TEL*`, `ADR*`, `IMPP*`, `URL*`, `BDAY`, `X-ANNIVERSARY`, `X-LIBERUX-EVENT*`, `NOTE`, `PHOTO`, `REV`, `END`.
2. Within a multi-value property family (e.g. `TEL`), entries are sorted by `(is_super_primary DESC, is_primary DESC, data._id ASC)`.
3. Group prefixes (`item1`, `item2`, …) are allocated in the order properties appear.
4. Folding always inserts the same column (`> 75 octets`) and uses a single space continuation.

Any tweak to this list of rules requires bumping the ETag generation prefix (`liberux-1-` → `liberux-2-`; see `carddav.md`).

## What is NOT mapped

- Group memberships (the `groups` / `group_membership` tables) are **not** emitted in vCards in v1. EDS represents groups via `CATEGORIES` but the cross-walk is lossy and v1 has no UI for groups. v2 will add `CATEGORIES:<group_titles>`.
- `…/relation`, `…/sip_address`, `…/identity` mimetypes from Android are not in the v1 mandatory mimetype list — they fall through to the `X-LIBERUX-<NAME>` fallback if encountered.
- Calls / SMS / call log are out of scope (separate daemon).
