vCard has ContactCard/parse, so the existing import uploads a blob and lets the server do the work. There is no LDIF equivalent, so the file is read in the browser — and split into the two problems it actually is:
web/src/lib/ldif.ts — RFC 2849 and nothing else. Folded lines, :: base64 values, case-insensitive attribute names, ; options, comments, version: headers, changetype records, CRLF. It knows no attribute by name.
web/src/lib/mozillaAb.ts — the attributes and no syntax. Mozilla's address book schema, the one you named, which is what Thunderbird and SOGo write.
LDIF is only a syntax — it says how to write name: value and nothing about what any name means — so a file is only readable against a schema. Keeping the two apart is what would let a second schema be added later without touching the reader.
What comes across
Work and home addresses (the schema keeps them in two separate attribute sets, and they become two addresses with the right contexts), every phone kind — telephoneNumber work, homePhone home, mobile, facsimileTelephoneNumber fax, pager — second email, organisation and its ou units, job title, nickname, both web pages, and the AIM handle.
The four mozillaCustom fields have no JSContact equivalent. Rather than drop them they're appended to the note, labelled the way Thunderbird labels them — keeping something somebody chose to write down beats the tidiness of losing it. Say the word if you'd rather they vanished.
The schema has no birthday attribute, so there is nothing to map there.
Two calls worth noting
An entry with neither a name nor an email is skipped, not imported. It would land as a blank row: present in the list, impossible to identify, tedious to find again to delete.
The dn is not used as the contact's uid. A distinguished name says where an entry sat in somebody else's directory; it is not an identity anywhere else. Each card gets a fresh uid, as the vCard path does.
One control, two formats
Import vCard is now Import contacts, accepting .vcf/.ldif/.ldi and friends, and it decides by what is in the file rather than what it is called — an address book exported as LDIF turns up as .ldif, .ldi, .txt or with no extension at all. (This orphans the "Import vCard" translation in nine catalogues; they fall back to English until retranslated.)
Verification
npm run typecheck, npm test (550 pass, 37 new) and npm run build green. The parser tests cover the fiddly half of RFC 2849 — folding, non-ASCII base64, a value that won't decode, URL references, comments, CRLF, changetype — and a file that isn't LDIF at all.
Driven by hand against dev:mock, importing a file built from your sample entry plus a richer second one:
Imported 2 contacts from three entries — the nameless third correctly skipped
Jane Doe came out with the work phone and mobile told apart, both addresses, and the description as her note — exactly the entry in the issue
Grace Hopper came out with title and company in the header, nickname, both emails, Example Corp · Research, the website, and Custom 1: Met at a conference in the notes
a vCard imported through the same control still works — Imported 1 contact
Screenshot of an imported card attached in the conversation.
Closes #174.
## Why this one is different from the vCard import
vCard has `ContactCard/parse`, so the existing import uploads a blob and lets the server do the work. **There is no LDIF equivalent**, so the file is read in the browser — and split into the two problems it actually is:
- **`web/src/lib/ldif.ts`** — RFC 2849 and nothing else. Folded lines, `::` base64 values, case-insensitive attribute names, `;` options, comments, `version:` headers, `changetype` records, CRLF. It knows no attribute by name.
- **`web/src/lib/mozillaAb.ts`** — the attributes and no syntax. [Mozilla's address book schema](https://wiki.mozilla.org/MailNews:Mozilla_LDAP_Address_Book_Schema), the one you named, which is what Thunderbird and SOGo write.
LDIF is only a syntax — it says how to write `name: value` and nothing about what any name means — so a file is only readable against a schema. Keeping the two apart is what would let a second schema be added later without touching the reader.
## What comes across
Work and home addresses (the schema keeps them in two separate attribute sets, and they become two addresses with the right contexts), every phone kind — `telephoneNumber` work, `homePhone` home, `mobile`, `facsimileTelephoneNumber` fax, `pager` — second email, organisation and its `ou` units, job title, nickname, both web pages, and the AIM handle.
The four `mozillaCustom` fields have no JSContact equivalent. Rather than drop them they're appended to the note, labelled the way Thunderbird labels them — keeping something somebody chose to write down beats the tidiness of losing it. Say the word if you'd rather they vanished.
The schema has **no birthday attribute**, so there is nothing to map there.
## Two calls worth noting
- **An entry with neither a name nor an email is skipped**, not imported. It would land as a blank row: present in the list, impossible to identify, tedious to find again to delete.
- **The `dn` is not used as the contact's `uid`.** A distinguished name says where an entry sat in somebody else's directory; it is not an identity anywhere else. Each card gets a fresh uid, as the vCard path does.
## One control, two formats
`Import vCard` is now `Import contacts`, accepting `.vcf`/`.ldif`/`.ldi` and friends, and it decides by **what is in the file** rather than what it is called — an address book exported as LDIF turns up as `.ldif`, `.ldi`, `.txt` or with no extension at all. (This orphans the `"Import vCard"` translation in nine catalogues; they fall back to English until retranslated.)
## Verification
`npm run typecheck`, `npm test` (550 pass, **37 new**) and `npm run build` green. The parser tests cover the fiddly half of RFC 2849 — folding, non-ASCII base64, a value that won't decode, URL references, comments, CRLF, `changetype` — and a file that isn't LDIF at all.
Driven by hand against `dev:mock`, importing a file built from your sample entry plus a richer second one:
- **Imported 2 contacts** from three entries — the nameless third correctly skipped
- Jane Doe came out with the work phone and mobile told apart, both addresses, and the description as her note — exactly the entry in the issue
- Grace Hopper came out with title and company in the header, nickname, both emails, `Example Corp · Research`, the website, and `Custom 1: Met at a conference` in the notes
- a vCard imported through the same control still works — **Imported 1 contact**
Screenshot of an imported card attached in the conversation.
**Merged** 2026-09-01 as coffey-labs/ihasmail@df9349935d69
<sub>Rebuilt from: git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #174.
Why this one is different from the vCard import
vCard has
ContactCard/parse, so the existing import uploads a blob and lets the server do the work. There is no LDIF equivalent, so the file is read in the browser — and split into the two problems it actually is:web/src/lib/ldif.ts— RFC 2849 and nothing else. Folded lines,::base64 values, case-insensitive attribute names,;options, comments,version:headers,changetyperecords, CRLF. It knows no attribute by name.web/src/lib/mozillaAb.ts— the attributes and no syntax. Mozilla's address book schema, the one you named, which is what Thunderbird and SOGo write.LDIF is only a syntax — it says how to write
name: valueand nothing about what any name means — so a file is only readable against a schema. Keeping the two apart is what would let a second schema be added later without touching the reader.What comes across
Work and home addresses (the schema keeps them in two separate attribute sets, and they become two addresses with the right contexts), every phone kind —
telephoneNumberwork,homePhonehome,mobile,facsimileTelephoneNumberfax,pager— second email, organisation and itsouunits, job title, nickname, both web pages, and the AIM handle.The four
mozillaCustomfields have no JSContact equivalent. Rather than drop them they're appended to the note, labelled the way Thunderbird labels them — keeping something somebody chose to write down beats the tidiness of losing it. Say the word if you'd rather they vanished.The schema has no birthday attribute, so there is nothing to map there.
Two calls worth noting
dnis not used as the contact'suid. A distinguished name says where an entry sat in somebody else's directory; it is not an identity anywhere else. Each card gets a fresh uid, as the vCard path does.One control, two formats
Import vCardis nowImport contacts, accepting.vcf/.ldif/.ldiand friends, and it decides by what is in the file rather than what it is called — an address book exported as LDIF turns up as.ldif,.ldi,.txtor with no extension at all. (This orphans the"Import vCard"translation in nine catalogues; they fall back to English until retranslated.)Verification
npm run typecheck,npm test(550 pass, 37 new) andnpm run buildgreen. The parser tests cover the fiddly half of RFC 2849 — folding, non-ASCII base64, a value that won't decode, URL references, comments, CRLF,changetype— and a file that isn't LDIF at all.Driven by hand against
dev:mock, importing a file built from your sample entry plus a richer second one:Example Corp · Research, the website, andCustom 1: Met at a conferencein the notesScreenshot of an imported card attached in the conversation.
Merged 2026-09-01 as coffey-labs/ihasmail@df9349935d
Rebuilt from: git history, session transcript.