Class: Vpim::Vcard

Inherits:
DirectoryInfo show all
Defined in:
lib/vpim/vcard.rb

Overview

A vCard, a specialization of a directory info object.

The vCard format is specified by:

  • RFC2426: vCard MIME Directory Profile (vCard 3.0)

  • RFC2425: A MIME Content-Type for Directory Information

This implements vCard 3.0, but it is also capable of working with vCard 2.1 if used with care.

All line values can be accessed with Vcard#value, Vcard#values, or even by iterating through Vcard#lines. Line types that don’t have specific support and non-standard line types (“X-MY-SPECIAL”, for example) will be returned as a String, with any base64 or quoted-printable encoding removed.

Specific support exists to return more useful values for the standard vCard types, where appropriate.

The wrapper functions (#birthday, #nicknames, #emails, etc.) exist partially as an API convenience, and partially as a place to document the values returned for the more complex types, like PHOTO and EMAIL.

For types that do not sensibly occur multiple times (like BDAY or GEO), sometimes a wrapper exists only to return a single line, using #value. However, if you find the need, you can still call #values to get all the lines, and both the singular and plural forms will eventually be implemented.

For more information see:

  • RFC2426: vCard MIME Directory Profile (vCard 3.0)

  • RFC2425: A MIME Content-Type for Directory Information

  • vCard2.1: vCard 2.1 Specifications

vCards are usually transmitted in files with .vcf extensions.

Examples

Defined Under Namespace

Classes: Address, Email, Line, Maker, Name, Telephone

Constant Summary collapse

@@decode =
{
  'BEGIN'      => :decode_invisible, # Don't return delimiter
  'END'        => :decode_invisible, # Don't return delimiter
  'FN'         => :decode_invisible, # Returned as part of N.
   'ADR'        => :decode_address,
  'AGENT'      => :decode_agent,
  'BDAY'       => :decode_bday,
  'CATEGORIES' => :decode_list_of_text,
  'EMAIL'      => :decode_email,
  'GEO'        => :decode_geo,
  'KEY'        => :decode_attachment,
  'LOGO'       => :decode_attachment,
  'MAILER'     => :decode_text,
  'N'          => :decode_n,
  'NAME'       => :decode_text,
  'NICKNAME'   => :decode_list_of_text,
  'NOTE'       => :decode_text,
  'ORG'        => :decode_structured_text,
  'PHOTO'      => :decode_attachment,
  'PRODID'     => :decode_text,
  'PROFILE'    => :decode_text,
  'REV'        => :decode_date_or_datetime,
  'ROLE'       => :decode_text,
  'SOUND'      => :decode_attachment,
  'SOURCE'     => :decode_text,
  'TEL'        => :decode_telephone,
  'TITLE'      => :decode_text,
  'UID'        => :decode_text,
  'URL'        => :decode_uri,
  'VERSION'    => :decode_version,
}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from DirectoryInfo

#check_begin_end, #delete, #dirty, #each, #encode, #enum_by_cond, #enum_by_group, #enum_by_name, #field, #fields, #groups, #push, #push_end, #push_unique, #text

Constructor Details

#initialize(fields, profile) ⇒ Vcard

:nodoc:



626
627
628
629
# File 'lib/vpim/vcard.rb', line 626

def initialize(fields, profile) #:nodoc:
  @cache = {}
  super(fields, profile)
end

Instance Attribute Details

#cacheObject (readonly)

Cache of decoded lines/fields, so we don’t have to decode a field more than once.



559
560
561
# File 'lib/vpim/vcard.rb', line 559

def cache
  @cache
end

Class Method Details

.create(fields = []) ⇒ Object

Create a vCard 3.0 object with the minimum required fields, plus any fields you want in the card (they can also be added later).



633
634
635
636
# File 'lib/vpim/vcard.rb', line 633

def Vcard.create(fields = [] )
  fields.unshift Field.create('VERSION', "3.0")
  super(fields, 'VCARD')
end

.decode(card) ⇒ Object

Decode a collection of vCards into an array of Vcard objects.

card can be either a String or an IO object.

Since vCards are self-delimited (by a BEGIN:vCard and an END:vCard), multiple vCards can be concatenated into a single directory info object. They may or may not be related. For example, AddressBook.app (the OS X contact manager) will export multiple selected cards in this format.

Input data will be converted from unicode if it is detected. The heuristic is based on the first bytes in the string:

  • 0xEF 0xBB 0xBF: UTF-8 with a BOM, the BOM is stripped

  • 0xFE 0xFF: UTF-16 with a BOM (big-endian), the BOM is stripped and string is converted to UTF-8

  • 0xFF 0xFE: UTF-16 with a BOM (little-endian), the BOM is stripped and string is converted to UTF-8

  • 0x00 ‘B’ or 0x00 ‘b’: UTF-16 (big-endian), the string is converted to UTF-8

  • ‘B’ 0x00 or ‘b’ 0x00: UTF-16 (little-endian), the string is converted to UTF-8

If you know that you have only one vCard, then you can decode that single vCard by doing something like:

vcard = Vcard.decode(card_data).first

Note: Should the import encoding be remembered, so that it can be reencoded in the same format?



664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
# File 'lib/vpim/vcard.rb', line 664

def Vcard.decode(card)
  if card.respond_to? :to_str
    string = card.to_str
  elsif card.respond_to? :read
    string = card.read(nil)
  else
    raise ArgumentError, "Vcard.decode cannot be called with a #{card.type}"
  end

  string.force_encoding "BINARY"

  case string
    when /^\xEF\xBB\xBF/
      string = string.sub("\xEF\xBB\xBF", '')
    when /^\xFE\xFF/
      arr = string.unpack('n*')
      arr.shift
      string = arr.pack('U*')
    when /^\xFF\xFE/
      arr = string.unpack('v*')
      arr.shift
      string = arr.pack('U*')
    when /^\x00B/i
      string = string.unpack('n*').pack('U*')
    when /^B\x00/i
      string = string.unpack('v*').pack('U*')
  end

  string.force_encoding "utf-8"

  entities = Vpim.expand(Vpim.decode(string))

  # Since all vCards must have a begin/end, the top-level should consist
  # entirely of entities/arrays, even if its a single vCard.
  if entities.detect { |e| ! e.kind_of? Array }
    raise "Not a valid vCard"
  end

  vcards = []

  for e in entities
    vcards.push(new(e.flatten, 'VCARD'))
  end

  vcards
end

Instance Method Details

#[](name, type = nil) ⇒ Object

The value of the field named name, optionally limited to fields of type type. If no match is found, nil is returned, if multiple matches are found, the first match to have one of its type values be ‘PREF’ (preferred) is returned, otherwise the first match is returned.

FIXME - this will become an alias for #value.



717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
# File 'lib/vpim/vcard.rb', line 717

def [](name, type=nil)
  fields = enum_by_name(name).find_all { |f| type == nil || f.type?(type) }

  valued = fields.select { |f| f.value != '' }
  if valued.first
    fields = valued
  end

  # limit to preferred, if possible
  pref = fields.select { |f| f.pref? }

  if pref.first
    fields = pref
  end

  fields.first ? fields.first.value : nil
end

#address(type = nil) ⇒ Object

The first ADR value of type type, a Address. Any of the location or delivery attributes of Address can be used as type. A wrapper around #value(‘ADR’, type).



804
805
806
# File 'lib/vpim/vcard.rb', line 804

def address(type=nil)
  value('ADR', type)
end

#addressesObject

The ADR values, an array of Address. If a block is given, the values are yielded. A wrapper around #values(‘ADR’).



810
811
812
# File 'lib/vpim/vcard.rb', line 810

def addresses #:yield:address
  values('ADR')
end

#agentsObject

The AGENT values. Each AGENT value is either a String, a Uri, or a Vcard. If a block is given, the values are yielded. A wrapper around #values(‘AGENT’).



817
818
819
# File 'lib/vpim/vcard.rb', line 817

def agents #:yield:agent
  values('AGENT')
end

#birthdayObject

The BDAY value as either a Date or a DateTime, or nil if there is none.

If the BDAY value is invalidly formatted, a feeble heuristic is applied to find the month and year, and return a Date in the current year.



825
826
827
# File 'lib/vpim/vcard.rb', line 825

def birthday
  value('BDAY')
end

#categoriesObject

The CATEGORIES values, an array of String. A wrapper around #value(‘CATEGORIES’).



831
832
833
# File 'lib/vpim/vcard.rb', line 831

def categories
  value('CATEGORIES')
end

#decode_address(field) ⇒ Object

:nodoc:



480
481
482
# File 'lib/vpim/vcard.rb', line 480

def decode_address(field) #:nodoc:
  Line.new( field.group, field.name, Address.decode(self, field) )
end

#decode_agent(field) ⇒ Object

:nodoc:



506
507
508
509
510
511
512
513
514
515
516
517
# File 'lib/vpim/vcard.rb', line 506

def decode_agent(field) #:nodoc:
  case field.kind
  when 'text'
    decode_text(field)
  when 'uri'
    decode_uri(field)
  when 'vcard', nil
    Line.new( field.group, field.name, Vcard.decode(Vpim.decode_text(field.value_raw)).first )
  else
    raise InvalidEncodingError, "AGENT type #{field.kind} is not allowed"
  end
end

#decode_attachment(field) ⇒ Object

:nodoc:



519
520
521
# File 'lib/vpim/vcard.rb', line 519

def decode_attachment(field) #:nodoc:
  Line.new( field.group, field.name, Attachment.decode(field, 'binary', 'TYPE') )
end

#decode_bday(field) ⇒ Object

:nodoc:



454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
# File 'lib/vpim/vcard.rb', line 454

def decode_bday(field) #:nodoc:
  begin
    return decode_date_or_datetime(field)

  rescue Vpim::InvalidEncodingError
    # Hack around BDAY dates hat are correct in the month and day, but have
    # some kind of garbage in the year.
    if field.value =~ /^\s*(\d+)-(\d+)-(\d+)\s*$/
      y = $1.to_i
      m = $2.to_i
      d = $3.to_i
      if(y < 1900)
        y = Time.now.year
      end
      Line.new( field.group, field.name, Date.new(y, m, d) )
    else
      raise
    end
  end
end

#decode_date_or_datetime(field) ⇒ Object

:nodoc:



444
445
446
447
448
449
450
451
452
# File 'lib/vpim/vcard.rb', line 444

def decode_date_or_datetime(field) #:nodoc:
  date = nil
  begin
    date = Vpim.decode_date_to_date(field.value_raw)
  rescue Vpim::InvalidEncodingError
    date = Vpim.decode_date_time_to_datetime(field.value_raw)
  end
  Line.new( field.group, field.name, date )
end

#decode_default(field) ⇒ Object

:nodoc:



428
429
430
# File 'lib/vpim/vcard.rb', line 428

def decode_default(field) #:nodoc:
  Line.new( field.group, field.name, field.value )
end

#decode_email(field) ⇒ Object

:nodoc:



484
485
486
# File 'lib/vpim/vcard.rb', line 484

def decode_email(field) #:nodoc:
  Line.new( field.group, field.name, Email.decode(field) )
end

#decode_geo(field) ⇒ Object

:nodoc:



475
476
477
478
# File 'lib/vpim/vcard.rb', line 475

def decode_geo(field) #:nodoc:
  geo = Vpim.decode_list(field.value_raw, ';') do |item| item.to_f end
  Line.new( field.group, field.name, geo )
end

#decode_invisible(field) ⇒ Object

:nodoc:



424
425
426
# File 'lib/vpim/vcard.rb', line 424

def decode_invisible(field) #:nodoc:
  nil
end

#decode_list_of_text(field) ⇒ Object

:nodoc:



492
493
494
495
496
# File 'lib/vpim/vcard.rb', line 492

def decode_list_of_text(field) #:nodoc:
  Line.new( field.group, field.name,
           Vpim.decode_text_list(field.value_raw).select{|t| t.length > 0}.uniq
          )
end

#decode_n(field) ⇒ Object

:nodoc:



440
441
442
# File 'lib/vpim/vcard.rb', line 440

def decode_n(field) #:nodoc:
  Line.new( field.group, field.name, Name.new(field.value, self['FN']).freeze )
end

#decode_structured_text(field) ⇒ Object

:nodoc:



498
499
500
# File 'lib/vpim/vcard.rb', line 498

def decode_structured_text(field) #:nodoc:
  Line.new( field.group, field.name, Vpim.decode_text_list(field.value_raw, ';') )
end

#decode_telephone(field) ⇒ Object

:nodoc:



488
489
490
# File 'lib/vpim/vcard.rb', line 488

def decode_telephone(field) #:nodoc:
  Line.new( field.group, field.name, Telephone.decode(field) )
end

#decode_text(field) ⇒ Object

:nodoc:



436
437
438
# File 'lib/vpim/vcard.rb', line 436

def decode_text(field) #:nodoc:
  Line.new( field.group, field.name, Vpim.decode_text(field.value_raw) )
end

#decode_uri(field) ⇒ Object

:nodoc:



502
503
504
# File 'lib/vpim/vcard.rb', line 502

def decode_uri(field) #:nodoc:
  Line.new( field.group, field.name, Attachment::Uri.new(field.value, nil) )
end

#decode_version(field) ⇒ Object

:nodoc:



432
433
434
# File 'lib/vpim/vcard.rb', line 432

def decode_version(field) #:nodoc:
  Line.new( field.group, field.name, (field.value.to_f * 10).to_i )
end

#delete_ifObject

Delete line if block yields true.



1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
# File 'lib/vpim/vcard.rb', line 1006

def delete_if #:nodoc: :yield: line
  # Do in two steps to not mess up progress through the enumerator.
  rm = []

  each do |f|
    line = f2l(f)
    if line && yield(line)
      rm << f

      # Hack - because we treat N and FN as one field
      if f.name? 'N'
        rm << field('FN')
      end
    end
  end

  rm.each do |f|
    @fields.delete( f )
    @cache.delete( f )
  end

end

#email(type = nil) ⇒ Object

The first EMAIL value of type type, a Email. Any of the location attributes of Email can be used as type. A wrapper around #value(‘EMAIL’, type).



838
839
840
# File 'lib/vpim/vcard.rb', line 838

def email(type=nil)
  value('EMAIL', type)
end

#emailsObject

The EMAIL values, an array of Email. If a block is given, the values are yielded. A wrapper around #values(‘EMAIL’).



844
845
846
# File 'lib/vpim/vcard.rb', line 844

def emails #:yield:email
  values('EMAIL')
end

#f2l(field) ⇒ Object

Return line for a field



586
587
588
589
590
591
592
# File 'lib/vpim/vcard.rb', line 586

def f2l(field) #:nodoc:
  begin
    Line.decode(@@decode, self, field)
  rescue InvalidEncodingError
    # Skip invalidly encoded fields.
  end
end

#geoObject

The GEO value, an Array of two Floats, [ latitude, longitude]. North of the equator is positive latitude, east of the meridian is positive longitude. See RFC2445 for more info, there are lots of special cases and RFC2445’s description is more complete thant RFC2426.



852
853
854
# File 'lib/vpim/vcard.rb', line 852

def geo
  value('GEO')
end

#keys(&proc) ⇒ Object

Return an Array of KEY Line#value, or yield each Line#value if a block is given. A wrapper around #values(‘KEY’).

KEY is a public key or authentication certificate associated with the object that the vCard represents. It is not commonly used, but could contain a X.509 or PGP certificate.

See Attachment for a description of the value.



864
865
866
# File 'lib/vpim/vcard.rb', line 864

def keys(&proc) #:yield: Line.value
  values('KEY', &proc)
end

#lines(name = nil) ⇒ Object

With no block, returns an Array of Line. If name is specified, the Array will only contain the Lines with that name. The Array may be empty.

If a block is given, each Line will be yielded instead of being returned in an Array.



600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
# File 'lib/vpim/vcard.rb', line 600

def lines(name=nil) #:yield: Line
  # FIXME - this would be much easier if #lines was #each, and there was a
  # different #lines that returned an Enumerator that used #each
  unless block_given?
    map do |f|
      if( !name || f.name?(name) )
       f2l(f)
      else
        nil
      end
    end.compact
  else
    each do |f|
      if( !name || f.name?(name) )
        line = f2l(f)
        if line
          yield line
        end
      end
    end
    self
  end
end

#logos(&proc) ⇒ Object

Return an Array of LOGO Line#value, or yield each Line#value if a block is given. A wrapper around #values(‘LOGO’).

LOGO is a graphic image of a logo associated with the object the vCard represents. Its not common, but would probably be equivalent to the logo on a printed card.

See Attachment for a description of the value.



876
877
878
# File 'lib/vpim/vcard.rb', line 876

def logos(&proc) #:yield: Line.value
  values('LOGO', &proc)
end

#makeObject

Make changes to a vCard.

Yields a Vpim::Vcard::Maker that can be used to modify this vCard.



999
1000
1001
1002
1003
# File 'lib/vpim/vcard.rb', line 999

def make #:yield: maker
  Vpim::Vcard::Maker.make2(self) do |maker|
    yield maker
  end
end

#nameObject

The N and FN as a Name object.

N is required for a vCards, this raises InvalidEncodingError if there is no N so it cannot return nil.



886
887
888
# File 'lib/vpim/vcard.rb', line 886

def name
  value('N') || raise(Vpim::InvalidEncodingError, "Missing mandatory N field")
end

#nicknameObject

The first NICKNAME value, nil if there are none.



891
892
893
894
895
# File 'lib/vpim/vcard.rb', line 891

def nickname
  v = value('NICKNAME')
  v = v.first if v
  v
end

#nicknamesObject

The NICKNAME values, an array of String. The array may be empty.



898
899
900
# File 'lib/vpim/vcard.rb', line 898

def nicknames
  values('NICKNAME').flatten.uniq
end

#noteObject

The NOTE value, a String. A wrapper around #value(‘NOTE’).



903
904
905
# File 'lib/vpim/vcard.rb', line 903

def note
  value('NOTE')
end

#orgObject

The ORG value, an Array of String. The first string is the organization, subsequent strings are departments within the organization. A wrapper around #value(‘ORG’).



910
911
912
# File 'lib/vpim/vcard.rb', line 910

def org
  value('ORG')
end

#photos(&proc) ⇒ Object

Return an Array of PHOTO Line#value, or yield each Line#value if a block is given. A wrapper around #values(‘PHOTO’).

PHOTO is an image or photograph information that annotates some aspect of the object the vCard represents. Commonly there is one PHOTO, and it is a photo of the person identified by the vCard.

See Attachment for a description of the value.



922
923
924
# File 'lib/vpim/vcard.rb', line 922

def photos(&proc) #:yield: Line.value
  values('PHOTO', &proc)
end

#sounds(&proc) ⇒ Object

Return an Array of SOUND Line#value, or yield each Line#value if a block is given. A wrapper around #values(‘SOUND’).

SOUND is digital sound content information that annotates some aspect of the vCard. By default this type is used to specify the proper pronunciation of the name associated with the vCard. It is not commonly used. Also, note that there is no mechanism available to specify that the SOUND is being used for anything other than the default.

See Attachment for a description of the value.



944
945
946
# File 'lib/vpim/vcard.rb', line 944

def sounds(&proc) #:yield: Line.value
  values('SOUND', &proc)
end

#telephone(type = nil) ⇒ Object

The first TEL value of type type, a Telephone. Any of the location or capability attributes of Telephone can be used as type. A wrapper around #value(‘TEL’, type).



953
954
955
# File 'lib/vpim/vcard.rb', line 953

def telephone(type=nil)
  value('TEL', type)
end

#telephonesObject

The TEL values, an array of Telephone. If a block is given, the values are yielded. A wrapper around #values(‘TEL’).



959
960
961
# File 'lib/vpim/vcard.rb', line 959

def telephones #:yield:tel
  values('TEL')
end

#titleObject

The TITLE value, a text string specifying the job title, functional position, or function of the object the card represents. A wrapper around #value(‘TITLE’).



966
967
968
# File 'lib/vpim/vcard.rb', line 966

def title
  value('TITLE')
end

#urlObject

The URL value, a Attachment::Uri. A wrapper around #value(‘URL’).



973
974
975
# File 'lib/vpim/vcard.rb', line 973

def url
  value('URL')
end

#urlsObject

The URL values, an Attachment::Uri. A wrapper around #values(‘URL’).



978
979
980
# File 'lib/vpim/vcard.rb', line 978

def urls
  values('URL')
end

#value(name, type = nil) ⇒ Object

Return the Line#value for a specific name, and optionally for a specific type.

If no line with the name (and, optionally, type) exists, nil is returned.

If multiple lines exist, the order of preference is:

  • lines with values over lines without

  • lines with a type of ‘pref’ over lines without

If multiple lines are equally preferred, then the first line will be returned.

This is most useful when looking for a line that can not occur multiple times, or when the line can occur multiple times, and you want to pick the first preferred line of a specific type. See #values if you need to access all the lines.

Note that the type field parameter is used for different purposes by the various kinds of vCard lines, but for the addressing lines (ADR, LABEL, TEL, EMAIL) it is has a reasonably consistent usage. Each addressing line can occur multiple times, and a type of ‘pref’ indicates that a particular line is the preferred line. Other type values tend to indicate some information about the location (‘home’, ‘work’, …) or some detail about the address (‘cell’, ‘fax’, ‘voice’, …). See the methods for the specific types of line for information about supported types and their meaning.



761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
# File 'lib/vpim/vcard.rb', line 761

def value(name, type = nil)
  v = nil

  fields = enum_by_name(name).find_all { |f| type == nil || f.type?(type) }

  valued = fields.select { |f| f.value != '' }
  if valued.first
    fields = valued
  end

  pref = fields.select { |f| f.pref? }

  if pref.first
    fields = pref
  end

  if fields.first
    line = begin
             Line.decode(@@decode, self, fields.first)
           rescue Vpim::InvalidEncodingError
           end

    if line
      return line.value
    end
  end

  nil
end

#values(name) ⇒ Object

A variant of #lines that only iterates over specific Line names. Since the name is known, only the Line#value is returned or yielded.



793
794
795
796
797
798
799
# File 'lib/vpim/vcard.rb', line 793

def values(name)
  unless block_given?
    lines(name).map { |line| line.value }
  else
    lines(name) { |line| yield line.value }
  end
end

#versionObject

The VERSION multiplied by 10 as an Integer. For example, a VERSION:2.1 vCard would have a version of 21, and a VERSION:3.0 vCard would have a version of 30.

VERSION is required for a vCard, this raises InvalidEncodingError if there is no VERSION so it cannot return nil.



988
989
990
991
992
993
994
# File 'lib/vpim/vcard.rb', line 988

def version
  v = value('VERSION')
  unless v
    raise Vpim::InvalidEncodingError, 'Invalid vCard - it has no version field!'
  end
  v
end