{
  $Project$
  $Workfile$
  $Revision$
  $DateUTC$
  $Id$

  This file is part of the Indy (Internet Direct) project, and is offered
  under the dual-licensing agreement described on the Indy website.
  (http://www.indyproject.org/)

  Copyright:
   (c) 1993-2005, Chad Z. Hower and the Indy Pit Crew. All rights reserved.
}
{
  $Log$
}
{
  Rev 1.7    2004.10.27 9:17:52 AM  czhower
  For TIdStrings


  Rev 1.6    10/26/2004 10:54:16 PM  JPMugaas
  Updated refs.


  Rev 1.5    2004.02.08 2:43:32 PM  czhower
  Fixed compile error.


  Rev 1.4    2/7/2004 12:47:16 PM  JPMugaas
  Should work in DotNET and not touch the system settings at all.


  Rev 1.3    2004.02.03 5:44:42 PM  czhower
  Name changes


  Rev 1.2    1/21/2004 4:21:10 PM  JPMugaas
  InitComponent


  Rev 1.1    6/13/2003 08:19:52 AM  JPMugaas
  Should now compile with new codders.


  Rev 1.0    11/13/2002 08:04:32 AM  JPMugaas
}
unit IdVCard;

{*******************************************************}
{                                                       }
{       Indy VCardObject TIdCard                        }
{                                                       }
{       Copyright (C) 2000 Winshoes Working Group       }
{       Original author J. Peter Mugaas                 }
{       2000-May-06                                     }
{       Based on RFC 2425, 2426                         }
{                                                       }
{*******************************************************}

{

2002-Jan-20 DOn Siders
 - Corrected spelling errors in Categories properties, members, methods

2000-07-24 Peter Mee
 - Added preliminary embedded vCard checking
 - Added QP Check & Decode of individual properties
}

interface

{$i IdCompilerDefines.inc}

uses
  Classes,
  IdGlobal,
  IdBaseComponent;

{ TODO:

Agent property does not work and the current parsing stops whenever it
sees END:VCard meaning that the VCard will be truncated if AGENT is
used to embed a VCard.

I omitted a property for spelling out a sound.  Appearently VCard 2.1
permitted a charactor representation of sound in addition to an embedded
sound, and a URL.

I am not sure how well the KEY property works.  That is used for
embedding some encryption keys into a VCard such as PGP public-key or
something from Versign.

VCard does not have any Quoted Printable decoding or Base64 encoding
and decoding.  Some routines may have to be changed to accomodate
this although I don't have the where-with-all.   

VCards can not be saved. }

type

  {This contains the object for Sound, Logo, Photo, Key, and Agent property}
  TIdVCardEmbeddedObject = class(TPersistent)
  protected
    FObjectType : String;
    FObjectURL  : String;
    FBase64Encoded : Boolean;
    FEmbeddedData : TStrings;
    {Embeded data property set method}
    procedure SetEmbeddedData(const Value: TStrings);
  public
    constructor Create;
    destructor Destroy; override;
  published
    {this indicates the type of media such as the file type or key type}
    property ObjectType : String read FObjectType write FObjectType;
    {pointer to the URL where the object is located if it is NOT in this card
    itself}
    property ObjectURL  : String read FObjectURL write FObjectURL;
    {The object }
    property Base64Encoded : Boolean read FBase64Encoded write FBase64Encoded;
    {The data for the object if it is in the VCard.  This is usually in an
    encoded format such as BASE64 although some keys may not require encoding}
    property EmbeddedData : TStrings read FEmbeddedData write SetEmbeddedData;
  end;

  {VCard business information}
  TIdVCardBusinessInfo = class(TPersistent)
  protected
    FTitle : String;
    FRole : String;
    FOrganization : String;
    FDivisions : TStrings;
    procedure SetDivisions(Value : TStrings);
  public
    constructor Create;
    destructor Destroy; override;
  published
    {The organization name such as XYZ Corp. }
    property Organization : String read FOrganization write FOrganization;
    { The divisions in the orginization the person is in - e.g.
      West Virginia Office, Computing Service}
    property Divisions: TStrings read FDivisions write SetDivisions;
    {The person's formal title in the business such
     "Director of Computing Services"}
    property Title : String read FTitle write FTitle;
    {The person's role in an organization such as "system administrator" }    
    property Role : String read FRole write FRole;
  end;

  {Geographical information such as Latitude/Longitude and Time Zone}
  TIdVCardGeog = class(TPersistent)
  protected
    FLatitude : Real;
    FLongitude : Real;
    FTimeZoneStr : String;
  published
    {Geographical latitude the person is in}
    property Latitude : Real read FLatitude write  FLatitude;
    {Geographical longitude the person is in}
    property Longitude : Real read FLongitude write FLongitude;
    {The time zone the person is in}
    property TimeZoneStr : String read FTimeZoneStr write FTimeZoneStr;
  end;

  TIdPhoneAttribute = ( tpaHome, tpaVoiceMessaging, tpaWork, tpaPreferred,
    tpaVoice, tpaFax, tpaCellular, tpaVideo, tpaBBS, tpaModem, tpaCar,
    tpaISDN, tpaPCS, tpaPager );

  TIdPhoneAttributes = set of TIdPhoneAttribute;

  { This encapsolates a telephone number }
  TIdCardPhoneNumber = class(TCollectionItem)
  protected
    FPhoneAttributes: TIdPhoneAttributes;
    FNumber : String;
  public
    procedure Assign(Source: TPersistent); override;
  published
    {This is a descriptor for the phone number }
    property PhoneAttributes: TIdPhoneAttributes read FPhoneAttributes write FPhoneAttributes;
    { the telephone number itself}
    property Number : String read FNumber write FNumber;
  end;

  {Since a person can have more than one address, we put them into this collection}
  TIdVCardTelephones = class(TOwnedCollection)
  protected
    function GetItem(Index: Integer) : TIdCardPhoneNumber;
    procedure SetItem(Index: Integer; const Value: TIdCardPhoneNumber);
  public
    constructor Create(AOwner : TPersistent); reintroduce;
    function Add: TIdCardPhoneNumber;
    property Items[Index: Integer] : TIdCardPhoneNumber read GetItem write SetItem; default;
  end;

  TIdCardAddressAttribute = ( tatHome, tatDomestic, tatInternational, tatPostal, tatParcel, tatWork, tatPreferred );
  TIdCardAddressAttributes = set of TIdCardAddressAttribute;

  {This encapsulates a person's address}    {Do not Localize}
  TIdCardAddressItem = class(TCollectionItem)
  protected
    FAddressAttributes : TIdCardAddressAttributes;
    FPOBox : String;
    FExtendedAddress : String;
    FStreetAddress : String;
    FLocality : String;
    FRegion : String;
    FPostalCode : String;
    FNation : String;
  public
    procedure Assign(Source: TPersistent); override;
  published
    { attributes for this address such as Home or Work, postal, parcel, etc.}
    property AddressAttributes : TIdCardAddressAttributes read FAddressAttributes write FAddressAttributes;
    { This is the P. O. Box for an address}
    property POBox : String read FPOBox write FPOBox;
    { This could be something such as an Office identifier for a building or
      an appartment number }
    property ExtendedAddress : String read FExtendedAddress write FExtendedAddress;
    {This is the streat address such as "101 Sample Avenue" }
    property StreetAddress : String read FStreetAddress write FStreetAddress;
    { This is a city or town (e.g. Chicago, New York City, Montreol }
    property Locality : String read FLocality write FLocality;
    { This is the political subdivision of a nation such as a Providence in Canda - Quebec,
     a State in US such as "West Virginia", or a county in England such as "Kent"}
    property Region : String read FRegion write FRegion;
    { This is the postal code for the locality such as a ZIP Code in the US }
    property PostalCode : String read FPostalCode write FPostalCode;
    { This is the nation such as Canada, U.S.A., Mexico, Russia, etc }
    property Nation : String read FNation write FNation;
  end;

  {Since a person can have more than one address, we put them into this collection}
  TIdVCardAddresses = class(TOwnedCollection)
  protected
    function GetItem(Index: Integer) : TIdCardAddressItem;
    procedure SetItem(Index: Integer; const Value: TIdCardAddressItem);
  public
    constructor Create(AOwner : TPersistent); reintroduce;
    function Add: TIdCardAddressItem;
    property Items[Index: Integer] : TIdCardAddressItem read GetItem write SetItem; default;
  end;

  {This type holds a mailing label }
  TIdVCardMailingLabelItem = class(TCollectionItem)
  private
    FAddressAttributes : TIdCardAddressAttributes;
    FMailingLabel : TStrings;
    procedure SetMailingLabel(Value : TStrings);
  public
    constructor Create(Collection: TCollection); override;
    destructor Destroy; override;
    procedure Assign(Source: TPersistent); override;
  published
    { attributes for this mailing label such as Home or Work, postal, parcel,
      etc.}
    property AddressAttributes : TIdCardAddressAttributes read FAddressAttributes write FAddressAttributes;
    { The mailing label itself}
    property MailingLabel : TStrings read FMailingLabel write SetMailingLabel;
  end;

  {This type holds the }
  TIdVCardMailingLabels = class(TOwnedCollection)
  protected
    function GetItem(Index: Integer) : TIdVCardMailingLabelItem;
    procedure SetItem(Index: Integer; const Value: TIdVCardMailingLabelItem);
  public
    constructor Create(AOwner : TPersistent); reintroduce;
    function Add : TIdVCardMailingLabelItem;
    property Items[Index: Integer] : TIdVCardMailingLabelItem read GetItem write SetItem; default;
  end;

  { This type is used to indicate the type E-Mail indicated in the VCard
    which can be of several types }
  TIdVCardEMailType = (
    ematAOL, {America On-Line}
    ematAppleLink, {AppleLink}
    ematATT,   { AT&T Mail }
    ematCIS,   { CompuServe Information Service }
    emateWorld, { eWorld }
    ematInternet, {Internet SMTP (default)}
    ematIBMMail, { IBM Mail }
    ematMCIMail, { Indicates MCI Mail }
    ematPowerShare, { PowerShare }
    ematProdigy, { Prodigy information service }
    ematTelex, { Telex number }
    ematX400 { X.400 service }
  );

  {This object encapsolates an E-Mail address in a Collection}
  TIdVCardEMailItem = class(TCollectionItem)
  protected
    FEMailType : TIdVCardEMailType;
    FPreferred : Boolean;
    FAddress : String;
  public
    constructor Create(Collection: TCollection); override;
    { This is the type of E-Mail address which defaults to Internet }
    procedure Assign(Source: TPersistent); override;
  published
    property EMailType : TIdVCardEMailType read FEMailType write FEMailType;
    { Is this the person's prefered E-Mail address? }    {Do not Localize}
    property Preferred : Boolean read FPreferred write FPreferred;
    { The user's E-Mail address itself }    {Do not Localize}
    property Address : String read FAddress write FAddress;
  end;

  TIdVCardEMailAddresses = class(TOwnedCollection)
  protected
    function GetItem(Index: Integer) : TIdVCardEMailItem;
    procedure SetItem(Index: Integer; const Value: TIdVCardEMailItem);
  public
    constructor Create(AOwner : TPersistent); reintroduce;
    function Add: TIdVCardEMailItem;
    property Items[Index: Integer] : TIdVCardEMailItem read GetItem write SetItem; default;
  end;

  TIdVCardName = class(TPersistent)
  protected
    FFirstName : String;
    FSurName : String;
    FOtherNames : TStrings;
    FPrefix : String;
    FSuffix : String;
    FFormattedName : String;
    FSortName : String;
    FNickNames : TStrings;
    procedure SetOtherNames(Value : TStrings);
    procedure SetNickNames(Value : TStrings);
  public
    constructor Create;
    destructor Destroy; override;
  published
    {This is the person's first name, in the case of "J. Peter Mugaas",
    this would be "J."}
    property FirstName : String read FFirstName write FFirstName;
    {This is the person's last name, in the case of "J. Peter Mugaas",
    this would be "Mugaas"}
    property SurName : String read FSurName write FSurName;
    {This is a place for a middle name and some other names such as a woman's  
    maiden name.  In the case of "J. Peter Mugaas", this would be "Peter".}
    property OtherNames : TStrings read FOtherNames write SetOtherNames;
    {This is a properly formatted name which was listed in the VCard}
    property FormattedName : String read FFormattedName write FFormattedName;
    {This is a prefix added to a name such as
    "Mr.", "Dr.", "Hon.", "Prof.", "Reverend", etc.}
    property Prefix : String read FPrefix write FPrefix;
    {This is a suffix added to a name such as
    "Ph.D.", "M.D.", "Esq.", "Jr.", "Sr.", "III", etc.}
    property Suffix : String read FSuffix write FSuffix;
    {The string used for sorting a name.  It may not always be the person's last    
    name}
    property SortName : String read FSortName write  FSortName;
    { Nick names which a person may have such as "Bill" or "Billy" for Wiliam.}
    property NickNames : TStrings read FNickNames write SetNickNames;
  end;

  TIdVCard = class(TIdBaseComponent)
  protected
    FComments : TStrings;
    FCategories : TStrings;
    FBusinessInfo : TIdVCardBusinessInfo;
    FGeography : TIdVCardGeog;
    FFullName : TIdVCardName;
    FRawForm : TStrings;
    FURLs : TStrings;
    FEMailProgram : String;
    FEMailAddresses : TIdVCardEMailAddresses;
    FAddresses : TIdVCardAddresses;
    FMailingLabels : TIdVCardMailingLabels;
    FTelephones : TIdVCardTelephones;
    FVCardVersion : Real;
    FProductID : String;
    FUniqueID : String;
    FClassification : String;
    FLastRevised : TDateTime;
    FBirthDay : TDateTime;
    FPhoto : TIdVCardEmbeddedObject;
    FLogo  : TIdVCardEmbeddedObject;
    FSound : TIdVCardEmbeddedObject;
    FKey : TIdVCardEmbeddedObject;
    procedure SetComments(Value : TStrings);
    procedure SetCategories(Value : TStrings);
    procedure SetURLs(Value : TStrings);
    {This processes some types of variables after reading the string}
    procedure SetVariablesAfterRead;
    procedure InitComponent; override;
  public
    destructor Destroy; override;
    { This reads a VCard from a TStrings object }
    procedure ReadFromStrings(s : TStrings);
    { This is the raw form of the VCard }
    property RawForm : TStrings read FRawForm;
  published
    { This is the VCard specification version used }
    property VCardVersion : Real read FVCardVersion;
    { URL's associated with the VCard such as the person's or organication's  
    webpage.  There can be more than one.}
    property URLs : TStrings read FURLs write SetURLs;
    { This is the product ID for the program which created this VCard}
    property ProductID : String read FProductID write FProductID;
    { This is a unique indentifier for the VCard }
    property UniqueID : String read FUniqueID write FUniqueID;
    { Intent of the VCard owner for general access to information described by the vCard
     VCard.}
    property Classification : String read FClassification write FClassification;
    { This is the person's birthday and possibly, time of birth}    {Do not Localize}
    property BirthDay : TDateTime read FBirthDay write FBirthDay;
    { This is the person's name }    {Do not Localize}
    property FullName : TIdVCardName read FFullName write FFullName;
    { This is the E-Mail program used by the card's owner}    {Do not Localize}
    property EMailProgram : String read FEMailProgram write FEMailProgram;
    { This is a list of the person's E-Mail address }    {Do not Localize}
    property EMailAddresses : TIdVCardEMailAddresses read FEMailAddresses;
    { This is a list of telephone numbers }
    property Telephones : TIdVCardTelephones read FTelephones;
    { This is busines related information on a VCard}
    property BusinessInfo : TIdVCardBusinessInfo read  FBusinessInfo;
    { This is a list of Categories used for classification }
    property Categories : TStrings read FCategories write SetCategories;
    { This is a list of addresses }
    property Addresses : TIdVCardAddresses read FAddresses;
    { This is a list of mailing labels }
    property MailingLabels : TIdVCardMailingLabels read FMailingLabels;
    { This is a miscellaneous comments, additional information, or whatever the
     VCard wishes to say }
    property Comments : TStrings read FComments write SetComments;
    { The owner's photograph}    {Do not Localize}
    property Photo : TIdVCardEmbeddedObject read FPhoto;
    { Organization's logo}    {Do not Localize}
    property Logo  : TIdVCardEmbeddedObject read FLogo;
    { A sound associated with the VCard such as how to pronounce a person's name  
      or something cute }
    property Sound : TIdVCardEmbeddedObject read FSound;
    { This is for an encryption key such as S/MIME, VeriSign, or PGP }
    property Key : TIdVCardEmbeddedObject read FKey;
  end;

  //public for testing
  type
    TIdISO8601DateComps = record
      Year, Month, Day: UInt16;
    end;
    TIdISO8601TimeComps = record
      Hour, Min, Sec, MSec: UInt16;
      UTCOffset: String;
    end;

  function ParseISO8601Date(const DateString: string; var VDate: TIdISO8601DateComps): Boolean;
  function ParseISO8601Time(const DateString: string; var VTime: TIdISO8601TimeComps): Boolean;
  function ParseISO8601DateTime(const DateString: string; var VDate: TIdISO8601DateComps; var VTime: TIdISO8601TimeComps): Boolean;
  function ParseISO8601DateAndOrTime(const DateString: string; var VDate: TIdISO8601DateComps; var VTime: TIdISO8601TimeComps): Boolean;
  function ParseISO8601DateTimeStamp(const DateString: string; var VDate: TIdISO8601DateComps; var VTime: TIdISO8601TimeComps): Boolean;

  function ParseDateTimeStamp(const DateString: string): TDateTime; {$IFDEF HAS_DEPRECATED}deprecated{$IFDEF HAS_DEPRECATED_MSG} 'Use ParseISO8601DateTimeStamp()'{$ENDIF};{$ENDIF}

implementation

uses
  IdCoderQuotedPrintable,
  IdException,
  IdGlobalProtocols, SysUtils;

const VCardProperties : array [0..27] of string = (
  'FN', 'N', 'NICKNAME', 'PHOTO',    {Do not Localize}
  'BDAY', 'ADR', 'LABEL', 'TEL',    {Do not Localize}
  'EMAIL', 'MAILER', 'TZ', 'GEO',    {Do not Localize}
  'TITLE',  'ROLE', 'LOGO', 'AGENT',    {Do not Localize}
  'ORG', 'CATEGORIES', 'NOTE', 'PRODID',    {Do not Localize}
  'REV', 'SORT-STRING', 'SOUND', 'URL',    {Do not Localize}
  'UID', 'VERSION', 'CLASS', 'KEY'    {Do not Localize}
);

{ These constants are for testing the VCard for E-Mail types.
  Don't alter these }    {Do not Localize}
const EMailTypePropertyParameter : array [0..11] of string = (
  'AOL', {America On-Line}    {Do not Localize}
  'APPLELINK',   {AppleLink}    {Do not Localize}
  'ATTMAIL', { AT&T Mail }    {Do not Localize}
  'CIS', { CompuServe Information Service }    {Do not Localize}
  'EWORLD', { eWorld }    {Do not Localize}
  'INTERNET', {Internet SMTP (default) }    {Do not Localize}
  'IBMMAIL', { IBM Mail }    {Do not Localize}
  'MCIMAIL', { MCI Mail }    {Do not Localize}
  'POWERSHARE', { PowerShare }    {Do not Localize}
  'PRODIGY', { Prodigy information service }    {Do not Localize}
  'TLX', { Telex number }    {Do not Localize}
  'X400' { X.400 service }    {Do not Localize}
);

//This is designed for decimals as written in the English language.
//We require this because some protocols may require this as standard representation
//for floats
function IndyStrToFloat(const AStr: string): Extended;
var
  LBuf : String;
  LHi, LLo : UInt32;
  i : Integer;
begin
  LBuf := AStr;
  //strip off
  for i := Length(LBuf) downto 1 do begin
    if LBuf[i] = ',' then begin
     IdDelete(LBuf, i, 1);
    end;
  end;
  LHi := IndyStrToInt(Fetch(LBuf,'.'), 0);
  LBuf := PadString(LBuf, 2, '0');
  LLo := IndyStrToInt(Copy(LBuf,1,2), 0);
  Result := LHi + (LLo / 100);
end;

{This only adds Value to strs if it is not zero}
procedure AddValueToStrings(strs : TStrings; Value : String);
begin
  if Length(Value) > 0 then begin
    strs.Add(Value);
  end; //  if Legnth ( Value ) then
end;

{This parses a delinated string into a TStrings}
procedure ParseDelimiterToStrings(strs : TStrings; str : String; const Delimiter : Char = ',');    {Do not Localize}
begin
  while str <> '' do begin   {Do not Localize}
    AddValueToStrings(strs, Fetch(str, Delimiter));
  end;
end;

{This parses time stamp from DateString and returns it as TDateTime

Per RFC 2425 Section 5.8.4:

   date = date-fullyear ["-"] date-month ["-"] date-mday

   date-fullyear = 4 DIGIT

   date-month = 2 DIGIT     ;01-12

   date-mday = 2 DIGIT      ;01-28, 01-29, 01-30, 01-31
                            ;based on month/year

   time = time-hour [":"] time-minute [":"] time-second [time-secfrac] [time-zone]

   time-hour = 2 DIGIT      ;00-23

   time-minute = 2 DIGIT    ;00-59

   time-second = 2 DIGIT    ;00-60 (leap second)

   time-secfrac = "," 1*DIGIT

   time-zone = "Z" / time-numzone

   time-numzome = sign time-hour [":"] time-minute

   "date", "time", and "date-time": Each of these value types is based
   on a subset of the definitions in ISO 8601 standard. Profiles MAY
   place further restrictions on "date" and "time" values.  Multiple
   "date" and "time" values can be specified using the comma-separated
   notation, unless restricted by a profile.

       Examples for "date":
                   1985-04-12
                   1996-08-05,1996-11-11
                   19850412

       Examples for "time":
                   10:22:00
                   102200
                   10:22:00.33
                   10:22:00.33Z
                   10:22:33,11:22:00
                   10:22:00-08:00

       Examples for "date-time":
                   1996-10-22T14:00:00Z
                   1996-08-11T12:34:56Z
                   19960811T123456Z
                   1996-10-22T14:00:00Z,1996-08-11T12:34:56Z


Per RFC 2426 Section 4:

   date-value   = <A single date value as defined in [MIME-DIR]>

   time-value   = <A single time value as defined in [MIME-DIR]>

   date-time-value = <A single date-time value as defined in [MIME-DIR]

[MIME-DIR]    Howes, T., Smith, M., and F. Dawson, "A MIME Content-
                 Type for Directory Information", RFC 2425, September
                 1998.


Per RFC 6350 Section 4.3:

   "date", "time", "date-time", "date-and-or-time", and "timestamp":
   Each of these value types is based on the definitions in
   [ISO.8601.2004].  Multiple such values can be specified using the
   comma-separated notation.

   Only the basic format is supported.

4.3.1.  DATE

   A calendar date as specified in [ISO.8601.2004], Section 4.1.2.

   Reduced accuracy, as specified in [ISO.8601.2004], Sections 4.1.2.3
   a) and b), but not c), is permitted.

   Expanded representation, as specified in [ISO.8601.2004], Section
   4.1.4, is forbidden.

   Truncated representation, as specified in [ISO.8601.2000], Sections
   5.2.1.3 d), e), and f), is permitted.

   Examples for "date":

             19850412
             1985-04
             1985
             --0412
             ---12

   Note the use of YYYY-MM in the second example above.  YYYYMM is
   disallowed to prevent confusion with YYMMDD.  Note also that
   YYYY-MM-DD is disallowed since we are using the basic format instead
   of the extended format.

4.3.2.  TIME

   A time of day as specified in [ISO.8601.2004], Section 4.2.

   Reduced accuracy, as specified in [ISO.8601.2004], Section 4.2.2.3,
   is permitted.

   Representation with decimal fraction, as specified in
   [ISO.8601.2004], Section 4.2.2.4, is forbidden.

   The midnight hour is always represented by 00, never 24 (see
   [ISO.8601.2004], Section 4.2.3).

   Truncated representation, as specified in [ISO.8601.2000], Sections
   5.3.1.4 a), b), and c), is permitted.

   Examples for "time":

             102200
             1022
             10
             -2200
             --00
             102200Z
             102200-0800

4.3.3.  DATE-TIME

   A date and time of day combination as specified in [ISO.8601.2004],
   Section 4.3.

   Truncation of the date part, as specified in [ISO.8601.2000], Section
   5.4.2 c), is permitted.

   Examples for "date-time":

             19961022T140000
             --1022T1400
             ---22T14

4.3.4.  DATE-AND-OR-TIME

   Either a DATE-TIME, a DATE, or a TIME value.  To allow unambiguous
   interpretation, a stand-alone TIME value is always preceded by a "T".

   Examples for "date-and-or-time":

             19961022T140000
             --1022T1400
             ---22T14
             19850412
             1985-04
             1985
             --0412
             ---12
             T102200
             T1022
             T10
             T-2200
             T--00
             T102200Z
             T102200-0800

4.3.5.  TIMESTAMP

   A complete date and time of day combination as specified in
   [ISO.8601.2004], Section 4.3.2.

   Examples for "timestamp":

             19961022T140000
             19961022T140000Z
             19961022T140000-05
             19961022T140000-0500

}

function ParseISO8601Date(const DateString: string; var VDate: TIdISO8601DateComps): Boolean;
var
  Year, Month, Day: UInt16;
  Len: Integer;
begin
  // TODO: move this logic into IdGlobalProtocols.RawStrInternetToDateTime().ParseISO8601()

  Result := False;
  VDate.Year := 0;
  VDate.Month := 0;
  VDate.Day := 0;

  Len := Length(DateString);

  if (Len >= 10) and
     IsNumeric(DateString, 4, 1) and CharEquals(DateString, 5, '-') and
     IsNumeric(DateString, 2, 6) and CharEquals(DateString, 8, '-') and
     IsNumeric(DateString, 2, 9) then
  begin
    Year := IndyStrToInt(Copy(DateString, 1, 4));
    Month := IndyStrToInt(Copy(DateString, 6, 2));
    Day := IndyStrToInt(Copy(DateString, 9, 2));
    Dec(Len, 10);
  end
  else if (Len >= 8) and IsNumeric(DateString, 8, 1) then
  begin
    Year := IndyStrToInt(Copy(DateString, 1, 4));
    Month := IndyStrToInt(Copy(DateString, 5, 2));
    Day := IndyStrToInt(Copy(DateString, 7, 2));
    Dec(Len, 8);
  end else
  begin
    Day := 1;
    if (Len >= 7) and
       IsNumeric(DateString, 4, 1) and CharEquals(DateString, 5, '-') and
       IsNumeric(DateString, 2, 6) then
    begin
      Year := IndyStrToInt(Copy(DateString, 1, 4));
      Month := IndyStrToInt(Copy(DateString, 6, 2));
      Dec(Len, 7);
    end
    else if (Len >= 4) and IsNumeric(DateString, 4, 1) then
    begin
      Month := 1;
      Year := IndyStrToInt(Copy(DateString, 1, 4));
      Dec(Len, 4);
    end
    else if (Len >= 4) and CharEquals(DateString, 1, '-') and CharEquals(DateString, 2, '-') then
    begin
      Year := 0;
      if (Len >= 7) and IsNumeric(DateString, 2, 3) and CharEquals(DateString, 5, '-') and
         IsNumeric(DateString, 2, 6) then
      begin
        Month := IndyStrToInt(Copy(DateString, 3, 2));
        Day := IndyStrToInt(Copy(DateString, 6, 2));
        Dec(Len, 7);
      end
      else if (Len >= 6) and IsNumeric(DateString, 4, 3) then
      begin
        Month := IndyStrToInt(Copy(DateString, 3, 2));
        Day := IndyStrToInt(Copy(DateString, 5, 2));
        Dec(Len, 6)
      end
      else if (Len >= 5) and CharEquals(DateString, 3, '-') and IsNumeric(DateString, 2, 4) then
      begin
        Month := 1;
        Day := IndyStrToInt(Copy(DateString, 4, 2));
        Dec(Len, 5);
      end
      else if (Len >= 4) and IsNumeric(DateString, 2, 3) then
      begin
        Month := IndyStrToInt(Copy(DateString, 3, 2));
        Day := 1;
        Dec(Len, 4);
      end else begin
        Exit;
      end;
    end else begin
      Exit;
    end;
  end;

  if Len > 0 then begin
    Exit;
  end;

  VDate.Year := Year;
  VDate.Month := Month;
  VDate.Day := Day;

  Result := True;
end;

function ParseISO8601Time(const DateString: string; var VTime: TIdISO8601TimeComps): Boolean;
type
  eFracComp = (fracMin, fracSec, fracMSec);
var
  Hour, Min, Sec, MSec: UInt16;
  Len, Offset, TmpOffset, TmpLen, I, Numerator, Denominator: Integer;
  LMultiplier: Single;
  FracComp: eFracComp;
begin
  // TODO: move this logic into IdGlobalProtocols.RawStrInternetToDateTime().ParseISO8601()

  Result := False;
  VTime.Hour := 0;
  VTime.Min := 0;
  VTime.Sec := 0;
  VTime.MSec := 0;
  VTime.UTCOffset := '';

  Len := Length(DateString);
  MSec := 0;

  if (Len >= 8) and
     IsNumeric(DateString, 2, 1) and CharEquals(DateString, 3, ':') and
     IsNumeric(DateString, 2, 4) and CharEquals(DateString, 6, ':') and
     IsNumeric(DateString, 2, 7) then
  begin
    Hour := IndyStrToInt(Copy(DateString, 1, 2));
    Min := IndyStrToInt(Copy(DateString, 4, 2));
    Sec := IndyStrToInt(Copy(DateString, 7, 2));
    Offset := 9;
    Dec(Len, 8);
    FracComp := fracMSec;
  end
  else if (Len >= 6) and IsNumeric(DateString, 6, 1) then
  begin
    Hour := IndyStrToInt(Copy(DateString, 1, 2));
    Min := IndyStrToInt(Copy(DateString, 3, 2));
    Sec := IndyStrToInt(Copy(DateString, 5, 2));
    Offset := 7;
    Dec(Len, 6);
    FracComp := fracMSec;
  end
  else begin
    Sec := 0;
    if (Len >= 5) and
       IsNumeric(DateString, 2, 1) and CharEquals(DateString, 3, ':') and
       IsNumeric(DateString, 2, 4) then
    begin
      Hour := IndyStrToInt(Copy(DateString, 1, 2));
      Min := IndyStrToInt(Copy(DateString, 4, 2));
      Offset := 6;
      Dec(Len, 5);
      FracComp := fracSec;
    end
    else if (Len >= 4) and IsNumeric(DateString, 4, 1) then
    begin
      Hour := IndyStrToInt(Copy(DateString, 1, 2));
      Min := IndyStrToInt(Copy(DateString, 3, 2));
      Offset := 5;
      Dec(Len, 4);
      FracComp := fracSec;
    end else
    begin
      if (Len >= 2) and IsNumeric(DateString, 2, 1) then begin
        Min := 0;
        Hour := IndyStrToInt(Copy(DateString, 1, 2));
        Offset := 3;
        Dec(Len, 2);
        FracComp := fracMin;
      end
      else if (Len >= 3) and CharEquals(DateString, 1, '-') then
      begin
        Hour := 0;
        if (Len >= 6) and IsNumeric(DateString, 2, 2) and CharEquals(DateString, 4, ':') and
           IsNumeric(DateString, 2, 5) then
        begin
          Min := IndyStrToInt(Copy(DateString, 2, 2));
          Sec := IndyStrToInt(Copy(DateString, 5, 2));
          Offset := 7;
          Dec(Len, 6);
          FracComp := fracMSec;
        end
        else if (Len >= 5) and IsNumeric(DateString, 4, 2) then
        begin
          Min := IndyStrToInt(Copy(DateString, 2, 2));
          Sec := IndyStrToInt(Copy(DateString, 4, 2));
          Offset := 6;
          Dec(Len, 5);
          FracComp := fracMSec;
        end
        else if (Len >= 4) and CharEquals(DateString, 2, '-') and IsNumeric(DateString, 2, 3) then
        begin
          Min := 0;
          Sec := IndyStrToInt(Copy(DateString, 3, 2));
          Offset := 5;
          Dec(Len, 4);
          FracComp := fracMSec;
        end
        else if (Len >= 3) and IsNumeric(DateString, 2, 2) then
        begin
          Min := IndyStrToInt(Copy(DateString, 3, 2));
          Sec := 0;
          Offset := 4;
          Dec(Len, 3);
          FracComp := fracSec;
        end else begin
          Exit;
        end;
      end else begin
        Exit;
      end;
    end;
  end;

  if (Len > 0) and CharIsInSet(DateString, Offset, '.,') then
  begin
    Inc(Offset);
    Dec(Len);

    Numerator := 0;
    Denominator := 1;
    for I := 0 to 8 do
    begin
      if Len = 0 then begin
        Break;
      end;
      if not IsNumeric(DateString[Offset]) then begin
        Break;
      end;
      Numerator := (Numerator * 10) + (Ord(DateString[Offset]) - Ord('0'));
      if Numerator < 0 then begin // overflow
        Exit;
      end;
      Denominator := Denominator * 10;
      Inc(Offset);
      Dec(Len);
    end;
    LMultiplier := Numerator / Denominator;

    case FracComp of
      fracMin: begin
        Min := UInt16(Trunc(60 * LMultiplier));
      end;
      fracSec: begin
        Sec := UInt16(Trunc(60 * LMultiplier));
      end;
      fracMSec: begin
        MSec := UInt16(Trunc(1000 * LMultiplier));
      end;
    end;
  end;

  if Len > 0 then
  begin
    TmpOffset := Offset;
    TmpLen := Len;
    if not CharIsInSet(DateString, Offset, '+-') then
    begin
      // TODO: parse time zones other than "Z" into offsets
      if CharEquals(DateString, Offset, 'Z') then begin
        Dec(Len);
      end;
    end else
    begin
      Inc(Offset);
      Dec(Len);
      if (Len >= 5) and
         IsNumeric(DateString, 2, Offset) and CharEquals(DateString, Offset+2, ':') and
         IsNumeric(DateString, 2, Offset+3) then
      begin
        Dec(Len, 5);
      end
      else if (Len >= 4) and IsNumeric(DateString, 4, Offset) then
      begin
        Dec(Len, 4);
      end
      else if (Len >= 2) and IsNumeric(DateString, 2, Offset) then
      begin
        Dec(Len, 2);
      end
      else begin
        Exit;
      end;
    end;
    if Len > 0 then begin
      Exit;
    end;
    Offset := TmpOffset;
    Len := TmpLen;
  end;

  VTime.Hour := Hour;
  VTime.Min := Min;
  VTime.Sec := Sec;
  VTime.MSec := MSec;
  VTime.UTCOffset := Copy(DateString, Offset, Len);

  Result := True;
end;

function ParseISO8601DateTime(const DateString: string; var VDate: TIdISO8601DateComps; var VTime: TIdISO8601TimeComps): Boolean;
var
  I: Integer;
begin
  // TODO: move this logic into IdGlobalProtocols.RawStrInternetToDateTime().ParseISO8601()

  Result := False;
  VDate.Year := 0;
  VDate.Month := 0;
  VDate.Day := 0;
  VTime.Hour := 0;
  VTime.Min := 0;
  VTime.Sec := 0;
  VTime.MSec := 0;
  VTime.UTCOffset := '';

  I := Pos('T', DateString);
  if I <> 0 then begin
    Result := ParseISO8601Date(Copy(DateString, 1, I-1), VDate) and
              ParseISO8601Time(Copy(DateString, I+1, MaxInt), VTime);
  end;
end;

function ParseISO8601DateAndOrTime(const DateString: string; var VDate: TIdISO8601DateComps; var VTime: TIdISO8601TimeComps): Boolean;
var
  I: Integer;
begin
  // TODO: move this logic into IdGlobalProtocols.RawStrInternetToDateTime().ParseISO8601()

  Result := False;
  VDate.Year := 0;
  VDate.Month := 0;
  VDate.Day := 0;
  VTime.Hour := 0;
  VTime.Min := 0;
  VTime.Sec := 0;
  VTime.MSec := 0;
  VTime.UTCOffset := '';

  I := Pos('T', DateString);
  if I = 0 then begin
    Result := ParseISO8601Date(DateString, VDate);
    Exit;
  end;

  if I > 1 then begin
    if not ParseISO8601Date(Copy(DateString, 1, I-1), VDate) then begin
      Exit;
    end;
  end;

  if not ParseISO8601Time(Copy(DateString, I+1, MaxInt), VTime) then begin
    Exit;
  end;

  Result := True;
end;

function ParseISO8601DateTimeStamp(const DateString: String; var VDate: TIdISO8601DateComps; var VTime: TIdISO8601TimeComps): Boolean;
{$IFDEF USE_INLINE}inline;{$ENDIF}
begin
  // TODO: how is TIMESTAMP different from DATE-TIME?
  Result := ParseISO8601DateTime(DateString, VDate, VTime);
end;

function ParseDateTimeStamp(const DateString: string): TDateTime;
var
  LDate: TIdISO8601DateComps;
  LTime: TIdISO8601TimeComps;
begin
  if ParseISO8601DateTimeStamp(DateString, LDate, LTime) then begin
    Result := EncodeDate(LDate.Year, LDate.Month, LDate.Day) + EncodeTime(LTime.Hour, LTime.Min, LTime.Sec, LTime.MSec);
  end else begin
    Result := 0.0;
  end;
end;

{This function returns a stringList with an item's
attributes and sets value to the value of the item}
function GetAttributesAndValue(Data : String; var Value : String) : TStringList;
var
  Buff, Buff2 : String;
begin
  Result := TStringList.Create;
  try
    if IndyPos(':', Data) <> 0 then    {Do not Localize}
    begin
      Buff := Fetch(Data, ':');    {Do not Localize}
      {This handles a VCard property attribute delimiter ","}
      Buff := ReplaceAll(Buff, ',', ';');    {Do not Localize}
      while Buff <> '' do begin   {Do not Localize}
        Buff2 := Fetch(Buff, ';');    {Do not Localize}
        if Length(Buff2) > 0 then begin
          Result.Add(Buff2);
        end;
      end;
    end;
    Value := Data;
  except
    FreeAndNil(Result);
    raise;
  end;
end;

{This parses the organization line from OrgString into}
procedure ParseOrg(OrgObj : TIdVCardBusinessInfo; OrgStr : String);
begin
  { Organization name }
  OrgObj.Organization := Fetch(OrgStr, ';');
  { Divisions }
  ParseDelimiterToStrings(OrgObj.Divisions, OrgStr, ';');    {Do not Localize}
end;

{This parses the geography latitude and longitude from GeogStr and
 puts it in Geog}
procedure ParseGeography(Geog : TIdVCardGeog; GeogStr : String);
begin
  {Latitude}
  Geog.Latitude := IndyStrToFloat(Fetch(GeogStr, ';'));    {Do not Localize}
  {Longitude}
  Geog.Longitude := IndyStrToFloat(Fetch(GeogStr, ';'));    {Do not Localize}
end;

{This parses  PhoneStr and places the attributes in PhoneObj }
procedure ParseTelephone(PhoneObj : TIdCardPhoneNumber; PhoneStr : String);
const
  TelephoneTypePropertyParameter : array [0..13] of string  = (
    'HOME', 'MSG',   'WORK', 'PREF',  'VOICE',  'FAX',    {Do not Localize}
    'CELL', 'VIDEO',  'BBS', 'MODEM', 'CAR',   'ISDN',    {Do not Localize}
    'PCS', 'PAGER'    {Do not Localize}
  );
var
  Value : String;
  idx : Integer;
  Attribs : TStringList;
begin
  attribs := GetAttributesAndValue(PhoneStr, Value);
  try
    for idx := 0 to Attribs.Count-1 do begin
      case PosInStrArray(attribs[idx], TelephoneTypePropertyParameter, False) of
        { home }
        0 : Include(PhoneObj.FPhoneAttributes, tpaHome);
        { voice messaging }
        1 : Include(PhoneObj.FPhoneAttributes, tpaVoiceMessaging);
        { work }
        2 : Include(PhoneObj.FPhoneAttributes, tpaWork);
        { preferred }
        3 : Include(PhoneObj.FPhoneAttributes, tpaPreferred);
        { Voice }
        4 : Include(PhoneObj.FPhoneAttributes, tpaVoice);
        { Fax }
        5 : Include(PhoneObj.FPhoneAttributes, tpaFax);
        { Cellular phone }
        6 : Include(PhoneObj.FPhoneAttributes, tpaCellular);
        { Video conferancing number }
        7 : Include(PhoneObj.FPhoneAttributes, tpaVideo);
        { Bulleton Board System (BBS) telephone number }
        8 : Include(PhoneObj.FPhoneAttributes, tpaBBS);
        { MODEM Connection number }
        9 : Include(PhoneObj.FPhoneAttributes, tpaModem);
        { Car phone number }
       10 : Include(PhoneObj.FPhoneAttributes, tpaCar);
        { ISDN Service Number }
       11 : Include(PhoneObj.FPhoneAttributes, tpaISDN);
       { personal communication services telephone number }
       12 : Include(PhoneObj.FPhoneAttributes, tpaPCS);
       { pager }
       13 : Include(PhoneObj.FPhoneAttributes, tpaPager);
      end;
    end;
    { default telephon number }
    if Attribs.Count = 0 then begin
      PhoneObj.PhoneAttributes := [tpaVoice];
    end;
    PhoneObj.Number := Value;
  finally
    FreeAndNil(attribs);
  end;
end;

{This parses AddressStr and places the attributes in AddressObj }
procedure ParseAddress(AddressObj : TIdCardAddressItem; AddressStr : String);
const
  AttribsArray : array[0..6] of String = (
    'HOME', 'DOM', 'INTL', 'POSTAL', 'PARCEL', 'WORK', 'PREF'    {Do not Localize}
  );
var
  Value : String;
  Attribs : TStringList;
  idx : Integer;
begin
  Attribs := GetAttributesAndValue(AddressStr, Value);
  try
    for idx := 0 to Attribs.Count-1 do begin
      case PosInStrArray(attribs[idx], AttribsArray, False) of
        { home }
        0 : Include(AddressObj.FAddressAttributes, tatHome);
        { domestic }
        1 : Include(AddressObj.FAddressAttributes, tatDomestic);
        { international }
        2 : Include(AddressObj.FAddressAttributes, tatInternational);
        { Postal }
        3 : Include(AddressObj.FAddressAttributes, tatPostal);
        { Parcel }
        4 : Include(AddressObj.FAddressAttributes, tatParcel);
        { Work }
        5 : Include(AddressObj.FAddressAttributes, tatWork);
        { Preferred }
        6 : Include(AddressObj.FAddressAttributes, tatPreferred);
      end;
    end;
    if Attribs.Count = 0 then begin
      AddressObj.AddressAttributes := [tatInternational, tatPostal, tatParcel, tatWork];
    end;
    AddressObj.POBox := Fetch(Value, ';');    {Do not Localize}
    AddressObj.ExtendedAddress := Fetch(Value, ';');    {Do not Localize}
    AddressObj.StreetAddress := Fetch(Value, ';');    {Do not Localize}
    AddressObj.Locality := Fetch(Value, ';');    {Do not Localize}
    AddressObj.Region := Fetch (Value, ';');    {Do not Localize}
    AddressObj.PostalCode := Fetch(Value, ';');    {Do not Localize}
    AddressObj.Nation := Fetch (Value, ';');    {Do not Localize}
  finally
    FreeAndNil(Attribs);
  end;
end;

{This parses LabelStr and places the attributes in TIdVCardMailingLabelItem }
procedure ParseMailingLabel(LabelObj : TIdVCardMailingLabelItem; LabelStr : String);
const
  AttribsArray : array[0..6] of String = (
    'HOME', 'DOM', 'INTL', 'POSTAL', 'PARCEL', 'WORK', 'PREF'    {Do not Localize}
  );
var
  Value : String;
  Attribs : TStringList;
  idx : Integer;
begin
  Attribs := GetAttributesAndValue(LabelStr, Value);
  try
    for idx := 0 to Attribs.Count-1 do begin
      case PosInStrArray(attribs[idx], AttribsArray, False) of
        { home }
        0 : Include(LabelObj.FAddressAttributes, tatHome);
        { domestic }
        1 : Include(LabelObj.FAddressAttributes, tatDomestic);
        { international }
        2 : Include(LabelObj.FAddressAttributes, tatInternational);
        { Postal }
        3 : Include(LabelObj.FAddressAttributes, tatPostal);
        { Parcel }
        4 : Include(LabelObj.FAddressAttributes, tatParcel);
        { Work }
        5 : Include(LabelObj.FAddressAttributes, tatWork);
        { Preferred }
        6 : Include(LabelObj.FAddressAttributes, tatPreferred);
      end;
    end;
    {Default Values}
    if Attribs.Count = 0 then begin
      LabelObj.AddressAttributes := [tatInternational,  tatPostal, tatParcel, tatWork];
    end;
    LabelObj.MailingLabel.Add(Value);
  finally
    FreeAndNil(Attribs);
  end;
end;

{This parses the Name and places the name in the TIdVCardName}
procedure ParseName(NameObj : TIdVCardName; NameStr : String);
var
  OtherNames : String;
begin
  { surname }
  NameObj.SurName := Fetch(NameStr, ';');    {Do not Localize}
  { first name }
  NameObj.FirstName := Fetch(NameStr, ';');    {Do not Localize}
  { middle and other names}
  OtherNames := Fetch(NameStr, ';');    {Do not Localize}
  { Prefix }
  NameObj.Prefix := Fetch(NameStr, ';');    {Do not Localize}
  { Suffix }
  NameObj.Suffix := Fetch(NameStr, ';');    {Do not Localize}
  OtherNames := ReplaceAll(OtherNames, ' ', ',');    {Do not Localize}
  ParseDelimiterToStrings(NameObj.OtherNames, OtherNames);
end;

{This parses EMailStr and places the attributes in EMailObj }
procedure ParseEMailAddress(EMailObj : TIdVCardEMailItem; EMailStr : String);
var
  Value : String;
  Attribs : TStringList;
  idx : Integer;
  {this is for testing the type so we can break out of the loop}
  ps : Integer;

  function IsPreferred: Boolean;
  var
    idx2: Integer;
  begin
    for idx2 := 0 to Attribs.Count-1 do begin
      if TextIsSame(Attribs[idx2], 'PREF') then begin    {Do not Localize}
        Result := True;
        Exit;
      end;
    end;
    Result := False;
  end;

begin
  Attribs := GetAttributesAndValue (EMailStr, Value);
  try
    EMailObj.Address := Value;
    EMailObj.Preferred := IsPreferred;
    for idx := 0 to Attribs.Count-1 do begin
      ps := PosInStrArray(Attribs[idx], EMailTypePropertyParameter);
      if ps <> -1 then begin
        case ps of
          0 : EMailObj.EMailType := ematAOL; {America On-Line}
          1 : EMailObj.EMailType := ematAppleLink; {AppleLink}
          2 : EMailObj.EMailType := ematATT;   { AT&T Mail }
          3 : EMailObj.EMailType := ematCIS;   { CompuServe Information Service }
          4 : EMailObj.EMailType := emateWorld; { eWorld }
          5 : EMailObj.EMailType := ematInternet; {Internet SMTP (default)}
          6 : EMailObj.EMailType := ematIBMMail; { IBM Mail }
          7 : EMailObj.EMailType := ematMCIMail; { Indicates MCI Mail }
          8 : EMailObj.EMailType := ematPowerShare; { PowerShare }
          9 : EMailObj.EMailType := ematProdigy; { Prodigy information service }
         10 : EMailObj.EMailType := ematTelex; { Telex number }
         11 : EMailObj.EMailType := ematX400; { X.400 service }
        end;
        Break;
      end;
    end;
  finally
    FreeAndNil(Attribs);
  end;
end;

{ TIdVCard }

procedure TIdVCard.InitComponent;
begin
  inherited InitComponent;
  FPhoto := TIdVCardEmbeddedObject.Create;
  FLogo  := TIdVCardEmbeddedObject.Create;
  FSound := TIdVCardEmbeddedObject.Create;
  FKey := TIdVCardEmbeddedObject.Create;
  FComments := TStringList.Create;
  FCategories := TStringList.Create;
  FBusinessInfo := TIdVCardBusinessInfo.Create;
  FGeography := TIdVCardGeog.Create;
  FFullName := TIdVCardName.Create;
  FRawForm := TStringList.Create;
  FEMailAddresses := TIdVCardEMailAddresses.Create(Self);
  FAddresses := TIdVCardAddresses.Create(Self);
  FTelephones := TIdVCardTelephones.Create(Self);
  FURLs := TStringList.Create;
  FMailingLabels := TIdVCardMailingLabels.Create(Self);
end;

destructor TIdVCard.Destroy;
begin
  FreeAndNil(FKey);
  FreeAndNil(FPhoto);
  FreeAndNil(FLogo);
  FreeAndNil(FSound);
  FreeAndNil(FComments);
  FreeAndNil(FMailingLabels);
  FreeAndNil(FCategories);
  FreeAndNil(FBusinessInfo);
  FreeAndNil(FGeography);
  FreeAndNil(FURLs);
  FreeAndNil(FTelephones);
  FreeAndNil(FAddresses);
  FreeAndNil(FEMailAddresses);
  FreeAndNil(FFullName);
  FreeAndNil(FRawForm);
  inherited Destroy;
end;

procedure TIdVCard.ReadFromStrings(s: TStrings);
var
  idx, level : Integer;
begin
  FRawForm.Clear;
  {Find the begin mark and accomodate broken VCard implemntations}
  level := 0;
  for idx := 0 to s.Count-1 do begin
    if TextIsSame(Trim(s[idx]), 'BEGIN:VCARD') then begin    {Do not Localize}
      Break;
    end;
  end;
  {Keep adding until end VCard }
  while idx < s.Count do begin
    if Length(s[idx]) > 0 then begin
      case PosInStrArray(Trim(s[idx]), ['BEGIN:VCARD', 'END:VCARD'], False) of {Do not Localize}
        0: begin
          // Have a new object - increment the counter & add
          Inc(level);
        end;
        1: begin
          // Have an END:
          Dec(level);
        end;
      end;
      // regardless of content, add it 
      FRawForm.Add(s[idx]);
      if level < 1 then begin
        Break;
      end;
    end;
    Inc(idx);
  end;
  SetVariablesAfterRead;
end;

procedure TIdVCard.SetCategories(Value: TStrings);
begin
  FCategories.Assign(Value);
end;

procedure TIdVCard.SetComments(Value: TStrings);
begin
  FComments.Assign(Value);
end;

procedure TIdVCard.SetURLs(Value: TStrings);
begin
  FURLs.Assign(Value);
end;

procedure TIdVCard.SetVariablesAfterRead;
var
  idx : Integer;
//  OrigLine : String;
  Line : String;
  Attribs : String;
  Data : String;
  Test : String;
  Colon : Integer;
  SColon : Integer;
  ColonFind : Integer;
  QPCoder : TIdDecoderQuotedPrintable;

  {These subroutines increment idx to prevent unneded comparisons of folded lines}

  function UnfoldLines : String;
  begin
    Result := '';    {Do not Localize}
    Inc(idx);
    while (idx < FRawForm.Count) and CharIsInSet(FRawForm[idx], 1, ' '#9) do    {Do not Localize}
    begin
      Result := Result + Trim(FRawForm[idx]);
      Inc(idx);
    end; // while
    {Correct for increment in the main while loop}
    Dec(idx);
  end;

  procedure ProcessAgent;
  begin
    // The current idx of FRawForm could be an embedded vCard.
    { TODO : Eliminate embedded vCard }
  end;

  procedure ParseEmbeddedObject(EmObj : TIdVCardEmbeddedObject; StLn : String);
  var
    Value : String;
    LAttribs : TStringList;
    idx2 : Integer;
    {this is for testing the type so we can break out of the loop}
  begin
    LAttribs := GetAttributesAndValue(StLn, Value);
    try
      for idx2 := 0 to LAttribs.Count-1 do begin
        if PosInStrArray(LAttribs[idx2], ['ENCODING=BASE64', 'BASE64']) <> -1 then begin   {Do not Localize}
          emObj.Base64Encoded := True;
        end
        else if PosInStrArray(LAttribs[idx2], ['VALUE=URI', 'VALUE=URL', 'URI', 'URL']) = -1 then begin   {Do not Localize}
          emObj.ObjectType := LAttribs[idx2];
        end;
      end;
      if (LAttribs.IndexOf('VALUE=URI') > -1) or    {Do not Localize}
          (LAttribs.IndexOf('VALUE=URL') > -1) or    {Do not Localize}
          (LAttribs.IndexOf('URI') > -1) or    {Do not Localize}
          (LAttribs.IndexOf('URL') > -1)  then    {Do not Localize}
      begin
        emObj.ObjectURL := Value + UnfoldLines;
      end else begin
        AddValueToStrings(EmObj.EmbeddedData, Value);
        {Add any folded lines}
        Inc(idx);
        while (idx < FRawForm.Count) and CharIsInSet(FRawForm[idx], 1, ' '#9) do begin   {Do not Localize}
          AddValueToStrings(EmObj.EmbeddedData, Trim(FRawForm[idx]));
          Inc(idx);
        end;
        {Correct for increment in the main while loop}
        Dec(idx);
      end;
    finally
      FreeAndNil(LAttribs);
    end;
  end;

  function GetDateTimeValue(St: String): TDateTime;
  var
    LAttribs: String;
    LDate: TIdISO8601DateComps;
    LTime: TIdISO8601TimeComps;
  begin
    Result := 0.0;

    // TODO: parse the attributes into a proper list
    LAttribs := UpperCase(Attribs);

    if IndyPos('TIMESTAMP', LAttribs) <> 0 then begin   {Do not Localize}
      if ParseISO8601DateTimeStamp(St, LDate, LTime) then begin
        Result := EncodeDate(LDate.Year, LDate.Month, LDate.Day) + EncodeTime(LTime.Hour, LTime.Min, LTime.Sec, LTime.MSec);
        // TODO: use LTime.UTCOffset if available
      end;
    end
    else if IndyPos('DATE-AND-OR-TIME', LAttribs) <> 0 then begin   {Do not Localize}
      if ParseISO8601DateAndOrTime(st, LDate, LTime) then begin
        if (LDate.Year <> 0) or (LDate.Month <> 0) or (LDate.Day <> 0) then begin
          Result := EncodeDate(LDate.Year, LDate.Month, LDate.Day);
        end;
        if (LTime.Hour <> 0) or (LTime.Min <> 0) or (LTime.Sec <> 0) or (LTime.MSec <> 0) then begin
          Result := Result + EncodeTime(LTime.Hour, LTime.Min, LTime.Sec, LTime.MSec);
          // TODO: use LTime.UTCOffset if available
        end;
      end;
    end
    else if IndyPos('DATE-TIME', LAttribs) <> 0 then begin   {Do not Localize}
      if ParseISO8601DateTime(st, LDate, LTime) then begin
        Result := EncodeDate(LDate.Year, LDate.Month, LDate.Day) + EncodeTime(LTime.Hour, LTime.Min, LTime.Sec, LTime.MSec);
        // TODO: use LTime.UTCOffset if available
      end;
    end
    else if IndyPos('DATE', LAttribs) <> 0 then begin   {Do not Localize}
      if ParseISO8601Date(st, LDate) then begin
        Result := EncodeDate(LDate.Year, LDate.Month, LDate.Day);
      end;
    end
    else if IndyPos('TIME', LAttribs) <> 0 then begin   {Do not Localize}
      if ParseISO8601Time(st, LTime) then begin
        Result := EncodeTime(LTime.Hour, LTime.Min, LTime.Sec, LTime.MSec);
        // TODO: use LTime.UTCOffset if available
      end;
    end else begin
      if ParseISO8601DateAndOrTime(st, LDate, LTime) then begin
        if (LDate.Year <> 0) or (LDate.Month <> 0) or (LDate.Day <> 0) then begin
          Result := EncodeDate(LDate.Year, LDate.Month, LDate.Day);
        end;
        if (LTime.Hour <> 0) or (LTime.Min <> 0) or (LTime.Sec <> 0) or (LTime.MSec <> 0) then begin
          Result := Result + EncodeTime(LTime.Hour, LTime.Min, LTime.Sec, LTime.MSec);
          // TODO: use LTime.UTCOffset if available
        end;
      end;
    end;
  end;

begin
  // At this point, FRawForm contains the entire vCard - including possible
  // embedded vCards.

  QPCoder := TIdDecoderQuotedPrintable.Create(Self);
  try
    idx := 0;
    while idx < FRawForm.Count do
    begin
      // Grab the line
      Line := FRawForm[idx];

      {We separate the property name from the parameters and values here.
      We have be careful because sometimes a property in a vCard is separed by a
      ; or : even if the RFC and standards don't permit this   
      - broken VCard creation tools }
      Colon := IndyPos(':', Line);    {Do not Localize}

      // Store the property & complete attributes
      // TODO: use a TStringList instead...
      Attribs := Copy(Line, 1, Colon - 1);

      // Must now check for Quoted-printable attribute.  vCard v2.1 allows
      // QP to be used in any field.

      //****  Begin QP check & decode
      if IndyPos('QUOTED-PRINTABLE', UpperCase(Attribs)) > 0 then begin   {Do not Localize}
        // First things first - make a copy of the Line.
      //  OrigLine := Line;

        // Set Data to be the data contained on this line of the vCard
        Data := Copy(Line, Colon + 1, MaxInt);

        // The problem with QP-embedded objects is that the Colon character is
        // not standard QP-encoded... however, it is the only reliable way to
        // discover the next property.  So loop here until the next property is
        // found (i.e., the next line with a colon).
        Inc(idx);
        ColonFind := IndyPos(':', FRawForm[idx]);    {Do not Localize}
        while ColonFind = 0 do begin
          Data := Data + TrimLeft(FRawForm[idx]);
          Inc(idx);
          if idx <> FRawForm.Count then begin
            ColonFind := IndyPos(':', FRawForm[idx]);    {Do not Localize}
          end else begin
            ColonFind := 1;
          end;
        end;
        // Return idx to this property's (last) line    {Do not Localize}
        Dec(idx);

        Data := QPCoder.DecodeString(Data);

        // Now reorganise property so that it does not have a QP attribute.
        ColonFind := IndyPos(';', Attribs);    {Do not Localize}
        Line := '';    {Do not Localize}
        while ColonFind <> 0 do begin
          Test := Copy(Attribs, 1, ColonFind);
          if IndyPos('QUOTED-PRINTABLE', UpperCase(Test)) = 0 then begin   {Do not Localize}
            // Add to Line.
            Line := Line + Test;
          end;
          Attribs := Copy(Attribs, ColonFind + 1, MaxInt);
          ColonFind := IndyPos(';', Attribs);    {Do not Localize}
        end;

        // Clean up variables
        if Length(Attribs) <> 0 then begin
          // Does Quoted-Printable occur in what's left?    {Do not Localize}
          if IndyPos('QUOTED-PRINTABLE', UpperCase(Attribs)) = 0 then begin   {Do not Localize}
            // Add to line
            Line := Line + Attribs;
          end;
        end;

        // Check if the last char of Line is a semi-colon.  If so, remove it.
        ColonFind := Length(Line);
        If ColonFind > 0 then
        begin
          if Line[ColonFind] = ';' then begin   {Do not Localize}
            Line := Copy(Line, 1, ColonFind - 1);
          end;
        end;
        Line := Line + ':' + Data;    {Do not Localize}
      end;
      //****  End QP check & decode

      Colon := IndyPos(':', Line);    {Do not Localize}
      SColon := IndyPos(';', Line);    {Do not Localize}
      if (Colon < SColon) or (SColon = 0) then begin
        Line := ReplaceOnlyFirst(Line, ':', ';');    {Do not Localize}
      end;

      // Grab the property name
      Test := Fetch(Line, ';');    {Do not Localize}

      // Discover which property it is.
      case PosInStrArray(Test, VCardProperties, False) of
        {'FN'}    {Do not Localize}
        0 : FFullName.FormattedName := Line + UnfoldLines;
        {'N'}    {Do not Localize}
        1 : ParseName(FFullName, Line + UnfoldLines);
        {'NICKNAME'}    {Do not Localize}
        2 : ParseDelimiterToStrings(FFullName.NickNames, Line + UnfoldLines);
        {'PHOTO'}    {Do not Localize}
        3 : ParseEmbeddedObject(FPhoto, Line);
        {'BDAY'}    {Do not Localize}
        4 : FBirthDay := GetDateTimeValue(Line + UnfoldLines);
        {'ADR'}    {Do not Localize}
        5 : ParseAddress(FAddresses.Add, Line + UnfoldLines);
        {'LABEL'}    {Do not Localize}
        6 : ParseMailingLabel(FMailingLabels.Add, Line + UnfoldLines);
        {'TEL'}    {Do not Localize}
        7 : ParseTelephone(FTelephones.Add, Line + UnfoldLines);
        {'EMAIL'}    {Do not Localize}
        8 : ParseEMailAddress(FEMailAddresses.Add, Line + UnfoldLines);
        {'MAILER'}    {Do not Localize}
        9 : FEMailProgram := Line + UnfoldLines;
        {'TZ'}    {Do not Localize}
       10 : FGeography.TimeZoneStr := Line + UnfoldLines;
        {'GEO'}    {Do not Localize}
       11 : ParseGeography(FGeography, Line + UnfoldLines);
        {'TITLE'}    {Do not Localize}
       12 : FBusinessInfo.Title := Line + UnfoldLines;
        {'ROLE'}    {Do not Localize}
       13 : FBusinessInfo.Role := Line + UnfoldLines;
        {'LOGO'}    {Do not Localize}
       14 : ParseEmbeddedObject (FLogo, Line);
        {'AGENT'}    {Do not Localize}
       15 : ProcessAgent;
        {'ORG'}    {Do not Localize}
       16 : ParseOrg(FBusinessInfo, Line + UnfoldLines);
        {'CATEGORIES'}    {Do not Localize}
       17 :  ParseDelimiterToStrings(FCategories, Line + UnfoldLines);
         {'NOTE'}    {Do not Localize}
       18 :  FComments.Add(Line + UnfoldLines);
        {'PRODID' }    {Do not Localize}
       19 : FProductID := Line + UnfoldLines;
        {'REV'}    {Do not Localize}
       20 : FLastRevised := GetDateTimeValue(Line + UnfoldLines);
        {'SORT-STRING'}    {Do not Localize}
       21 : FFullName.SortName := Line + UnfoldLines;
        {'SOUND'}    {Do not Localize}
       22 :  ParseEmbeddedObject(FSound, Line);
        {'URL'}    {Do not Localize}
       23 : AddValueToStrings(FURLs, Line + UnfoldLines);
        {'UID'}    {Do not Localize}
       24 : FUniqueID := Line + UnfoldLines;
        {'VERSION'}    {Do not Localize}
       25 : FVCardVersion := IndyStrToFloat(Line  + UnfoldLines);
        {'CLASS'}    {Do not Localize}
       26 :  FClassification := Line + UnfoldLines;
        {'KEY'}    {Do not Localize}
       27 : ParseEmbeddedObject(FKey, Line);
      end;
      Inc(idx);
    end;
  finally
    FreeAndNil(QPCoder);
  end;
end;

{ TIdVCardEMailAddresses }

function TIdVCardEMailAddresses.Add: TIdVCardEMailItem;
begin
  Result := TIdVCardEMailItem(inherited Add);
end;

constructor TIdVCardEMailAddresses.Create(AOwner : TPersistent);
begin
  inherited Create(AOwner, TIdVCardEMailItem);
end;

function TIdVCardEMailAddresses.GetItem(Index: Integer): TIdVCardEMailItem;
begin
  Result := TIdVCardEMailItem(inherited Items[Index]);
end;

procedure TIdVCardEMailAddresses.SetItem(Index: Integer; const Value: TIdVCardEMailItem);
begin
  inherited SetItem(Index, Value);
end;

{ TIdVCardEMailItem }

procedure TIdVCardEMailItem.Assign(Source: TPersistent);
var
  EMail : TIdVCardEMailItem;
begin
  if Source is TIdVCardEMailItem then begin
    EMail := Source as TIdVCardEMailItem;
    EMailType := EMail.EMailType;
    Preferred := EMail.Preferred;
    Address := EMail.Address;
  end else begin
    inherited Assign(Source);
  end;
end;

constructor TIdVCardEMailItem.Create(Collection: TCollection);
begin
  inherited Create(Collection);
  FEMailType := ematInternet;
end;

{ TIdVCardAddresses }

function TIdVCardAddresses.Add: TIdCardAddressItem;
begin
  Result := TIdCardAddressItem(inherited Add);
end;

constructor TIdVCardAddresses.Create(AOwner : TPersistent);
begin
  inherited Create(AOwner, TIdCardAddressItem);
end;

function TIdVCardAddresses.GetItem(Index: Integer): TIdCardAddressItem;
begin
  Result := TIdCardAddressItem(inherited Items[Index]);
end;

procedure TIdVCardAddresses.SetItem(Index: Integer; const Value: TIdCardAddressItem);
begin
  inherited SetItem(Index, Value);
end;

{ TIdVCardTelephones }

function TIdVCardTelephones.Add: TIdCardPhoneNumber;
begin
  Result := TIdCardPhoneNumber(inherited Add);
end;

constructor TIdVCardTelephones.Create(AOwner : TPersistent);
begin
   inherited Create(AOwner, TIdCardPhoneNumber);
end;

function TIdVCardTelephones.GetItem(Index: Integer): TIdCardPhoneNumber;
begin
  Result := TIdCardPhoneNumber(inherited Items[Index]);
end;

procedure TIdVCardTelephones.SetItem(Index: Integer; const Value: TIdCardPhoneNumber);
begin
  inherited SetItem(Index, Value);
end;

{ TIdVCardName }

constructor TIdVCardName.Create;
begin
  inherited Create;
  FOtherNames := TStringList.Create;
  FNickNames := TStringList.Create;
end;

destructor TIdVCardName.Destroy;
begin
  FreeAndNil(FNickNames);
  FreeAndNil(FOtherNames);
  inherited Destroy;
end;

procedure TIdVCardName.SetNickNames(Value: TStrings);
begin
  FNickNames.Assign(Value);
end;

procedure TIdVCardName.SetOtherNames(Value: TStrings);
begin
  FOtherNames.Assign(Value);
end;

{ TIdVCardBusinessInfo }

constructor TIdVCardBusinessInfo.Create;
begin
  inherited Create;
  FDivisions := TStringList.Create;
end;

destructor TIdVCardBusinessInfo.Destroy;
begin
  FreeAndNil(FDivisions);
  inherited Destroy;
end;

procedure TIdVCardBusinessInfo.SetDivisions(Value: TStrings);
begin
  FDivisions.Assign(Value);
end;

{ TIdVCardMailingLabelItem }

procedure TIdVCardMailingLabelItem.Assign(Source: TPersistent);
var
  lbl : TIdVCardMailingLabelItem;
begin
  if Source is TIdVCardMailingLabelItem then begin
    lbl := Source as TIdVCardMailingLabelItem;
    AddressAttributes := lbl.AddressAttributes;
    MailingLabel.Assign(lbl.MailingLabel);
  end else begin
    inherited Assign(Source);
  end;
end;

constructor TIdVCardMailingLabelItem.Create(Collection: TCollection);
begin
  inherited Create(Collection);
  FMailingLabel := TStringList.Create;
end;

destructor TIdVCardMailingLabelItem.Destroy;
begin
  FreeAndNil(FMailingLabel);
  inherited Destroy;
end;

procedure TIdVCardMailingLabelItem.SetMailingLabel(Value: TStrings);
begin
  FMailingLabel.Assign(Value);
end;

{ TIdVCardMailingLabels }

function TIdVCardMailingLabels.Add: TIdVCardMailingLabelItem;
begin
  Result := TIdVCardMailingLabelItem(inherited Add);
end;

constructor TIdVCardMailingLabels.Create(AOwner: TPersistent);
begin
  inherited Create(AOwner, TIdVCardMailingLabelItem);
end;

function TIdVCardMailingLabels.GetItem(Index: Integer): TIdVCardMailingLabelItem;
begin
  Result := TIdVCardMailingLabelItem(inherited GetItem(Index));
end;

procedure TIdVCardMailingLabels.SetItem(Index: Integer; const Value: TIdVCardMailingLabelItem);
begin
  inherited SetItem(Index, Value);
end;

{ TIdEmbeddedObject }

constructor TIdVCardEmbeddedObject.Create;
begin
  inherited Create;
  FEmbeddedData := TStringList.Create;
end;

destructor TIdVCardEmbeddedObject.Destroy;
begin
  FreeAndNil(FEmbeddedData);
  inherited Destroy;
end;

procedure TIdVCardEmbeddedObject.SetEmbeddedData(const Value: TStrings);
begin
  FEmbeddedData.Assign(Value);
end;

{ TIdCardPhoneNumber }

procedure TIdCardPhoneNumber.Assign(Source: TPersistent);
var
  Phone : TIdCardPhoneNumber;
begin
  if Source is TIdCardPhoneNumber then begin
    Phone := Source as TIdCardPhoneNumber;
    PhoneAttributes := Phone.PhoneAttributes;
    Number := Phone.Number;
  end else begin
    inherited Assign(Source);
  end;
end;

{ TIdCardAddressItem }

procedure TIdCardAddressItem.Assign(Source: TPersistent);
var
  LAddr : TIdCardAddressItem;
begin
  if Source is TIdCardAddressItem then begin
    LAddr := Source as TIdCardAddressItem;
    AddressAttributes := LAddr.AddressAttributes;
    POBox := LAddr.POBox;
    ExtendedAddress := LAddr.ExtendedAddress;
    StreetAddress := LAddr.StreetAddress;
    Locality := LAddr.Locality;
    Region := LAddr.Region;
    PostalCode := LAddr.PostalCode;
    Nation := LAddr.Nation;
  end else begin
    inherited Assign(Source);
  end;
end;

end.
