From 578928a9cdedd1437e3ddbb227eb350cb87ae04a Mon Sep 17 00:00:00 2001 From: Aptivi CEO Date: Sat, 16 Dec 2023 15:01:18 +0300 Subject: [PATCH] add - doc - Added support for vCard 5.0 files --- We've added support for our own version of vCard 4.0, dubbed vCard 5.0, to VisualCard's parser. Check out the specs in the vcard-50-aptivi.txt file. --- Type: add Breaking: False Doc Required: True Part: 1/1 --- VisualCard.ShowContacts/Program.cs | 3 + VisualCard.Tests/ContactData.cs | 261 +++++++++++ VisualCard/CardTools.cs | 2 +- VisualCard/Parsers/Five/VcardFive.cs | 660 +++++++++++++++++++++++++++ VisualCard/Parsers/Four/VcardFour.cs | 13 - VisualCard/Parsers/VcardConstants.cs | 18 +- VisualCard/Parts/AddressInfo.cs | 9 + VisualCard/Parts/EmailInfo.cs | 9 + VisualCard/Parts/GeoInfo.cs | 9 + VisualCard/Parts/ImppInfo.cs | 9 + VisualCard/Parts/LabelAddressInfo.cs | 60 ++- VisualCard/Parts/LogoInfo.cs | 6 + VisualCard/Parts/NameInfo.cs | 9 + VisualCard/Parts/NicknameInfo.cs | 9 + VisualCard/Parts/OrganizationInfo.cs | 9 + VisualCard/Parts/PhotoInfo.cs | 6 + VisualCard/Parts/RoleInfo.cs | 9 + VisualCard/Parts/SoundInfo.cs | 6 + VisualCard/Parts/TelephoneInfo.cs | 9 + VisualCard/Parts/TimeZoneInfo.cs | 9 + VisualCard/Parts/TitleInfo.cs | 9 + VisualCard/Parts/XNameInfo.cs | 6 + VisualCard/Specs/vcard-50-aptivi.txt | 203 ++++++++ 23 files changed, 1314 insertions(+), 29 deletions(-) create mode 100644 VisualCard/Parsers/Five/VcardFive.cs create mode 100644 VisualCard/Specs/vcard-50-aptivi.txt diff --git a/VisualCard.ShowContacts/Program.cs b/VisualCard.ShowContacts/Program.cs index 34148f5..796d0eb 100644 --- a/VisualCard.ShowContacts/Program.cs +++ b/VisualCard.ShowContacts/Program.cs @@ -86,6 +86,7 @@ static void Main(string[] args) } // Show contact information + bool showVcard5Disclaimer = Contacts.Any((card) => card.CardVersion == "5.0"); foreach (Card Contact in Contacts) { TextWriterColor.WriteColor("----------------------------", ConsoleColors.Green); @@ -181,6 +182,8 @@ static void Main(string[] args) ); TextWriterColor.Write(raw); } + if (showVcard5Disclaimer) + TextWriterColor.WriteColor("This application uses vCard 5.0, a revised version of vCard 4.0, made by Aptivi.", ConsoleColors.Gray); } } } diff --git a/VisualCard.Tests/ContactData.cs b/VisualCard.Tests/ContactData.cs index 4b8bfcd..c97d279 100644 --- a/VisualCard.Tests/ContactData.cs +++ b/VisualCard.Tests/ContactData.cs @@ -134,6 +134,31 @@ public static class ContactData }; #endregion + #region singleVcardFiveContactShort + private static readonly string singleVcardFiveContactShort = + """ + BEGIN:VCARD + VERSION:5.0 + N:Hood;Rick;;; + FN:Rick Hood + END:VCARD + """ + ; + + private static readonly Card singleVcardFiveContactShortInstance = new + ( + null, + "5.0" + ) + { + ContactNames = + [ + new NameInfo(0, [], "Rick", "Hood", [], [], []) + ], + ContactFullName = "Rick Hood" + }; + #endregion + #region singleVcardTwoContact private static readonly string singleMeCardContact = """ @@ -376,6 +401,74 @@ public static class ContactData }; #endregion + #region singleVcardFiveContact + private static readonly string singleVcardFiveContact = + """ + BEGIN:VCARD + VERSION:5.0 + ADR;TYPE=home:;;Los Angeles\, USA;;;; + EMAIL;TYPE=HOME:john.s@acme.co + FN:John Sanders + IMPP:aim:john.s + N:Sanders;John;;; + NOTE:Note test for VisualCard + ORG:Acme Co. + TEL;TYPE=cell:495-522-3560 + TITLE:Product Manager + X-ANDROID-CUSTOM:vnd.android.cursor.item/nickname;JS;1;;;;;;;;;;;;; + X-PHONETIC-FIRST-NAME:Saunders + X-PHONETIC-LAST-NAME:John + SORT-STRING:johnsanders + END:VCARD + """ + ; + + private static readonly Card singleVcardFiveContactInstance = new + ( + null, + "5.0" + ) + { + ContactNames = + [ + new NameInfo(0, [], "John", "Sanders", [], [], []) + ], + ContactFullName = "John Sanders", + ContactTelephones = + [ + new TelephoneInfo(0, [], ["cell"], "495-522-3560") + ], + ContactAddresses = + [ + new AddressInfo(0, [], ["home"], "", "", "Los Angeles, USA", "", "", "", "") + ], + ContactOrganizations = + [ + new OrganizationInfo(0, [], "Acme Co.", "", "", ["WORK"]) + ], + ContactTitles = + [ + new TitleInfo(0, [], "Product Manager") + ], + ContactNotes = "Note test for VisualCard", + ContactMails = + [ + new EmailInfo(0, [], ["HOME"], "john.s@acme.co") + ], + ContactXNames = + [ + new XNameInfo(0, [], "ANDROID-CUSTOM", ["vnd.android.cursor.item/nickname", "JS", "1", "", "", "", "", "", "", "", "", "", "", "", "", ""], []), + new XNameInfo(0, [], "PHONETIC-FIRST-NAME", ["Saunders"], []), + new XNameInfo(0, [], "PHONETIC-LAST-NAME", ["John"], []) + ], + ContactImpps = + [ + new ImppInfo(0, [], "aim:john.s", ["HOME"]) + ], + ContactSortString = "johnsanders" + }; + #endregion + #region multipleVcardTwoContacts private static readonly string multipleVcardTwoContacts = """ @@ -833,6 +926,165 @@ public static class ContactData private static readonly Card multipleVcardFourContactsInstanceFour = singleVcardFourContactInstance; #endregion + #region multipleVcardFiveContacts + private static readonly string multipleVcardFiveContacts = + """ + BEGIN:VCARD + VERSION:5.0 + FN:Rick Hood + N:Hood;Rick;;; + END:VCARD + + BEGIN:VCARD + VERSION:5.0 + ADR;TYPE=work:POBOX;;Street Address ExtAddress;Reg;Loc;Postal;Country + ADR;TYPE=home:;;Street Address;;;; + EMAIL;TYPE=HOME:neville.nvs@gmail.com + EMAIL;TYPE=WORK:neville.nvs@nvsc.com + FN:Neville Navasquillo + IMPP;TYPE=HOME:aim:IM + IMPP;TYPE=HOME:msn:Windows LIVE + IMPP;TYPE=HOME:ymsgr:Yahoo + N:Navasquillo;Neville;Neville\,Nevile;Mr.;Jr. + N;ALTID=0;LANGUAGE=de:NAVASQUILLO;Neville;Neville\,Nevile;Mr.;Jr. + NOTE:Notes + ORG:Organization + TEL;TYPE=work:098-765-4321 + TEL;TYPE=cell:1-234-567-890 + TEL;TYPE=voice:078-494-6434 + TEL;TYPE=home:348-404-8404 + TITLE:Title + X-ANDROID-CUSTOM:vnd.android.cursor.item/nickname;NVL.N;1;;;;;;;;;;;;; + END:VCARD + + BEGIN:VCARD + VERSION:5.0 + ADR;TYPE=home:;;New York\, USA;;;; + EMAIL;TYPE=HOME:sarah.s@gmail.com + EMAIL;TYPE=WORK:sarah.s@sso.org + FN:Sarah Santos + N:Santos;Sarah;;; + ORG:Support Scammer Outcry Organization + TEL;TYPE=cell:589-210-1059 + TITLE:Chief Executive Officer + URL:https://sso.org/ + BDAY:19890222 + X-SIP-SIP:sip test + SORT-STRING:sarahsantos + END:VCARD + + BEGIN:VCARD + VERSION:5.0 + ADR;TYPE=home:;;Los Angeles\, USA;;;; + EMAIL;TYPE=HOME:john.s@acme.co + FN:John Sanders + IMPP:aim:john.s + N:Sanders;John;;; + NOTE:Note test for VisualCard + ORG:Acme Co. + TEL;TYPE=cell:495-522-3560 + TITLE:Product Manager + X-ANDROID-CUSTOM:vnd.android.cursor.item/nickname;JS;1;;;;;;;;;;;;; + X-PHONETIC-FIRST-NAME:Saunders + X-PHONETIC-LAST-NAME:John + SORT-STRING:johnsanders + END:VCARD + """ + ; + + private static readonly Card multipleVcardFiveContactsInstanceOne = singleVcardFiveContactShortInstance; + private static readonly Card multipleVcardFiveContactsInstanceTwo = new + ( + null, + "5.0" + ) + { + ContactNames = + [ + new NameInfo(0, [], "Neville", "Navasquillo", ["Neville", "Nevile"], ["Mr."], ["Jr."]), + new NameInfo(0, ["LANGUAGE=de"], "Neville", "NAVASQUILLO", ["Neville", "Nevile"], ["Mr."], ["Jr."]) + ], + ContactFullName = "Neville Navasquillo", + ContactTelephones = + [ + new TelephoneInfo(0, [], ["work"], "098-765-4321"), + new TelephoneInfo(0, [], ["cell"], "1-234-567-890"), + new TelephoneInfo(0, [], ["voice"], "078-494-6434"), + new TelephoneInfo(0, [], ["home"], "348-404-8404"), + ], + ContactAddresses = + [ + new AddressInfo(0, [], ["work"], "POBOX", "", "Street Address ExtAddress", "Reg", "Loc", "Postal", "Country"), + new AddressInfo(0, [], ["home"], "", "", "Street Address", "", "", "", ""), + ], + ContactOrganizations = + [ + new OrganizationInfo(0, [], "Organization", "", "", ["WORK"]) + ], + ContactTitles = + [ + new TitleInfo(0, [], "Title") + ], + ContactNotes = "Notes", + ContactMails = + [ + new EmailInfo(0, [], ["HOME"], "neville.nvs@gmail.com"), + new EmailInfo(0, [], ["WORK"], "neville.nvs@nvsc.com"), + ], + ContactXNames = + [ + new XNameInfo(0, [], "ANDROID-CUSTOM", ["vnd.android.cursor.item/nickname", "NVL.N", "1", "", "", "", "", "", "", "", "", "", "", "", "", ""], []), + ], + ContactImpps = + [ + new ImppInfo(0, [], "aim:IM", ["HOME"]), + new ImppInfo(0, [], "msn:Windows LIVE", ["HOME"]), + new ImppInfo(0, [], "ymsgr:Yahoo", ["HOME"]) + ], + }; + private static readonly Card multipleVcardFiveContactsInstanceThree = new + ( + null, + "5.0" + ) + { + ContactNames = + [ + new NameInfo(0, [], "Sarah", "Santos", [], [], []) + ], + ContactFullName = "Sarah Santos", + ContactTelephones = + [ + new TelephoneInfo(0, [], ["cell"], "589-210-1059") + ], + ContactAddresses = + [ + new AddressInfo(0, [], ["home"], "", "", "New York, USA", "", "", "", "") + ], + ContactOrganizations = + [ + new OrganizationInfo(0, [], "Support Scammer Outcry Organization", "", "", ["WORK"]) + ], + ContactTitles = + [ + new TitleInfo(0, [], "Chief Executive Officer") + ], + ContactURL = "https://sso.org/", + ContactMails = + [ + new EmailInfo(0, [], ["HOME"], "sarah.s@gmail.com"), + new EmailInfo(0, [], ["WORK"], "sarah.s@sso.org"), + ], + ContactXNames = + [ + new XNameInfo(0, [], "SIP-SIP", ["sip test"], []), + ], + ContactBirthdate = new DateTime(1989, 2, 22), + ContactSortString = "sarahsantos" + }; + private static readonly Card multipleVcardFiveContactsInstanceFour = singleVcardFiveContactInstance; + #endregion + #region vcardThreeOldSample private static readonly string vcardThreeOldSample = """ @@ -949,6 +1201,7 @@ public static class ContactData singleVcardTwoContactShort, singleVcardThreeContactShort, singleVcardFourContactShort, + singleVcardFiveContactShort, ]; /// @@ -959,6 +1212,7 @@ public static class ContactData singleVcardTwoContact, singleVcardThreeContact, singleVcardFourContact, + singleVcardFiveContact, ]; /// @@ -969,6 +1223,7 @@ public static class ContactData multipleVcardTwoContacts, multipleVcardThreeContacts, multipleVcardFourContacts, + multipleVcardFiveContacts, ]; /// @@ -1005,9 +1260,11 @@ public static readonly (string, string)[] vCardFromMeCardContacts = singleVcardTwoContactShortInstance, singleVcardThreeContactShortInstance, singleVcardFourContactShortInstance, + singleVcardFiveContactShortInstance, singleVcardTwoContactInstance, singleVcardThreeContactInstance, singleVcardFourContactInstance, + singleVcardFiveContactInstance, multipleVcardTwoContactsInstanceOne, multipleVcardTwoContactsInstanceTwo, multipleVcardTwoContactsInstanceThree, @@ -1020,6 +1277,10 @@ public static readonly (string, string)[] vCardFromMeCardContacts = multipleVcardFourContactsInstanceTwo, multipleVcardFourContactsInstanceThree, multipleVcardFourContactsInstanceFour, + multipleVcardFiveContactsInstanceOne, + multipleVcardFiveContactsInstanceTwo, + multipleVcardFiveContactsInstanceThree, + multipleVcardFiveContactsInstanceFour, vcardThreeOldSampleInstanceOne, vcardThreeOldSampleInstanceTwo, vcardThreeOldSampleInstanceThree, diff --git a/VisualCard/CardTools.cs b/VisualCard/CardTools.cs index c91ff1b..20894dd 100644 --- a/VisualCard/CardTools.cs +++ b/VisualCard/CardTools.cs @@ -111,7 +111,7 @@ public static List GetCardParsers(StreamReader stream) if (!CardSawNull) CardLine = stream.ReadLine(); CardSawNull = false; - if (CardLine != "VERSION:2.1" && CardLine != "VERSION:3.0" && CardLine != "VERSION:4.0" && !VersionSpotted) + if (CardLine != "VERSION:2.1" && CardLine != "VERSION:3.0" && CardLine != "VERSION:4.0" && CardLine != "VERSION:5.0" && !VersionSpotted) throw new InvalidDataException($"This has an invalid VCard version {CardLine}."); else if (!VersionSpotted) { diff --git a/VisualCard/Parsers/Five/VcardFive.cs b/VisualCard/Parsers/Five/VcardFive.cs new file mode 100644 index 0000000..685aca7 --- /dev/null +++ b/VisualCard/Parsers/Five/VcardFive.cs @@ -0,0 +1,660 @@ +// +// MIT License +// +// Copyright (c) 2021-2024 Aptivi +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using VisualCard.Exceptions; +using VisualCard.Parts; +using TimeZoneInfo = VisualCard.Parts.TimeZoneInfo; + +namespace VisualCard.Parsers.Five +{ + /// + /// Parser for VCard version 5.0. Consult the vcard-40-rfc6350.txt file in source for the specification. + /// + public class VcardFive : BaseVcardParser, IVcardParser + { + /// + public override string CardContent { get; } + /// + public override string CardVersion { get; } + + /// + public override Card Parse() + { + // Check the version to ensure that we're really dealing with VCard 5.0 contact + if (CardVersion != "5.0") + throw new InvalidDataException($"Card version {CardVersion} doesn't match expected \"5.0\"."); + + // Check the content to ensure that we really have data + if (string.IsNullOrEmpty(CardContent)) + throw new InvalidDataException($"Card content is empty."); + + // Now, make a stream out of card content + byte[] CardContentData = Encoding.Default.GetBytes(CardContent); + MemoryStream CardContentStream = new(CardContentData, false); + StreamReader CardContentReader = new(CardContentStream); + + // Some variables to assign to the Card() ctor + string _kind = "individual"; + string _fullName = ""; + string _url = ""; + string _note = ""; + string _prodId = ""; + string _sortString = ""; + string _source = ""; + string _fbUrl = ""; + string _calUri = ""; + string _caladrUri = ""; + string _class = ""; + string _mailer = ""; + DateTime _rev = DateTime.MinValue; + DateTime _bday = DateTime.MinValue; + List _names = []; + List _telephones = []; + List _emails = []; + List _addresses = []; + List _labels = []; + List _orgs = []; + List _titles = []; + List _logos = []; + List _photos = []; + List _sounds = []; + List _nicks = []; + List _roles = []; + List _categories = []; + List _timezones = []; + List _geos = []; + List _impps = []; + List _xes = []; + + // Name and Full Name specifiers are required + bool nameSpecifierSpotted = false; + bool fullNameSpecifierSpotted = false; + + // Flags + bool idReservedForName = false; + + // Iterate through all the lines + int lineNumber = 0; + while (!CardContentReader.EndOfStream) + { + // Get line + string _value = CardContentReader.ReadLine(); + lineNumber += 1; + + // Check for type + bool isWithType = false; + var valueSplit = VcardParserTools.SplitToKeyAndValueFromString(_value); + if (valueSplit[0].Contains(";")) + isWithType = true; + var delimiter = isWithType ? VcardConstants._fieldDelimiter : VcardConstants._argumentDelimiter; + + try + { + // Variables + string[] splitValueParts = _value.Split(VcardConstants._argumentDelimiter); + string[] splitArgs = splitValueParts[0].Split(VcardConstants._fieldDelimiter); + splitArgs = splitArgs.Except(new string[] { splitArgs[0] }).ToArray(); + string[] splitValues = splitValueParts[1].Split(VcardConstants._fieldDelimiter); + List finalArgs = []; + int altId = 0; + + if (splitArgs.Length > 0) + { + // If we have more than one argument, check for ALTID + if (splitArgs[0].StartsWith(VcardConstants._altIdArgumentSpecifier)) + { + if (!int.TryParse(splitArgs[0].Substring(VcardConstants._altIdArgumentSpecifier.Length), out altId)) + throw new InvalidDataException("ALTID must be numeric"); + + // Here, we require arguments for ALTID + if (splitArgs.Length <= 1) + throw new InvalidDataException("ALTID must have one or more arguments to specify why is this instance an alternative"); + } + + // Finalize the arguments + finalArgs.AddRange(splitArgs.Except( + splitArgs.Where((arg) => + arg.StartsWith(VcardConstants._altIdArgumentSpecifier) || + arg.StartsWith(VcardConstants._valueArgumentSpecifier) || + arg.StartsWith(VcardConstants._typeArgumentSpecifier) + ) + )); + } + + // Card type (KIND:individual, KIND:group, KIND:org, KIND:location, ...) + // Here, we don't support ALTID. + if (_value.StartsWith(VcardConstants._kindSpecifier + delimiter)) + { + // Get the value + string kindValue = _value.Substring(VcardConstants._kindSpecifier.Length + 1); + + // Populate field + if (!string.IsNullOrEmpty(kindValue)) + _kind = Regex.Unescape(kindValue); + } + + // The name (N:Sanders;John;;; or N;ALTID=1;LANGUAGE=en:Sanders;John;;;) + // ALTID is supported. + if (_value.StartsWith(VcardConstants._nameSpecifier + delimiter)) + { + // Get the name + if (isWithType) + _names.Add(NameInfo.FromStringVcardFiveWithType(_value, splitArgs, finalArgs, altId, _names, idReservedForName)); + else + _names.Add(NameInfo.FromStringVcardFive(splitValues, idReservedForName)); + + // Set flag to indicate that the required field is spotted + nameSpecifierSpotted = true; + + // Since we've reserved id 0, set the flag + idReservedForName = true; + } + + // Full name (FN:John Sanders) + // Here, we don't support ALTID. + if (_value.StartsWith(VcardConstants._fullNameSpecifier + delimiter)) + { + // Get the value + string fullNameValue = _value.Substring(VcardConstants._fullNameSpecifier.Length + 1); + + // Populate field + _fullName = Regex.Unescape(fullNameValue); + + // Set flag to indicate that the required field is spotted + fullNameSpecifierSpotted = true; + } + + // Telephone (TEL;CELL;TYPE=HOME:495-522-3560 or TEL;TYPE=cell,home:495-522-3560 or TEL:495-522-3560) + // Type is supported + if (_value.StartsWith(VcardConstants._telephoneSpecifier + delimiter)) + { + if (isWithType) + _telephones.Add(TelephoneInfo.FromStringVcardFiveWithType(_value, finalArgs, altId)); + else + _telephones.Add(TelephoneInfo.FromStringVcardFive(_value, altId)); + } + + // Address (ADR;TYPE=HOME:;;Los Angeles, USA;;;; or ADR:;;Los Angeles, USA;;;;) + if (_value.StartsWith(VcardConstants._addressSpecifier + delimiter)) + { + if (isWithType) + _addresses.Add(AddressInfo.FromStringVcardFiveWithType(_value, finalArgs, altId)); + else + _addresses.Add(AddressInfo.FromStringVcardFive(_value, altId)); + } + + // Label (LABEL;TYPE=dom,home,postal,parcel:Mr.John Q. Public\, Esq.\nMail Drop: TNE QB\n123 Main Street\nAny Town\, CA 91921 - 1234\nU.S.A.) + if (_value.StartsWith(VcardConstants._labelSpecifier + delimiter)) + { + if (isWithType) + _labels.Add(LabelAddressInfo.FromStringVcardFiveWithType(_value, finalArgs, altId)); + else + _labels.Add(LabelAddressInfo.FromStringVcardFive(_value, altId)); + } + + // Email (EMAIL;TYPE=HOME,INTERNET:john.s@acme.co) + // ALTID is supported. + if (_value.StartsWith(VcardConstants._emailSpecifier + delimiter)) + { + if (isWithType) + _emails.Add(EmailInfo.FromStringVcardFiveWithType(_value, finalArgs, altId)); + else + _emails.Add(EmailInfo.FromStringVcardFive(_value, altId)); + } + + // Organization (ORG:Acme Co. or ORG:ABC, Inc.;North American Division;Marketing) + // ALTID is supported. + if (_value.StartsWith(VcardConstants._orgSpecifier + delimiter)) + { + if (isWithType) + _orgs.Add(OrganizationInfo.FromStringVcardFiveWithType(_value, finalArgs, altId)); + else + _orgs.Add(OrganizationInfo.FromStringVcardFive(_value, altId)); + } + + // Title (TITLE:Product Manager) + // ALTID is supported. + if (_value.StartsWith(VcardConstants._titleSpecifier + delimiter)) + { + if (isWithType) + _titles.Add(TitleInfo.FromStringVcardFiveWithType(_value, finalArgs, altId)); + else + _titles.Add(TitleInfo.FromStringVcardFive(_value, altId)); + } + + // Website link (URL:https://sso.org/) + // Here, we don't support ALTID. + if (_value.StartsWith(VcardConstants._urlSpecifier + delimiter)) + { + // Get the value + string urlValue = _value.Substring(VcardConstants._urlSpecifier.Length + 1); + + // Try to parse the URL to ensure that it conforms to IETF RFC 1738: Uniform Resource Locators + if (!Uri.TryCreate(urlValue, UriKind.Absolute, out Uri uri)) + throw new InvalidDataException($"URL {urlValue} is invalid"); + + // Populate field + _url = uri.ToString(); + } + + // Note (NOTE:Product Manager) + // Here, we don't support ALTID. + if (_value.StartsWith(VcardConstants._noteSpecifier + delimiter)) + { + // Get the value + string noteValue = _value.Substring(VcardConstants._noteSpecifier.Length + 1); + + // Populate field + _note = Regex.Unescape(noteValue); + } + + // Photo (PHOTO;ENCODING=BASE64;JPEG:... or PHOTO;VALUE=URL:file:///jqpublic.gif or PHOTO;ENCODING=BASE64;TYPE=GIF:...) + // ALTID is supported. + if (_value.StartsWith(VcardConstants._photoSpecifier + delimiter)) + { + if (isWithType) + _photos.Add(PhotoInfo.FromStringVcardFiveWithType(_value, finalArgs, altId, CardContentReader)); + else + throw new InvalidDataException("Photo field must not have empty type."); + } + + // Logo (LOGO;ENCODING=BASE64;JPEG:... or LOGO;VALUE=URL:file:///jqpublic.gif or LOGO;ENCODING=BASE64;TYPE=GIF:...) + // ALTID is supported. + if (_value.StartsWith(VcardConstants._logoSpecifier + delimiter)) + { + if (isWithType) + _logos.Add(LogoInfo.FromStringVcardFiveWithType(_value, finalArgs, altId, CardContentReader)); + else + throw new InvalidDataException("Photo field must not have empty type."); + } + + // Sound (SOUND;VALUE=URL:file///multimed/audio/jqpublic.wav or SOUND;WAVE;BASE64:... or SOUND;TYPE=WAVE;ENCODING=BASE64:...) + // ALTID is supported. + if (_value.StartsWith(VcardConstants._soundSpecifier + delimiter)) + { + if (isWithType) + _sounds.Add(SoundInfo.FromStringVcardFiveWithType(_value, finalArgs, altId, CardContentReader)); + else + throw new InvalidDataException("Photo field must not have empty type."); + } + + // Revision (REV:1995-10-31T22:27:10Z or REV:19951031T222710) + // Here, we don't support ALTID. + if (_value.StartsWith(VcardConstants._revSpecifier + delimiter)) + { + // Get the value + string revValue = _value.Substring(VcardConstants._revSpecifier.Length + 1); + + // Populate field + _rev = DateTime.Parse(revValue); + } + + // Nickname (NICKNAME;TYPE=cell,home:495-522-3560) + // ALTID is supported. + if (_value.StartsWith(VcardConstants._nicknameSpecifier + delimiter)) + { + if (isWithType) + _nicks.Add(NicknameInfo.FromStringVcardFiveWithType(_value, finalArgs, altId)); + else + _nicks.Add(NicknameInfo.FromStringVcardFive(_value, altId)); + } + + // Birthdate (BDAY:19950415 or BDAY:1953-10-15T23:10:00Z) + // Here, we don't support ALTID. + if (_value.StartsWith(VcardConstants._birthSpecifier + delimiter)) + { + // Get the value + string bdayValue = ""; + if (isWithType) + bdayValue = _value.Substring(_value.IndexOf(VcardConstants._argumentDelimiter) + 1); + else + bdayValue = _value.Substring(VcardConstants._birthSpecifier.Length + 1); + + // Populate field + if (int.TryParse(bdayValue, out int bdayDigits) && bdayValue.Length == 8) + { + int birthNum = int.Parse(bdayValue); + var birthDigits = VcardParserTools.GetDigits(birthNum).ToList(); + int birthYear = (birthDigits[0] * 1000) + (birthDigits[1] * 100) + (birthDigits[2] * 10) + birthDigits[3]; + int birthMonth = (birthDigits[4] * 10) + birthDigits[5]; + int birthDay = (birthDigits[6] * 10) + birthDigits[7]; + _bday = new DateTime(birthYear, birthMonth, birthDay); + } + else + _bday = DateTime.Parse(bdayValue); + } + + // Role (ROLE:Programmer) + // ALTID is supported. + if (_value.StartsWith(VcardConstants._roleSpecifier + delimiter)) + { + if (isWithType) + _roles.Add(RoleInfo.FromStringVcardFiveWithType(_value, finalArgs, altId)); + else + _roles.Add(RoleInfo.FromStringVcardFive(_value, altId)); + } + + // Categories (CATEGORIES:INTERNET or CATEGORIES:INTERNET,IETF,INDUSTRY,INFORMATION TECHNOLOGY) + // Here, we don't support ALTID. + if (_value.StartsWith(VcardConstants._categoriesSpecifier + delimiter)) + { + // Get the value + string categoriesValue = _value.Substring(VcardConstants._categoriesSpecifier.Length + 1); + + // Populate field + _categories.AddRange(Regex.Unescape(categoriesValue).Split(',')); + } + + // Product ID (PRODID:-//ONLINE DIRECTORY//NONSGML Version 1//EN) + // Here, we don't support ALTID. + if (_value.StartsWith(VcardConstants._productIdSpecifier + delimiter)) + { + // Get the value + string prodIdValue = _value.Substring(VcardConstants._productIdSpecifier.Length + 1); + + // Populate field + _prodId = Regex.Unescape(prodIdValue); + } + + // Sort string (SORT-STRING:Harten) + // Here, we don't support ALTID. + if (_value.StartsWith(VcardConstants._sortStringSpecifier + delimiter)) + { + // Get the value + string sortStringValue = _value.Substring(VcardConstants._sortStringSpecifier.Length + 1); + + // Populate field + _sortString = Regex.Unescape(sortStringValue); + } + + // Time Zone (TZ;VALUE=text:-05:00; EST; Raleigh/North America) + // ALTID is supported. + if (_value.StartsWith(VcardConstants._timeZoneSpecifier + delimiter)) + { + if (isWithType) + _timezones.Add(TimeZoneInfo.FromStringVcardFiveWithType(_value, finalArgs, altId)); + else + _timezones.Add(TimeZoneInfo.FromStringVcardFive(_value, altId)); + } + + // Geo (GEO;VALUE=uri:https://...) + // ALTID is supported. + if (_value.StartsWith(VcardConstants._geoSpecifier + delimiter)) + { + if (isWithType) + _geos.Add(GeoInfo.FromStringVcardFiveWithType(_value, finalArgs, altId)); + else + _geos.Add(GeoInfo.FromStringVcardFive(_value, altId)); + } + + // IMPP information (IMPP;TYPE=home:sip:test) + // ALTID is supported. + if (_value.StartsWith(VcardConstants._imppSpecifier + delimiter)) + { + if (isWithType) + _impps.Add(ImppInfo.FromStringVcardFiveWithType(_value, finalArgs, altId)); + else + _impps.Add(ImppInfo.FromStringVcardFive(_value, altId)); + } + + // Source (SOURCE:http://johndoe.com/vcard.vcf) + // Here, we don't support ALTID. + if (_value.StartsWith(VcardConstants._sourceSpecifier + delimiter)) + { + // Get the value + string sourceStringValue = _value.Substring(VcardConstants._sourceSpecifier.Length + 1); + + // Try to parse the URL to ensure that it conforms to IETF RFC 1738: Uniform Resource Locators + if (!Uri.TryCreate(sourceStringValue, UriKind.Absolute, out Uri uri)) + throw new InvalidDataException($"URL {sourceStringValue} is invalid"); + + // Populate field + _source = uri.ToString(); + } + + // Mailer (MAILER:ccMail 2.2 or MAILER:PigeonMail 2.1) + if (_value.StartsWith(VcardConstants._mailerSpecifier + delimiter)) + { + // Get the value + string mailerValue = _value.Substring(VcardConstants._mailerSpecifier.Length + 1); + + // Populate field + _mailer = Regex.Unescape(mailerValue); + } + + // Free/busy URL (FBURL:http://example.com/fb/jdoe) + // Here, we don't support ALTID. + if (_value.StartsWith(VcardConstants._fbUrlSpecifier + delimiter)) + { + // Get the value + string fbUrlStringValue = _value.Substring(VcardConstants._fbUrlSpecifier.Length + 1); + + // Try to parse the URL to ensure that it conforms to IETF RFC 1738: Uniform Resource Locators + if (!Uri.TryCreate(fbUrlStringValue, UriKind.Absolute, out Uri uri)) + throw new InvalidDataException($"URL {fbUrlStringValue} is invalid"); + + // Populate field + _fbUrl = uri.ToString(); + } + + // Calendar URL (CALURI:http://example.com/calendar/jdoe) + // Here, we don't support ALTID. + if (_value.StartsWith(VcardConstants._calUriSpecifier + delimiter)) + { + // Get the value + string calUriStringValue = _value.Substring(VcardConstants._calUriSpecifier.Length + 1); + + // Try to parse the URL to ensure that it conforms to IETF RFC 1738: Uniform Resource Locators + if (!Uri.TryCreate(calUriStringValue, UriKind.Absolute, out Uri uri)) + throw new InvalidDataException($"URL {calUriStringValue} is invalid"); + + // Populate field + _calUri = uri.ToString(); + } + + // Calendar Request URL (CALADRURI:http://example.com/calendar/jdoe) + // Here, we don't support ALTID. + if (_value.StartsWith(VcardConstants._caladrUriSpecifier + delimiter)) + { + // Get the value + string caladrUriStringValue = _value.Substring(VcardConstants._caladrUriSpecifier.Length + 1); + + // Try to parse the URL to ensure that it conforms to IETF RFC 1738: Uniform Resource Locators + if (!Uri.TryCreate(caladrUriStringValue, UriKind.Absolute, out Uri uri)) + throw new InvalidDataException($"URL {caladrUriStringValue} is invalid"); + + // Populate field + _caladrUri = uri.ToString(); + } + + // Class (CLASS:PUBLIC, CLASS:PRIVATE, or CLASS:CONFIDENTIAL) + if (_value.StartsWith(VcardConstants._classSpecifier + delimiter)) + { + // Get the value + string classValue = _value.Substring(VcardConstants._classSpecifier.Length + 1); + + // Populate field + _class = Regex.Unescape(classValue); + } + + // X-nonstandard (X-AIM:john.s or X-DL;Design Work Group:List Item 1;List Item 2;List Item 3) + // ALTID is supported. + if (_value.StartsWith(VcardConstants._xSpecifier)) + _xes.Add(XNameInfo.FromStringVcardFive(_value, finalArgs, altId)); + } + catch (Exception ex) + { + throw new VCardParseException(ex.Message, _value, lineNumber, ex); + } + } + + // Requirement checks + if (!nameSpecifierSpotted) + throw new InvalidDataException("The name specifier, \"N:\", is required."); + if (!fullNameSpecifierSpotted) + throw new InvalidDataException("The full name specifier, \"FN:\", is required."); + + // Make a new instance of the card + return new Card(this, CardVersion, _kind) + { + CardRevision = _rev, + ContactNames = [.. _names], + ContactFullName = _fullName, + ContactTelephones = [.. _telephones], + ContactAddresses = [.. _addresses], + ContactLabels = [.. _labels], + ContactOrganizations = [.. _orgs], + ContactTitles = [.. _titles], + ContactURL = _url, + ContactNotes = _note, + ContactMails = [.. _emails], + ContactXNames = [.. _xes], + ContactPhotos = [.. _photos], + ContactNicknames = [.. _nicks], + ContactBirthdate = _bday, + ContactMailer = _mailer, + ContactRoles = [.. _roles], + ContactCategories = [.. _categories], + ContactLogos = [.. _logos], + ContactProdId = _prodId, + ContactSortString = _sortString, + ContactTimeZone = [.. _timezones], + ContactGeo = [.. _geos], + ContactSounds = [.. _sounds], + ContactImpps = [.. _impps], + ContactSource = _source, + ContactFreeBusyUrl = _fbUrl, + ContactCalendarUrl = _calUri, + ContactCalendarSchedulingRequestUrl = _caladrUri, + ContactAccessClassification = _class + }; + } + + internal override string SaveToString(Card card) + { + // Check the version to ensure that we're really dealing with VCard 5.0 contact + if (CardVersion != "5.0") + throw new InvalidDataException($"Card version {CardVersion} doesn't match expected \"5.0\"."); + + // Check the content to ensure that we really have data + if (string.IsNullOrEmpty(CardContent)) + throw new InvalidDataException($"Card content is empty."); + + // Initialize the card builder + var cardBuilder = new StringBuilder(); + + // First, write the header + cardBuilder.AppendLine("BEGIN:VCARD"); + cardBuilder.AppendLine($"VERSION:{CardVersion}"); + cardBuilder.AppendLine($"{VcardConstants._kindSpecifier}:{card.CardKind}"); + + // Then, write the full name and the name + if (!string.IsNullOrWhiteSpace(card.ContactFullName)) + cardBuilder.AppendLine($"{VcardConstants._fullNameSpecifier}:{card.ContactFullName}"); + foreach (NameInfo name in card.ContactNames) + cardBuilder.AppendLine(name.ToStringVcardFive()); + + // Now, start filling in the rest... + foreach (TelephoneInfo telephone in card.ContactTelephones) + cardBuilder.AppendLine(telephone.ToStringVcardFive()); + foreach (AddressInfo address in card.ContactAddresses) + cardBuilder.AppendLine(address.ToStringVcardFive()); + foreach (LabelAddressInfo label in card.ContactLabels) + cardBuilder.AppendLine(label.ToStringVcardFive()); + foreach (EmailInfo email in card.ContactMails) + cardBuilder.AppendLine(email.ToStringVcardFive()); + foreach (OrganizationInfo organization in card.ContactOrganizations) + cardBuilder.AppendLine(organization.ToStringVcardFive()); + foreach (TitleInfo title in card.ContactTitles) + cardBuilder.AppendLine(title.ToStringVcardFive()); + if (!string.IsNullOrWhiteSpace(card.ContactURL)) + cardBuilder.AppendLine($"{VcardConstants._urlSpecifier}:{card.ContactURL}"); + if (!string.IsNullOrWhiteSpace(card.ContactNotes)) + cardBuilder.AppendLine($"{VcardConstants._noteSpecifier}:{card.ContactNotes}"); + foreach (PhotoInfo photo in card.ContactPhotos) + cardBuilder.AppendLine(photo.ToStringVcardFive()); + foreach (LogoInfo logo in card.ContactLogos) + cardBuilder.AppendLine(logo.ToStringVcardFive()); + foreach (SoundInfo sound in card.ContactSounds) + cardBuilder.AppendLine(sound.ToStringVcardFive()); + if (card.CardRevision is not null && card.CardRevision != DateTime.MinValue) + cardBuilder.AppendLine($"{VcardConstants._revSpecifier}:{card.CardRevision:yyyy-MM-dd HH:mm:ss}"); + foreach (NicknameInfo nickname in card.ContactNicknames) + cardBuilder.AppendLine(nickname.ToStringVcardFive()); + if (card.ContactBirthdate is not null && card.ContactBirthdate != DateTime.MinValue) + cardBuilder.AppendLine($"{VcardConstants._birthSpecifier}:{card.ContactBirthdate:yyyy-MM-dd}"); + if (!string.IsNullOrWhiteSpace(card.ContactMailer)) + cardBuilder.AppendLine($"{VcardConstants._mailerSpecifier}:{card.ContactMailer}"); + foreach (RoleInfo role in card.ContactRoles) + cardBuilder.AppendLine(role.ToStringVcardFive()); + if (card.ContactCategories is not null && card.ContactCategories.Length > 0) + cardBuilder.AppendLine($"{VcardConstants._categoriesSpecifier}:{string.Join(",", card.ContactCategories)}"); + if (!string.IsNullOrWhiteSpace(card.ContactProdId)) + cardBuilder.AppendLine($"{VcardConstants._productIdSpecifier}:{card.ContactProdId}"); + if (!string.IsNullOrWhiteSpace(card.ContactSortString)) + cardBuilder.AppendLine($"{VcardConstants._sortStringSpecifier}:{card.ContactSortString}"); + foreach (TimeZoneInfo timeZone in card.ContactTimeZone) + cardBuilder.AppendLine(timeZone.ToStringVcardFive()); + foreach (GeoInfo geo in card.ContactGeo) + cardBuilder.AppendLine(geo.ToStringVcardFive()); + foreach (ImppInfo impp in card.ContactImpps) + cardBuilder.AppendLine(impp.ToStringVcardFive()); + if (!string.IsNullOrWhiteSpace(card.ContactAccessClassification)) + cardBuilder.AppendLine($"{VcardConstants._classSpecifier}:{card.ContactAccessClassification}"); + foreach (XNameInfo xname in card.ContactXNames) + cardBuilder.AppendLine(xname.ToStringVcardFive()); + + // Finally, end the card and return it + cardBuilder.AppendLine("END:VCARD"); + return cardBuilder.ToString(); + } + + internal override void SaveTo(string path, Card card) + { + // Check the version to ensure that we're really dealing with VCard 5.0 contact + if (CardVersion != "5.0") + throw new InvalidDataException($"Card version {CardVersion} doesn't match expected \"5.0\"."); + + // Check the content to ensure that we really have data + if (string.IsNullOrEmpty(CardContent)) + throw new InvalidDataException($"Card content is empty."); + + // Save all the changes to the file + var cardString = SaveToString(card); + File.WriteAllText(path, cardString); + } + + internal VcardFive(string cardContent, string cardVersion) + { + CardContent = cardContent; + CardVersion = cardVersion; + } + } +} diff --git a/VisualCard/Parsers/Four/VcardFour.cs b/VisualCard/Parsers/Four/VcardFour.cs index 913c291..9cad73d 100644 --- a/VisualCard/Parsers/Four/VcardFour.cs +++ b/VisualCard/Parsers/Four/VcardFour.cs @@ -66,7 +66,6 @@ public override Card Parse() string _url = ""; string _note = ""; string _prodId = ""; - string _sortString = ""; string _source = ""; string _xml = ""; string _fbUrl = ""; @@ -368,17 +367,6 @@ public override Card Parse() _prodId = Regex.Unescape(prodIdValue); } - // Sort string (SORT-STRING:Harten) - // Here, we don't support ALTID. - if (_value.StartsWith(VcardConstants._sortStringSpecifier + delimiter)) - { - // Get the value - string sortStringValue = _value.Substring(VcardConstants._sortStringSpecifier.Length + 1); - - // Populate field - _sortString = Regex.Unescape(sortStringValue); - } - // Time Zone (TZ;VALUE=text:-05:00; EST; Raleigh/North America) // ALTID is supported. if (_value.StartsWith(VcardConstants._timeZoneSpecifier + delimiter)) @@ -517,7 +505,6 @@ public override Card Parse() ContactCategories = [.. _categories], ContactLogos = [.. _logos], ContactProdId = _prodId, - ContactSortString = _sortString, ContactTimeZone = [.. _timezones], ContactGeo = [.. _geos], ContactSounds = [.. _sounds], diff --git a/VisualCard/Parsers/VcardConstants.cs b/VisualCard/Parsers/VcardConstants.cs index 07dc414..f7e351d 100644 --- a/VisualCard/Parsers/VcardConstants.cs +++ b/VisualCard/Parsers/VcardConstants.cs @@ -26,7 +26,7 @@ namespace VisualCard.Parsers { internal static class VcardConstants { - // Available in vCard 2.1, vCard 3.0, and vCard 4.0 + // Available in vCard 2.1, 3.0, 4.0, and 5.0 internal const char _fieldDelimiter = ';'; internal const char _valueDelimiter = ','; internal const char _argumentDelimiter = ':'; @@ -54,24 +54,24 @@ internal static class VcardConstants internal const string _fbUrlSpecifier = "FBURL"; internal const string _calUriSpecifier = "CALURI"; internal const string _caladrUriSpecifier = "CALADRURI"; + internal const string _categoriesSpecifier = "CATEGORIES"; internal const string _xSpecifier = "X-"; internal const string _typeArgumentSpecifier = "TYPE="; internal const string _valueArgumentSpecifier = "VALUE="; internal const string _encodingArgumentSpecifier = "ENCODING="; - // Available in vCard 2.1 and 3.0 + // Available in vCard 2.1, 3.0, and 5.0 internal const string _labelSpecifier = "LABEL"; + internal const string _sortStringSpecifier = "SORT-STRING"; - // Available in vCard 3.0 - internal const string _classSpecifier = "CLASS"; - - // Available in vCard 3.0 and vCard 4.0 + // Available in vCard 3.0, 4.0, and 5.0 internal const string _nicknameSpecifier = "NICKNAME"; - internal const string _categoriesSpecifier = "CATEGORIES"; internal const string _productIdSpecifier = "PRODID"; - internal const string _sortStringSpecifier = "SORT-STRING"; - // Available in vCard 4.0 + // Available in vCard 3.0 and 5.0 + internal const string _classSpecifier = "CLASS"; + + // Available in vCard 4.0 and 5.0 internal const string _kindSpecifier = "KIND"; internal const string _altIdArgumentSpecifier = "ALTID="; } diff --git a/VisualCard/Parts/AddressInfo.cs b/VisualCard/Parts/AddressInfo.cs index 997b702..0b91222 100644 --- a/VisualCard/Parts/AddressInfo.cs +++ b/VisualCard/Parts/AddressInfo.cs @@ -179,6 +179,9 @@ internal string ToStringVcardFour() $"{Country}"; } + internal string ToStringVcardFive() => + ToStringVcardFour(); + internal static AddressInfo FromStringVcardTwo(string value) { // Get the value @@ -329,6 +332,12 @@ internal static AddressInfo FromStringVcardFourWithType(string value, List + FromStringVcardFour(value, altId); + + internal static AddressInfo FromStringVcardFiveWithType(string value, List finalArgs, int altId) => + FromStringVcardFourWithType(value, finalArgs, altId); + internal AddressInfo() { } internal AddressInfo(int altId, string[] altArguments, string[] addressTypes, string postOfficeBox, string extendedAddress, string streetAddress, string locality, string region, string postalCode, string country) diff --git a/VisualCard/Parts/EmailInfo.cs b/VisualCard/Parts/EmailInfo.cs index e4294ec..80ebc68 100644 --- a/VisualCard/Parts/EmailInfo.cs +++ b/VisualCard/Parts/EmailInfo.cs @@ -124,6 +124,9 @@ internal string ToStringVcardFour() $"{ContactEmailAddress}"; } + internal string ToStringVcardFive() => + ToStringVcardFour(); + internal static EmailInfo FromStringVcardTwo(string value) { // Get the value @@ -274,6 +277,12 @@ internal static EmailInfo FromStringVcardFourWithType(string value, List return _email; } + internal static EmailInfo FromStringVcardFive(string value, int altId) => + FromStringVcardFour(value, altId); + + internal static EmailInfo FromStringVcardFiveWithType(string value, List finalArgs, int altId) => + FromStringVcardFourWithType(value, finalArgs, altId); + internal EmailInfo() { } internal EmailInfo(int altId, string[] altArguments, string[] contactEmailTypes, string contactEmailAddress) diff --git a/VisualCard/Parts/GeoInfo.cs b/VisualCard/Parts/GeoInfo.cs index f8299f9..f940ac7 100644 --- a/VisualCard/Parts/GeoInfo.cs +++ b/VisualCard/Parts/GeoInfo.cs @@ -123,6 +123,9 @@ internal string ToStringVcardFour() $"{Geo}"; } + internal string ToStringVcardFive() => + ToStringVcardFour(); + internal static GeoInfo FromStringVcardTwo(string value) { string geoValue = value.Substring(VcardConstants._geoSpecifier.Length + 1); @@ -205,6 +208,12 @@ internal static GeoInfo FromStringVcardFourWithType(string value, List f return _geo; } + internal static GeoInfo FromStringVcardFive(string value, int altId) => + FromStringVcardFour(value, altId); + + internal static GeoInfo FromStringVcardFiveWithType(string value, List finalArgs, int altId) => + FromStringVcardFourWithType(value, finalArgs, altId); + internal GeoInfo() { } internal GeoInfo(int altId, string[] altArguments, string[] geoTypes, string geo) diff --git a/VisualCard/Parts/ImppInfo.cs b/VisualCard/Parts/ImppInfo.cs index 72017a1..39fb853 100644 --- a/VisualCard/Parts/ImppInfo.cs +++ b/VisualCard/Parts/ImppInfo.cs @@ -128,6 +128,9 @@ internal string ToStringVcardFour() $"{ContactIMPP}"; } + internal string ToStringVcardFive() => + ToStringVcardFour(); + internal static ImppInfo FromStringVcardTwo(string value) { string imppValue = value.Substring(VcardConstants._imppSpecifier.Length + 1); @@ -205,6 +208,12 @@ internal static ImppInfo FromStringVcardFourWithType(string value, List return _imppInstance; } + internal static ImppInfo FromStringVcardFive(string value, int altId) => + FromStringVcardFour(value, altId); + + internal static ImppInfo FromStringVcardFiveWithType(string value, List finalArgs, int altId) => + FromStringVcardFourWithType(value, finalArgs, altId); + internal ImppInfo() { } internal ImppInfo(int altId, string[] altArguments, string contactImpp, string[] imppTypes) diff --git a/VisualCard/Parts/LabelAddressInfo.cs b/VisualCard/Parts/LabelAddressInfo.cs index 5b4a25b..6a48f8d 100644 --- a/VisualCard/Parts/LabelAddressInfo.cs +++ b/VisualCard/Parts/LabelAddressInfo.cs @@ -102,7 +102,7 @@ public override int GetHashCode() internal string ToStringVcardTwo() { return - $"{VcardConstants._addressSpecifier};" + + $"{VcardConstants._labelSpecifier};" + $"{VcardConstants._typeArgumentSpecifier}{string.Join(",", AddressTypes)}{VcardConstants._argumentDelimiter}" + $"{DeliveryLabel}"; } @@ -110,7 +110,17 @@ internal string ToStringVcardTwo() internal string ToStringVcardThree() { return - $"{VcardConstants._addressSpecifier};" + + $"{VcardConstants._labelSpecifier};" + + $"{VcardConstants._typeArgumentSpecifier}{string.Join(",", AddressTypes)}{VcardConstants._argumentDelimiter}" + + $"{DeliveryLabel}"; + } + + internal string ToStringVcardFive() + { + bool installAltId = AltId >= 0 && AltArguments.Length > 0; + return + $"{VcardConstants._labelSpecifier};" + + $"{(installAltId ? VcardConstants._altIdArgumentSpecifier + AltId + VcardConstants._fieldDelimiter : "")}" + $"{VcardConstants._typeArgumentSpecifier}{string.Join(",", AddressTypes)}{VcardConstants._argumentDelimiter}" + $"{DeliveryLabel}"; } @@ -118,7 +128,7 @@ internal string ToStringVcardThree() internal static LabelAddressInfo FromStringVcardTwo(string value) { // Get the value - string adrValue = value.Substring(VcardConstants._addressSpecifier.Length + 1); + string adrValue = value.Substring(VcardConstants._labelSpecifier.Length + 1); string[] splitAdr = adrValue.Split(VcardConstants._argumentDelimiter); // Check the provided address @@ -136,7 +146,7 @@ internal static LabelAddressInfo FromStringVcardTwo(string value) internal static LabelAddressInfo FromStringVcardTwoWithType(string value) { // Get the value - string adrValue = value.Substring(VcardConstants._addressSpecifier.Length + 1); + string adrValue = value.Substring(VcardConstants._labelSpecifier.Length + 1); string[] splitAdr = adrValue.Split(VcardConstants._argumentDelimiter); if (splitAdr.Length < 2) throw new InvalidDataException("Label address field must specify exactly two values (Type (optionally prepended with TYPE=), and address information)"); @@ -156,7 +166,7 @@ internal static LabelAddressInfo FromStringVcardTwoWithType(string value) internal static LabelAddressInfo FromStringVcardThree(string value) { // Get the value - string adrValue = value.Substring(VcardConstants._addressSpecifier.Length + 1); + string adrValue = value.Substring(VcardConstants._labelSpecifier.Length + 1); string[] splitAdr = adrValue.Split(VcardConstants._argumentDelimiter); // Check the provided address @@ -174,7 +184,7 @@ internal static LabelAddressInfo FromStringVcardThree(string value) internal static LabelAddressInfo FromStringVcardThreeWithType(string value) { // Get the value - string adrValue = value.Substring(VcardConstants._addressSpecifier.Length + 1); + string adrValue = value.Substring(VcardConstants._labelSpecifier.Length + 1); string[] splitAdr = adrValue.Split(VcardConstants._argumentDelimiter); if (splitAdr.Length < 2) throw new InvalidDataException("Label address field must specify exactly two values (Type (must be prepended with TYPE=), and address information)"); @@ -191,6 +201,44 @@ internal static LabelAddressInfo FromStringVcardThreeWithType(string value) return _address; } + internal static LabelAddressInfo FromStringVcardFive(string value, int altId) + { + // Get the value + string adrValue = value.Substring(VcardConstants._labelSpecifier.Length + 1); + string[] splitAdr = adrValue.Split(VcardConstants._argumentDelimiter); + + // Check the provided address + string[] splitAddressValues = splitAdr[0].Split(VcardConstants._fieldDelimiter); + if (splitAddressValues.Length < 1) + throw new InvalidDataException("Label address information must specify exactly one value (address label)"); + + // Populate the fields + string[] _addressTypes = ["HOME"]; + string _addressLabel = Regex.Unescape(splitAddressValues[0]); + LabelAddressInfo _address = new(altId, [], _addressTypes, _addressLabel); + return _address; + } + + internal static LabelAddressInfo FromStringVcardFiveWithType(string value, List finalArgs, int altId) + { + // Get the value + string adrValue = value.Substring(VcardConstants._labelSpecifier.Length + 1); + string[] splitAdr = adrValue.Split(VcardConstants._argumentDelimiter); + if (splitAdr.Length < 2) + throw new InvalidDataException("Label address field must specify exactly two values (Type (must be prepended with TYPE=), and address information)"); + + // Check the provided address + string[] splitAddressValues = splitAdr[1].Split(VcardConstants._fieldDelimiter); + if (splitAddressValues.Length < 1) + throw new InvalidDataException("Label address information must specify exactly one value (address label)"); + + // Populate the fields + string[] _addressTypes = VcardParserTools.GetTypes(splitAdr, "HOME", true); + string _addressLabel = Regex.Unescape(splitAddressValues[0]); + LabelAddressInfo _address = new(altId, [.. finalArgs], _addressTypes, _addressLabel); + return _address; + } + internal LabelAddressInfo() { } internal LabelAddressInfo(int altId, string[] altArguments, string[] addressTypes, string label) diff --git a/VisualCard/Parts/LogoInfo.cs b/VisualCard/Parts/LogoInfo.cs index 00b22f2..81b38ad 100644 --- a/VisualCard/Parts/LogoInfo.cs +++ b/VisualCard/Parts/LogoInfo.cs @@ -176,6 +176,9 @@ internal string ToStringVcardFour() } } + internal string ToStringVcardFive() => + ToStringVcardFour(); + internal static LogoInfo FromStringVcardTwoWithType(string value, StreamReader cardContentReader) { // Get the value @@ -293,6 +296,9 @@ internal static LogoInfo FromStringVcardFourWithType(string value, List return _logo; } + internal static LogoInfo FromStringVcardFiveWithType(string value, List finalArgs, int altId, StreamReader cardContentReader) => + FromStringVcardFourWithType(value, finalArgs, altId, cardContentReader); + internal LogoInfo() { } internal LogoInfo(int altId, string[] altArguments, string valueType, string encoding, string logoType, string logoEncoded) diff --git a/VisualCard/Parts/NameInfo.cs b/VisualCard/Parts/NameInfo.cs index 807ac91..58b05e9 100644 --- a/VisualCard/Parts/NameInfo.cs +++ b/VisualCard/Parts/NameInfo.cs @@ -162,6 +162,9 @@ internal string ToStringVcardFour() $"{suffixesStr}"; } + internal string ToStringVcardFive() => + ToStringVcardFour(); + internal static NameInfo FromStringVcardTwo(string value) { // Check the line @@ -245,6 +248,12 @@ internal static NameInfo FromStringVcardFourWithType(string value, string[] spli return _name; } + internal static NameInfo FromStringVcardFive(string[] splitValues, bool idReservedForName) => + FromStringVcardFour(splitValues, idReservedForName); + + internal static NameInfo FromStringVcardFiveWithType(string value, string[] splitArgs, List finalArgs, int altId, List _names, bool idReservedForName) => + FromStringVcardFourWithType(value, splitArgs, finalArgs, altId, _names, idReservedForName); + internal NameInfo() { } internal NameInfo(int altId, string[] altArguments, string contactFirstName, string contactLastName, string[] altNames, string[] prefixes, string[] suffixes) diff --git a/VisualCard/Parts/NicknameInfo.cs b/VisualCard/Parts/NicknameInfo.cs index cbf9319..f08ae38 100644 --- a/VisualCard/Parts/NicknameInfo.cs +++ b/VisualCard/Parts/NicknameInfo.cs @@ -123,6 +123,9 @@ internal string ToStringVcardFour() $"{ContactNickname}"; } + internal string ToStringVcardFive() => + ToStringVcardFour(); + internal static NicknameInfo FromStringVcardThree(string value) { // Get the value @@ -177,6 +180,12 @@ internal static NicknameInfo FromStringVcardFourWithType(string value, List + FromStringVcardFour(value, altId); + + internal static NicknameInfo FromStringVcardFiveWithType(string value, List finalArgs, int altId) => + FromStringVcardFourWithType(value, finalArgs, altId); + internal NicknameInfo() { } internal NicknameInfo(int altId, string[] altArguments, string contactNickname, string[] nicknameTypes) diff --git a/VisualCard/Parts/OrganizationInfo.cs b/VisualCard/Parts/OrganizationInfo.cs index d796ac5..68faabd 100644 --- a/VisualCard/Parts/OrganizationInfo.cs +++ b/VisualCard/Parts/OrganizationInfo.cs @@ -146,6 +146,9 @@ internal string ToStringVcardFour() $"{Role}"; } + internal string ToStringVcardFive() => + ToStringVcardFour(); + internal static OrganizationInfo FromStringVcardTwo(string value) { // Get the value @@ -257,6 +260,12 @@ internal static OrganizationInfo FromStringVcardFourWithType(string value, List< return _org; } + internal static OrganizationInfo FromStringVcardFive(string value, int altId) => + FromStringVcardFour(value, altId); + + internal static OrganizationInfo FromStringVcardFiveWithType(string value, List finalArgs, int altId) => + FromStringVcardFourWithType(value, finalArgs, altId); + internal OrganizationInfo() { } internal OrganizationInfo(int altId, string[] altArguments, string name, string unit, string role, string[] orgTypes) diff --git a/VisualCard/Parts/PhotoInfo.cs b/VisualCard/Parts/PhotoInfo.cs index 7f8e5f3..a13520c 100644 --- a/VisualCard/Parts/PhotoInfo.cs +++ b/VisualCard/Parts/PhotoInfo.cs @@ -176,6 +176,9 @@ internal string ToStringVcardFour() } } + internal string ToStringVcardFive() => + ToStringVcardFour(); + internal static PhotoInfo FromStringVcardTwoWithType(string value, StreamReader cardContentReader) { // Get the value @@ -293,6 +296,9 @@ internal static PhotoInfo FromStringVcardFourWithType(string value, List return _photo; } + internal static PhotoInfo FromStringVcardFiveWithType(string value, List finalArgs, int altId, StreamReader cardContentReader) => + FromStringVcardFourWithType(value, finalArgs, altId, cardContentReader); + internal PhotoInfo() { } internal PhotoInfo(int altId, string[] altArguments, string valueType, string encoding, string photoType, string photoEncoded) diff --git a/VisualCard/Parts/RoleInfo.cs b/VisualCard/Parts/RoleInfo.cs index 37094f9..8046483 100644 --- a/VisualCard/Parts/RoleInfo.cs +++ b/VisualCard/Parts/RoleInfo.cs @@ -115,6 +115,9 @@ internal string ToStringVcardFour() $"{ContactRole}"; } + internal string ToStringVcardFive() => + ToStringVcardFour(); + internal static RoleInfo FromStringVcardTwo(string value) { // Get the value @@ -155,6 +158,12 @@ internal static RoleInfo FromStringVcardFourWithType(string value, List return _role; } + internal static RoleInfo FromStringVcardFive(string value, int altId) => + FromStringVcardFour(value, altId); + + internal static RoleInfo FromStringVcardFiveWithType(string value, List finalArgs, int altId) => + FromStringVcardFourWithType(value, finalArgs, altId); + internal RoleInfo() { } internal RoleInfo(int altId, string[] altArguments, string contactRole) diff --git a/VisualCard/Parts/SoundInfo.cs b/VisualCard/Parts/SoundInfo.cs index b501bed..2d8989b 100644 --- a/VisualCard/Parts/SoundInfo.cs +++ b/VisualCard/Parts/SoundInfo.cs @@ -176,6 +176,9 @@ internal string ToStringVcardFour() } } + internal string ToStringVcardFive() => + ToStringVcardFour(); + internal static SoundInfo FromStringVcardTwoWithType(string value, StreamReader cardContentReader) { // Get the value @@ -293,6 +296,9 @@ internal static SoundInfo FromStringVcardFourWithType(string value, List return _sound; } + internal static SoundInfo FromStringVcardFiveWithType(string value, List finalArgs, int altId, StreamReader cardContentReader) => + FromStringVcardFourWithType(value, finalArgs, altId, cardContentReader); + internal SoundInfo() { } internal SoundInfo(int altId, string[] altArguments, string valueType, string encoding, string soundType, string soundEncoded) diff --git a/VisualCard/Parts/TelephoneInfo.cs b/VisualCard/Parts/TelephoneInfo.cs index cc10e47..24daf12 100644 --- a/VisualCard/Parts/TelephoneInfo.cs +++ b/VisualCard/Parts/TelephoneInfo.cs @@ -125,6 +125,9 @@ internal string ToStringVcardFour() $"{ContactPhoneNumber}"; } + internal string ToStringVcardFive() => + ToStringVcardFour(); + internal static TelephoneInfo FromStringVcardTwo(string value) { // Get the value @@ -206,6 +209,12 @@ internal static TelephoneInfo FromStringVcardFourWithType(string value, List + FromStringVcardFour(value, altId); + + internal static TelephoneInfo FromStringVcardFiveWithType(string value, List finalArgs, int altId) => + FromStringVcardFourWithType(value, finalArgs, altId); + internal TelephoneInfo() { } internal TelephoneInfo(int altId, string[] altArguments, string[] contactPhoneTypes, string contactPhoneNumber) diff --git a/VisualCard/Parts/TimeZoneInfo.cs b/VisualCard/Parts/TimeZoneInfo.cs index 6a7a78f..bd947ab 100644 --- a/VisualCard/Parts/TimeZoneInfo.cs +++ b/VisualCard/Parts/TimeZoneInfo.cs @@ -123,6 +123,9 @@ internal string ToStringVcardFour() $"{TimeZone}"; } + internal string ToStringVcardFive() => + ToStringVcardFour(); + internal static TimeZoneInfo FromStringVcardTwo(string value) { string tzValue = value.Substring(VcardConstants._timeZoneSpecifier.Length + 1); @@ -205,6 +208,12 @@ internal static TimeZoneInfo FromStringVcardFourWithType(string value, List + FromStringVcardFour(value, altId); + + internal static TimeZoneInfo FromStringVcardFiveWithType(string value, List finalArgs, int altId) => + FromStringVcardFourWithType(value, finalArgs, altId); + internal TimeZoneInfo() { } internal TimeZoneInfo(int altId, string[] altArguments, string[] timeZoneTypes, string timeZone) diff --git a/VisualCard/Parts/TitleInfo.cs b/VisualCard/Parts/TitleInfo.cs index ffb96de..a17dc0e 100644 --- a/VisualCard/Parts/TitleInfo.cs +++ b/VisualCard/Parts/TitleInfo.cs @@ -116,6 +116,9 @@ internal string ToStringVcardFour() $"{ContactTitle}"; } + internal string ToStringVcardFive() => + ToStringVcardFour(); + internal static TitleInfo FromStringVcardTwo(string value) { // Get the value @@ -161,6 +164,12 @@ internal static TitleInfo FromStringVcardFourWithType(string value, List return title; } + internal static TitleInfo FromStringVcardFive(string value, int altId) => + FromStringVcardFour(value, altId); + + internal static TitleInfo FromStringVcardFiveWithType(string value, List finalArgs, int altId) => + FromStringVcardFourWithType(value, finalArgs, altId); + internal TitleInfo() { } internal TitleInfo(int altId, string[] altArguments, string contactTitle) diff --git a/VisualCard/Parts/XNameInfo.cs b/VisualCard/Parts/XNameInfo.cs index b6760ae..9e05c8e 100644 --- a/VisualCard/Parts/XNameInfo.cs +++ b/VisualCard/Parts/XNameInfo.cs @@ -133,6 +133,9 @@ internal string ToStringVcardFour() $"{string.Join(VcardConstants._fieldDelimiter.ToString(), XValues)}"; } + internal string ToStringVcardFive() => + ToStringVcardFour(); + internal static XNameInfo FromStringVcardTwo(string value) { string xValue = value.Substring(VcardConstants._xSpecifier.Length); @@ -191,6 +194,9 @@ internal static XNameInfo FromStringVcardFour(string value, List finalAr return _x; } + internal static XNameInfo FromStringVcardFive(string value, List finalArgs, int altId) => + FromStringVcardFour(value, finalArgs, altId); + internal XNameInfo() { } internal XNameInfo(int altId, string[] altArguments, string xKeyName, string[] xValues, string[] xKeyTypes) diff --git a/VisualCard/Specs/vcard-50-aptivi.txt b/VisualCard/Specs/vcard-50-aptivi.txt new file mode 100644 index 0000000..0e37ef2 --- /dev/null +++ b/VisualCard/Specs/vcard-50-aptivi.txt @@ -0,0 +1,203 @@ +------------------------------------------------------------------------------------ + + ▲ Copyright (c) 2014-2023 Aptivi OSS +------------------------------------- + + This document is property of Aptivi OSS and the parent, Aptivi, and may + be viewed, edited, and re-distributed, as long as the license which comes + with this document is followed. It may not be re-sold. Any unauthorized + re-selling of any information at any price at the current currency, local + or foreign, are subject to actions determined by the criminal or civil laws. + + | <-- Margin is here 80-character width limit --> | + +------------------------------------------------------------------------------------ + + This document contains info about the vCard 5.0 specification. + + Date created: December 16th, 2023 Document ID: A-INT-0028-121623 + Date modified: Check the filesystem entry Doc Record: f7650b80-054ba689 + Author: Aptivi OSS Content SHA: a8d9b1437508ce58f + +------------------------------------------------------------------------------------ + +Specification Details +===================== + + This specification is basically the same as vCard 4.0, except that it is a fusion + of the three vCard versions with one feature removed. References below: + + - vCard 2.1 + https://github.com/Aptivi/VisualCard/blob/main/VisualCard/Specs/vcard-21.txt + - vCard 3.0 + https://github.com/Aptivi/VisualCard/blob/main/VisualCard/Specs/vcard-30-rfc + 2426.txt + - vCard 4.0 + https://github.com/Aptivi/VisualCard/blob/main/VisualCard/Specs/vcard-40-rfc + 6350.txt + + This new version of vCard is currently exclusive to VisualCard. Any application + that supports this version of vCard should print the below statement in their + appropriate "About" section (beginning of the application, About dialog box, etc- + etera): + + --------------------------------------------------------------------------------- + This application uses vCard 5.0, a revised version of vCard 4.0, made by Aptivi. + --------------------------------------------------------------------------------- + + Every vCard file that conforms to this spec should have the below contents: + + BEGIN:VCARD + VERSION:5.0 + (...) + END:VCARD + +Supported types +=============== + + According to the three versions of vCard in their own RFC documents, the + following types are supported on vCard 5.0: + + --------------------------------------------------------------------------------- + Key vCard 2.1 vCard 3.0 vCard 4.0 vCard 5.0 Difference + ================================================================================= + ADR Optional Optional Optional Optional + AGENT Optional Optional Undefined Optional + [Add] + ANNIVERSARY Undefined Undefined Optional Optional + BDAY Optional Optional Optional Optional + BEGIN Required Required Required Required + CALADRURI Undefined Undefined Optional Optional + CALURI Undefined Undefined Optional Optional + CATEGORIES Optional Optional Optional Optional + CLASS Undefined Optional Undefined Optional + [Add] + CLIENTPIDMAP Undefined Undefined Optional Undefined - [Del] + EMAIL Optional Optional Optional Optional + END Required Required Required Required + FBURL Undefined Undefined Optional Optional + FN Optional Required Required Required + GENDER Undefined Undefined Optional Optional + GEO Optional Optional Optional Optional + IMPP Undefined Maybe Optional Optional + KEY Optional Optional Optional Optional + KIND Undefined Undefined Optional Optional + LABEL Optional Optional Undefined Optional + [Add] + LANG Undefined Undefined Optional Optional + LOGO Optional Optional Optional Optional + MAILER Optional Optional Undefined Optional + [Add] + MEMBER Undefined Undefined Optional Undefined - [Del] + N Required Required Optional Required * [Mod] + NAME Undefined Optional Undefined Optional + [Add] + NICKNAME Undefined Optional Optional Optional + NOTE Optional Optional Optional Optional + ORG Optional Optional Optional Optional + PHOTO Optional Optional Optional Optional + PRODID Undefined Optional Optional Optional + PROFILE Optional Optional Undefined Optional + [Add] + RELATED Undefined Undefined Optional Undefined - [Del] + REV Optional Optional Optional Optional + ROLE Optional Optional Optional Optional + SORT-STRING Undefined Optional Undefined Optional + [Add] + SOUND Optional Optional Optional Optional + SOURCE Optional Optional Optional Optional + TEL Optional Optional Optional Optional + TITLE Optional Optional Optional Optional + TZ Optional Optional Optional Optional + UID Optional Optional Optional Undefined - [Del] + URL Optional Optional Optional Optional + VERSION Required Required Required Required + XML Undefined Undefined Optional Undefined - [Del] + ================================================================================= + 44 keys 13 changes + --------------------------------------------------------------------------------- + + Added features to vCard 5.0 are (7): + + - AGENT + - CLASS + - LABEL + - MAILER + - NAME + - PROFILE + - SORT-STRING + + Removed features from vCard 5.0 are (5): + + - CLIENTPIDMAP + - MEMBER + - RELATED + - UID + - XML + + Modified features on vCard 5.0 are (1): + + - N (Changed from Optional to Required) + + All the syntaxes are the same. However, all the features related to relationship, + such as UID, CLIENTPIDMAP, RELATED, etc. and their optional parameters for every + vCard value type, such as PID, are removed. Otherwise, all the features from the + three vCard versions exist. + +Examples +======== + + Here are the four examples for vCard 5.0: + + BEGIN:VCARD + VERSION:5.0 + FN:Rick Hood + N:Hood;Rick;;; + END:VCARD + + BEGIN:VCARD + VERSION:5.0 + ADR;TYPE=work:POBOX;;Street Address ExtAddress;Reg;Loc;Postal;Country + ADR;TYPE=home:;;Street Address;;;; + EMAIL;TYPE=HOME:neville.nvs@gmail.com + EMAIL;TYPE=WORK:neville.nvs@nvsc.com + FN:Neville Navasquillo + IMPP;TYPE=HOME:aim:IM + IMPP;TYPE=HOME:msn:Windows LIVE + IMPP;TYPE=HOME:ymsgr:Yahoo + N:Navasquillo;Neville;Neville\,Nevile;Mr.;Jr. + N;ALTID=0;LANGUAGE=de:NAVASQUILLO;Neville;Neville\,Nevile;Mr.;Jr. + NOTE:Notes + ORG:Organization + TEL;TYPE=work:098-765-4321 + TEL;TYPE=cell:1-234-567-890 + TEL;TYPE=voice:078-494-6434 + TEL;TYPE=home:348-404-8404 + TITLE:Title + X-ANDROID-CUSTOM:vnd.android.cursor.item/nickname;NVL.N;1;;;;;;;;;;;;; + END:VCARD + + BEGIN:VCARD + VERSION:5.0 + ADR;TYPE=home:;;New York\, USA;;;; + EMAIL;TYPE=HOME:sarah.s@gmail.com + EMAIL;TYPE=WORK:sarah.s@sso.org + FN:Sarah Santos + N:Santos;Sarah;;; + ORG:Support Scammer Outcry Organization + TEL;TYPE=cell:589-210-1059 + TITLE:Chief Executive Officer + URL:https://sso.org/ + X-SIP-SIP:sip test + SORT-STRING:sarahsantos + END:VCARD + + BEGIN:VCARD + VERSION:5.0 + ADR;TYPE=home:;;Los Angeles\, USA;;;; + EMAIL;TYPE=HOME:john.s@acme.co + FN:John Sanders + IMPP:aim:john.s + N:Sanders;John;;; + NOTE:Note test for VisualCard + ORG:Acme Co. + TEL;TYPE=cell:495-522-3560 + TITLE:Product Manager + X-ANDROID-CUSTOM:vnd.android.cursor.item/nickname;JS;1;;;;;;;;;;;;; + X-PHONETIC-FIRST-NAME:Saunders + X-PHONETIC-LAST-NAME:John + SORT-STRING:johnsanders + END:VCARD