Parse the file with ContentDocument.Parse and read each card through VCardDocument.Cards: names, emails, phones, addresses and dates come back typed and decoded. vCard 2.1, 3.0 and 4.0 all read the same way. The program below reads three contacts, one in each version.
dotnet new console -o ReadVcf && cd ReadVcf dotnet add package RecursiveAI.OneMail.Calendar --version 0.2.0-preview.2
Save this as contacts.vcf. The 2.1 card is quoted-printable UTF-8, as older phones and Outlook export it.
BEGIN:VCARD VERSION:3.0 FN:Ana Ruiz N:Ruiz;Ana;;; ORG:Example Corp;Sales TITLE:Account manager EMAIL;TYPE=INTERNET,WORK:[email protected] TEL;TYPE=WORK,VOICE:+1 555 0100 TEL;TYPE=CELL:+1 555 0199 ADR;TYPE=WORK:;;100 Main St;Springfield;IL;62701;USA BDAY:1988-04-12 END:VCARD BEGIN:VCARD VERSION:2.1 N;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:M=C3=BCller;J=C3=BCrgen FN;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:J=C3=BCrgen M=C3=BCller EMAIL;PREF;INTERNET:[email protected] TEL;HOME:+49 30 1234567 END:VCARD BEGIN:VCARD VERSION:4.0 FN:Sam Lee N:Lee;Sam;;; EMAIL;TYPE=home;PREF=1:[email protected] TEL;VALUE=uri;TYPE="voice,cell":tel:+1-555-0142 BDAY:--0615 END:VCARD
Put this in Program.cs and run dotnet run:
using RecursiveAI.OneMail.Calendar; using RecursiveAI.OneMail.Calendar.VCard; // vCard 2.1, 3.0 and 4.0 in one file; quoted-printable and CHARSET are decoded ContentDocument doc = ContentDocument.Parse(File.ReadAllBytes("contacts.vcf")); foreach (VCardView card in VCardDocument.Cards(doc)) { Console.WriteLine($"{card.FormattedName} (vCard {card.VersionText})"); if (card.Name is { } n) Console.WriteLine($" name: {Join(n.Given)} / {Join(n.Family)}"); if (card.Organization.Count > 0) { string org = string.Join(", ", card.Organization); Console.WriteLine($" org: {org}; {card.Title}"); } foreach (VCardTyped email in card.Emails) Console.WriteLine($" email: {email.Value} [{string.Join(",", email.Types)}]"); foreach (VCardTyped phone in card.Phones) Console.WriteLine($" phone: {phone.Value} [{string.Join(",", phone.Types)}]"); foreach (VCardAddress adr in card.Addresses) Console.WriteLine($" addr: {Join(adr.Street)}, {Join(adr.Locality)}"); // vCard 4.0 allows a birthday without a year if (card.Birthday is { } b) { string year = b.Year?.ToString() ?? "-"; Console.WriteLine($" bday: {year}-{b.Month:00}-{b.Day:00}"); } } static string Join(IReadOnlyList<string> parts) => string.Join(" ", parts);
Ana Ruiz (vCard 3.0) name: Ana / Ruiz org: Example Corp, Sales; Account manager email: [email protected] [INTERNET,WORK] phone: +1 555 0100 [WORK,VOICE] phone: +1 555 0199 [CELL] addr: 100 Main St, Springfield bday: 1988-04-12 Jürgen Müller (vCard 2.1) name: Jürgen / Müller email: [email protected] [PREF,INTERNET] phone: +49 30 1234567 [HOME] Sam Lee (vCard 4.0) name: Sam / Lee email: [email protected] [home] phone: tel:+1-555-0142 [voice,cell] bday: --06-15
The quoted-printable name decodes to “Jürgen Müller”. Type parameters are kept as written in each version (INTERNET,WORK, PREF, voice,cell). The 4.0 birthday has no year, which vCard 4.0 allows.
What you get
- Every field as a list.
VCardNameandVCardAddresshold lists, because a vCard field can have several values. - The rest of RFC 6350:
Photos(inline or by URL),Impps,Related,Categories,Note,Uidand more. - Lossless. An untouched file writes back byte for byte with
doc.Write(stream). - Outlook contacts.
RecursiveAI.OneMailreads contacts from MSG and PST files asMapiContact, and converts them to and from vCard with a loss report.
Limits
- A preview. 0.2.0-preview.2: the API can change before 1.0.
- Unchecked against Outlook. The project lists this gap: “no Outlook-produced contact PST or MSG with the full property set, and no Outlook import/export of vCards to compare against”.
- Phone and email slots. Outlook’s fixed slots (business phone, mobile, email 1 to 3) map to vCard only approximately. The conversion reports each such change (
PhoneSlotChanged,EmailSlotChanged).
Everything about contacts: Contacts. The parser: ICS and vCard.