├── go.mod ├── .github └── ISSUE_TEMPLATE │ ├── config.yml │ └── issue_template.md ├── .gitignore ├── .build.yml ├── v4.go ├── README.md ├── LICENSE ├── example_test.go ├── encoder.go ├── encoder_test.go ├── decoder_test.go ├── decoder.go ├── card_test.go └── card.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/emersion/go-vcard 2 | 3 | go 1.13 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Question 4 | url: "https://web.libera.chat/gamja/#emersion" 5 | about: "Please ask questions in #emersion on Libera Chat" 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report or feature request 3 | about: Report a bug or request a new feature 4 | --- 5 | 6 | 13 | -------------------------------------------------------------------------------- /.build.yml: -------------------------------------------------------------------------------- 1 | image: alpine/edge 2 | packages: 3 | - go 4 | sources: 5 | - https://github.com/emersion/go-vcard 6 | artifacts: 7 | - coverage.html 8 | tasks: 9 | - build: | 10 | cd go-vcard 11 | go build -race -v ./... 12 | - test: | 13 | cd go-vcard 14 | go test -coverprofile=coverage.txt -covermode=atomic ./... 15 | - coverage: | 16 | cd go-vcard 17 | go tool cover -html=coverage.txt -o ~/coverage.html 18 | -------------------------------------------------------------------------------- /v4.go: -------------------------------------------------------------------------------- 1 | package vcard 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // See https://github.com/mangstadt/ez-vcard/wiki/Version-differences 8 | 9 | // ToV4 converts a card to vCard version 4. 10 | func ToV4(card Card) { 11 | version := card.Value(FieldVersion) 12 | if strings.HasPrefix(version, "4.") { 13 | return 14 | } 15 | 16 | card.SetValue(FieldVersion, "4.0") 17 | 18 | for k, fields := range card { 19 | if strings.EqualFold(k, FieldVersion) { 20 | continue 21 | } 22 | 23 | for _, f := range fields { 24 | if f.Params.HasType("pref") { 25 | delete(f.Params, "pref") 26 | f.Params.Set("pref", "1") 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-vcard 2 | 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/emersion/go-vcard.svg)](https://pkg.go.dev/github.com/emersion/go-vcard) 4 | [![builds.sr.ht status](https://builds.sr.ht/~emersion/go-vcard/commits.svg)](https://builds.sr.ht/~emersion/go-vcard/commits?) 5 | 6 | A Go library to parse and format [vCard](https://tools.ietf.org/html/rfc6350). 7 | 8 | ## Usage 9 | 10 | ```go 11 | f, err := os.Open("cards.vcf") 12 | if err != nil { 13 | log.Fatal(err) 14 | } 15 | defer f.Close() 16 | 17 | dec := vcard.NewDecoder(f) 18 | for { 19 | card, err := dec.Decode() 20 | if err == io.EOF { 21 | break 22 | } else if err != nil { 23 | log.Fatal(err) 24 | } 25 | 26 | log.Println(card.PreferredValue(vcard.FieldFormattedName)) 27 | } 28 | ``` 29 | 30 | ## License 31 | 32 | MIT 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 emersion 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package vcard_test 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "os" 7 | "strings" 8 | 9 | "github.com/emersion/go-vcard" 10 | ) 11 | 12 | func ExampleNewDecoder() { 13 | f, err := os.Open("cards.vcf") 14 | if err != nil { 15 | log.Fatal(err) 16 | } 17 | defer f.Close() 18 | 19 | dec := vcard.NewDecoder(f) 20 | for { 21 | card, err := dec.Decode() 22 | if err == io.EOF { 23 | break 24 | } else if err != nil { 25 | log.Fatal(err) 26 | } 27 | 28 | log.Println(card.PreferredValue(vcard.FieldFormattedName)) 29 | } 30 | } 31 | 32 | // encoding a vcard can be done as follows 33 | 34 | func ExampleNewEncoder() { 35 | destFile, err := os.Create("cards.vcf") 36 | if err != nil { 37 | log.Fatal(err) 38 | } 39 | defer destFile.Close() 40 | 41 | // data in order: first name, middle name, last name, telephone number 42 | contacts := [][4]string{ 43 | {"John", "Webber", "Maxwell", "(+1) 199 8714"}, 44 | {"Donald", "", "Ron", "(+44) 421 8913"}, 45 | {"Eric", "E.", "Peter", "(+37) 221 9903"}, 46 | {"Nelson", "D.", "Patrick", "(+1) 122 8810"}, 47 | } 48 | 49 | var ( 50 | // card is a map of strings to []*vcard.Field objects 51 | card = make(vcard.Card) 52 | 53 | // destination where the vcard will be encoded to 54 | enc = vcard.NewEncoder(destFile) 55 | ) 56 | 57 | for _, entry := range contacts { 58 | // set only the value of a field by using card.SetValue. 59 | // This does not set parameters 60 | card.SetValue(vcard.FieldFormattedName, strings.Join(entry[:3], " ")) 61 | card.SetValue(vcard.FieldTelephone, entry[3]) 62 | 63 | // set the value of a field and other parameters by using card.Set 64 | card.Set(vcard.FieldName, &vcard.Field{ 65 | Value: strings.Join(entry[:3], ";"), 66 | Params: map[string][]string{ 67 | vcard.ParamSortAs: []string{ 68 | entry[0] + " " + entry[2], 69 | }, 70 | }, 71 | }) 72 | 73 | // make the vCard version 4 compliant 74 | vcard.ToV4(card) 75 | err := enc.Encode(card) 76 | if err != nil { 77 | log.Fatal(err) 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /encoder.go: -------------------------------------------------------------------------------- 1 | package vcard 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "sort" 7 | "strings" 8 | ) 9 | 10 | // An Encoder formats cards. 11 | type Encoder struct { 12 | w io.Writer 13 | } 14 | 15 | // NewEncoder creates a new Encoder that writes cards to w. 16 | func NewEncoder(w io.Writer) *Encoder { 17 | return &Encoder{w} 18 | } 19 | 20 | // Encode formats a card. The card must have a FieldVersion field. 21 | func (enc *Encoder) Encode(c Card) error { 22 | begin := "BEGIN:VCARD\r\n" 23 | if _, err := io.WriteString(enc.w, begin); err != nil { 24 | return err 25 | } 26 | 27 | version := c.Get(FieldVersion) 28 | if version == nil { 29 | return errors.New("vcard: VERSION field missing") 30 | } 31 | if _, err := io.WriteString(enc.w, formatLine(FieldVersion, version)+"\r\n"); err != nil { 32 | return err 33 | } 34 | 35 | var keys []string 36 | for k := range c { 37 | keys = append(keys, k) 38 | } 39 | sort.Strings(keys) 40 | for _, k := range keys { 41 | fields := c[k] 42 | if strings.EqualFold(k, FieldVersion) { 43 | continue 44 | } 45 | for _, f := range fields { 46 | if _, err := io.WriteString(enc.w, formatLine(k, f)+"\r\n"); err != nil { 47 | return err 48 | } 49 | } 50 | } 51 | 52 | end := "END:VCARD\r\n" 53 | _, err := io.WriteString(enc.w, end) 54 | return err 55 | } 56 | 57 | func formatLine(key string, field *Field) string { 58 | var s string 59 | 60 | if field.Group != "" { 61 | s += field.Group + "." 62 | } 63 | s += key 64 | 65 | var keys []string 66 | for k := range field.Params { 67 | keys = append(keys, k) 68 | } 69 | sort.Strings(keys) 70 | for _, pk := range keys { 71 | for _, pv := range field.Params[pk] { 72 | s += ";" + formatParam(pk, pv) 73 | } 74 | } 75 | 76 | s += ":" + formatValue(field.Value) 77 | return s 78 | } 79 | 80 | func formatParam(k, v string) string { 81 | return k + "=" + formatValue(v) 82 | } 83 | 84 | var valueFormatter = strings.NewReplacer("\\", "\\\\", "\n", "\\n", ",", "\\,") 85 | 86 | func formatValue(v string) string { 87 | return valueFormatter.Replace(v) 88 | } 89 | -------------------------------------------------------------------------------- /encoder_test.go: -------------------------------------------------------------------------------- 1 | package vcard 2 | 3 | import ( 4 | "bytes" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestEncoder(t *testing.T) { 10 | var b bytes.Buffer 11 | if err := NewEncoder(&b).Encode(testCard); err != nil { 12 | t.Fatal("Expected no error when formatting card, got:", err) 13 | } 14 | 15 | expected := "BEGIN:VCARD\r\nVERSION:4.0\r\nCLIENTPIDMAP:1;urn:uuid:53e374d9-337e-4727-8803-a1e9c14e0556\r\nEMAIL;PID=1.1:jdoe@example.com\r\nFN;PID=1.1:J. Doe\r\nN:Doe;J.;;;\r\nUID:urn:uuid:4fbe8971-0bc3-424c-9c26-36c3e1eff6b1\r\nEND:VCARD\r\n" 16 | if b.String() != expected { 17 | t.Errorf("Excpected vcard to be %q, but got %q", expected, b.String()) 18 | } 19 | 20 | card, err := NewDecoder(&b).Decode() 21 | if err != nil { 22 | t.Fatal("Expected no error when parsing formatted card, got:", err) 23 | } 24 | 25 | if !reflect.DeepEqual(card, testCard) { 26 | t.Errorf("Invalid parsed card: expected \n%+v\n but got \n%+v", testCard, card) 27 | } 28 | } 29 | 30 | func TestFormatLine_withGroup(t *testing.T) { 31 | l := formatLine("FN", &Field{ 32 | Value: "Akiyama Mio", 33 | Group: "item1", 34 | }) 35 | 36 | expected := "item1.FN:Akiyama Mio" 37 | if l != expected { 38 | t.Errorf("Excpected formatted line with group to be %q, but got %q", expected, l) 39 | } 40 | } 41 | 42 | var testValue = []struct { 43 | v string 44 | formatted string 45 | }{ 46 | {"Hello World!", "Hello World!"}, 47 | {"this is a single value, with a comma encoded", "this is a single value\\, with a comma encoded"}, 48 | {"Mythical Manager\nHyjinx Software Division", "Mythical Manager\\nHyjinx Software Division"}, 49 | {"aa\\\nbb", "aa\\\\\\nbb"}, 50 | } 51 | 52 | func TestFormatValue(t *testing.T) { 53 | for _, test := range testValue { 54 | if formatted := formatValue(test.v); formatted != test.formatted { 55 | t.Errorf("formatValue(%q): expected %q, got %q", test.v, test.formatted, formatted) 56 | } 57 | } 58 | } 59 | 60 | func TestEncoderDeterminism(t *testing.T) { 61 | card := Card{ 62 | "first-key": []*Field{ 63 | { 64 | Value: "value-a", 65 | Params: map[string][]string{ 66 | "p-i": {"s", "ss", "sss"}, 67 | "p-ii": {"s", "ss", "sss"}, 68 | "p-iii": {"s", "ss", "sss"}, 69 | "p-iv": {"sss", "ss", "s"}, 70 | }, 71 | Group: "", 72 | }, 73 | { 74 | Value: "value-B", 75 | Params: map[string][]string{ 76 | "p-i": {"t", "tt", "ttt"}, 77 | "p-ii": {"t", "tt", "ttt"}, 78 | "p-iii": {"t", "tt", "ttt"}, 79 | "p-iv": {"ttt", "tt", "t"}, 80 | }, 81 | Group: "", 82 | }, 83 | { 84 | Value: "VALUE-C", 85 | Params: map[string][]string{}, 86 | Group: "", 87 | }, 88 | }, 89 | "second-KEY": []*Field{ 90 | 91 | { 92 | Value: "value-a\\,b", 93 | Params: map[string][]string{ 94 | "p-i": {"s", "ss", "sss"}, 95 | "p-ii": {"s", "ss", "sss"}, 96 | "p-iii": {"s", "ss", "sss"}, 97 | "p-iv": {"sss", "ss", "s"}, 98 | }, 99 | Group: "G1", 100 | }, 101 | { 102 | Value: "value-B\\1,2", 103 | Params: map[string][]string{ 104 | "p-i": {"t", "tt", "ttt"}, 105 | "p-ii": {"t", "tt", "ttt"}, 106 | "p-iii": {"t", "tt", "ttt"}, 107 | "p-iv": {"ttt", "tt", "t"}, 108 | }, 109 | Group: "GG", 110 | }, 111 | { 112 | Value: "VALUE-C,1,2", 113 | Params: map[string][]string{}, 114 | Group: "", 115 | }, 116 | }, 117 | "THIRD-KEY": []*Field{}, 118 | } 119 | ToV4(card) 120 | var b bytes.Buffer 121 | if err := NewEncoder(&b).Encode(card); err != nil { 122 | t.Fatal("Expected no error when formatting card, got:", err) 123 | } 124 | canonical := b.String() 125 | 126 | for i := 0; i < 100; i++ { 127 | b.Reset() 128 | if err := NewEncoder(&b).Encode(card); err != nil { 129 | t.Fatal("Expected no error when formatting card, got:", err) 130 | } 131 | if b.String() != canonical { 132 | t.Fatalf("Encode: expected canonical %q, got %q", canonical, b.String()) 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /decoder_test.go: -------------------------------------------------------------------------------- 1 | package vcard 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | // RFC 10 | var testCardString = `BEGIN:VCARD 11 | VERSION:4.0 12 | UID:urn:uuid:4fbe8971-0bc3-424c-9c26-36c3e1eff6b1 13 | FN;PID=1.1:J. Doe 14 | N:Doe;J.;;; 15 | EMAIL;PID=1.1:jdoe@example.com 16 | CLIENTPIDMAP:1;urn:uuid:53e374d9-337e-4727-8803-a1e9c14e0556 17 | END:VCARD` 18 | 19 | var testCardHandmadeString = `BEGIN:VCARD 20 | VERSION:4.0 21 | N:Bloggs;Joe;;; 22 | FN:Joe Bloggs 23 | EMAIL;TYPE=home;PREF=1:me@joebloggs.com 24 | TEL;TYPE="cell,home";PREF=1:tel:+44 20 1234 5678 25 | ADR;TYPE=home;PREF=1:;;1 Trafalgar Square;London;;WC2N;United Kingdom 26 | URL;TYPE=home;PREF=1:http://joebloggs.com 27 | IMPP;TYPE=home;PREF=1:skype:joe.bloggs 28 | X-SOCIALPROFILE;TYPE=home;PREF=1:twitter:https://twitter.com/joebloggs 29 | END:VCARD` 30 | 31 | // Google Contacts (15 November 2012) 32 | var testCardGoogleString = `BEGIN:VCARD 33 | VERSION:3.0 34 | N:Bloggs;Joe;;; 35 | FN:Joe Bloggs 36 | EMAIL;TYPE=INTERNET;TYPE=HOME:me@joebloggs.com 37 | TEL;TYPE=CELL:+44 20 1234 5678 38 | ADR;TYPE=HOME:;;1 Trafalgar Square;London;;WC2N;United Kingdom 39 | item1.URL:http\://joebloggs.com 40 | item1.X-ABLabel:_$!!$_ 41 | X-SKYPE:joe.bloggs 42 | item2.URL:http\://twitter.com/test 43 | item2.X-ABLabel:Twitter 44 | END:VCARD` 45 | 46 | // Apple Contacts (version 7.1) 47 | var testCardAppleString = `BEGIN:VCARD 48 | VERSION:3.0 49 | N:Bloggs;Joe;;; 50 | FN:Joe Bloggs 51 | EMAIL;type=INTERNET;type=HOME;type=pref:me@joebloggs.com 52 | TEL;type=CELL;type=VOICE;type=pref:+44 20 1234 5678 53 | ADR;type=HOME;type=pref:;;1 Trafalgar Square;London;;WC2N;United Kingdom 54 | item1.URL;type=pref:http://joebloggs.com 55 | item1.X-ABLabel:_$!!$_ 56 | IMPP;X-SERVICE-TYPE=Skype;type=HOME;type=pref:skype:joe.bloggs 57 | X-SOCIALPROFILE;type=twitter:https://twitter.com/joebloggs 58 | END:VCARD` 59 | 60 | var testCardLineFoldingString = `BEGIN:VCARD 61 | VERSION:4.0 62 | 63 | NOTE:This is a long description 64 | that exists o 65 | n a long line. 66 | 67 | END:VCARD 68 | ` 69 | 70 | var testCardLineFolding = Card{ 71 | "VERSION": {{Value: "4.0"}}, 72 | "NOTE": {{Value: "This is a long description that exists on a long line."}}, 73 | } 74 | 75 | var decoderTests = []struct { 76 | s string 77 | card Card 78 | }{ 79 | {testCardString, testCard}, 80 | {testCardHandmadeString, testCardHandmade}, 81 | {testCardGoogleString, testCardGoogle}, 82 | {testCardAppleString, testCardApple}, 83 | {testCardLineFoldingString, testCardLineFolding}, 84 | } 85 | 86 | func TestDecoder(t *testing.T) { 87 | for _, test := range decoderTests { 88 | r := strings.NewReader(test.s) 89 | dec := NewDecoder(r) 90 | card, err := dec.Decode() 91 | if err != nil { 92 | t.Fatal("Expected no error when decoding card, got:", err) 93 | } 94 | if !reflect.DeepEqual(card, test.card) { 95 | t.Errorf("Invalid parsed card: expected \n%+v\n but got \n%+v", test.card, card) 96 | for k, fields := range test.card { 97 | t.Log(k, reflect.DeepEqual(fields, card[k]), fields[0], card[k][0]) 98 | } 99 | } 100 | } 101 | } 102 | 103 | const testInvalidBegin = `BEGIN:INVALID 104 | END:VCARD` 105 | 106 | const testInvalidEnd = `BEGIN:VCARD 107 | END:INVALID` 108 | 109 | const testInvalidNoBegin = `VERSION:4.0 110 | END:VCARD` 111 | 112 | const testInvalidNoEnd = `BEGIN:VCARD 113 | VERSION:4.0` 114 | 115 | var decoderInvalidTests = []string{ 116 | testInvalidBegin, 117 | testInvalidEnd, 118 | testInvalidNoBegin, 119 | testInvalidNoEnd, 120 | } 121 | 122 | func TestDecoder_invalid(t *testing.T) { 123 | for _, test := range decoderInvalidTests { 124 | r := strings.NewReader(test) 125 | dec := NewDecoder(r) 126 | if _, err := dec.Decode(); err == nil { 127 | t.Fatalf("Expected error when decoding invalid card:\n%v", test) 128 | } 129 | } 130 | } 131 | 132 | func TestParseLine_escaped(t *testing.T) { 133 | l := "NOTE:Mythical Manager\\nHyjinx Software Division\\nBabsCo\\, Inc.\\n" 134 | expectedKey := "NOTE" 135 | expectedValue := "Mythical Manager\nHyjinx Software Division\nBabsCo, Inc.\n" 136 | 137 | if key, field, err := parseLine(l); err != nil { 138 | t.Fatal("Expected no error while parsing line, got:", err) 139 | } else if key != expectedKey || field.Value != expectedValue { 140 | t.Errorf("parseLine(%q): expected (%q, %q), got (%q, %q)", l, expectedKey, expectedValue, key, field.Value) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /decoder.go: -------------------------------------------------------------------------------- 1 | package vcard 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "io" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | // A Decoder parses cards. 12 | type Decoder struct { 13 | r *bufio.Reader 14 | } 15 | 16 | // NewDecoder creates a new Decoder reading cards from an io.Reader. 17 | func NewDecoder(r io.Reader) *Decoder { 18 | return &Decoder{r: bufio.NewReader(r)} 19 | } 20 | 21 | func (dec *Decoder) readLine() (string, error) { 22 | l, err := dec.r.ReadString('\n') 23 | l = strings.TrimRight(l, "\r\n") 24 | if len(l) > 0 && err == io.EOF { 25 | return l, nil 26 | } else if err != nil { 27 | return l, err 28 | } 29 | 30 | for { 31 | next, err := dec.r.Peek(1) 32 | if err == io.EOF { 33 | break 34 | } else if err != nil { 35 | return l, err 36 | } 37 | 38 | if ch := next[0]; ch != ' ' && ch != '\t' { 39 | break 40 | } 41 | 42 | if _, err := dec.r.Discard(1); err != nil { 43 | return l, err 44 | } 45 | 46 | folded, err := dec.r.ReadString('\n') 47 | if err != nil { 48 | return l, err 49 | } 50 | l += strings.TrimRight(folded, "\r\n") 51 | } 52 | 53 | return l, nil 54 | } 55 | 56 | // Decode parses a single card. 57 | func (dec *Decoder) Decode() (Card, error) { 58 | card := make(Card) 59 | 60 | var hasBegin, hasEnd bool 61 | for { 62 | l, err := dec.readLine() 63 | if err == io.EOF { 64 | break 65 | } else if err != nil { 66 | return card, err 67 | } 68 | 69 | k, f, err := parseLine(l) 70 | if err != nil { 71 | continue 72 | } 73 | 74 | if !hasBegin { 75 | if k == "BEGIN" { 76 | if strings.ToUpper(f.Value) != "VCARD" { 77 | return card, errors.New("vcard: invalid BEGIN value") 78 | } 79 | hasBegin = true 80 | continue 81 | } else { 82 | return card, errors.New("vcard: no BEGIN field found") 83 | } 84 | } else if k == "END" { 85 | if strings.ToUpper(f.Value) != "VCARD" { 86 | return card, errors.New("vcard: invalid END value") 87 | } 88 | hasEnd = true 89 | break 90 | } 91 | 92 | card[k] = append(card[k], f) 93 | } 94 | 95 | if !hasEnd { 96 | if !hasBegin { 97 | return nil, io.EOF 98 | } 99 | return card, errors.New("vcard: no END field found") 100 | } 101 | return card, nil 102 | } 103 | 104 | func parseLine(l string) (key string, field *Field, err error) { 105 | field = new(Field) 106 | field.Group, l = parseGroup(l) 107 | key, hasParams, l, err := parseKey(l) 108 | if err != nil { 109 | return 110 | } 111 | 112 | if hasParams { 113 | field.Params, l, err = parseParams(l) 114 | if err != nil { 115 | return 116 | } 117 | } 118 | 119 | field.Value = parseValue(l) 120 | return 121 | } 122 | 123 | func parseGroup(s string) (group, tail string) { 124 | i := strings.IndexAny(s, ".;:") 125 | if i < 0 || s[i] != '.' { 126 | return "", s 127 | } 128 | return s[:i], s[i+1:] 129 | } 130 | 131 | func parseKey(s string) (key string, params bool, tail string, err error) { 132 | i := strings.IndexAny(s, ";:") 133 | if i < 0 { 134 | err = errors.New("vcard: invalid property key") 135 | return 136 | } 137 | return strings.ToUpper(s[:i]), s[i] == ';', s[i+1:], nil 138 | } 139 | 140 | func parseParams(s string) (params Params, tail string, err error) { 141 | tail = s 142 | params = make(Params) 143 | for tail != "" { 144 | i := strings.IndexAny(tail, "=;:") 145 | if i < 0 { 146 | err = errors.New("vcard: malformed parameters") 147 | return 148 | } 149 | if tail[i] == ';' { 150 | tail = tail[i+1:] 151 | continue 152 | } 153 | 154 | k := strings.ToUpper(tail[:i]) 155 | 156 | var values []string 157 | var more bool 158 | values, more, tail, err = parseParamValues(tail[i+1:]) 159 | if err != nil { 160 | return 161 | } 162 | 163 | params[k] = append(params[k], values...) 164 | 165 | if !more { 166 | break 167 | } 168 | } 169 | return 170 | } 171 | 172 | func parseParamValues(s string) (values []string, more bool, tail string, err error) { 173 | if s == "" { 174 | return 175 | } 176 | quote := s[0] 177 | 178 | var vs string 179 | if quote == '"' { 180 | vs, tail, err = parseQuoted(s[1:], quote) 181 | if tail == "" || (tail[0] != ';' && tail[0] != ':') { 182 | err = errors.New("vcard: malformed quoted parameter value") 183 | return 184 | } 185 | more = tail[0] != ':' 186 | tail = tail[1:] 187 | } else { 188 | i := strings.IndexAny(s, ";:") 189 | if i < 0 { 190 | vs = s 191 | } else { 192 | vs, more, tail = s[:i], s[i] != ':', s[i+1:] 193 | } 194 | } 195 | 196 | values = strings.Split(vs, ",") 197 | for i, value := range values { 198 | values[i] = parseValue(value) 199 | } 200 | return 201 | } 202 | 203 | func parseQuoted(s string, quote byte) (value, tail string, err error) { 204 | tail = s 205 | var buf []rune 206 | for tail != "" { 207 | if tail[0] == quote { 208 | tail = tail[1:] 209 | break 210 | } 211 | 212 | var r rune 213 | r, _, tail, err = strconv.UnquoteChar(tail, quote) 214 | if err != nil { 215 | return 216 | } 217 | buf = append(buf, r) 218 | } 219 | value = string(buf) 220 | return 221 | } 222 | 223 | var valueParser = strings.NewReplacer("\\\\", "\\", "\\n", "\n", "\\,", ",") 224 | 225 | func parseValue(s string) string { 226 | return valueParser.Replace(s) 227 | } 228 | -------------------------------------------------------------------------------- /card_test.go: -------------------------------------------------------------------------------- 1 | package vcard 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | var testCard = Card{ 10 | "VERSION": []*Field{{Value: "4.0"}}, 11 | "UID": []*Field{{Value: "urn:uuid:4fbe8971-0bc3-424c-9c26-36c3e1eff6b1"}}, 12 | "FN": []*Field{{ 13 | Value: "J. Doe", 14 | Params: Params{"PID": {"1.1"}}, 15 | }}, 16 | "N": []*Field{{Value: "Doe;J.;;;"}}, 17 | "EMAIL": []*Field{{ 18 | Value: "jdoe@example.com", 19 | Params: Params{"PID": {"1.1"}}, 20 | }}, 21 | "CLIENTPIDMAP": []*Field{{Value: "1;urn:uuid:53e374d9-337e-4727-8803-a1e9c14e0556"}}, 22 | } 23 | 24 | var testCardHandmade = Card{ 25 | "VERSION": []*Field{{Value: "4.0"}}, 26 | "N": []*Field{{Value: "Bloggs;Joe;;;"}}, 27 | "FN": []*Field{{Value: "Joe Bloggs"}}, 28 | "EMAIL": []*Field{{ 29 | Value: "me@joebloggs.com", 30 | Params: Params{"TYPE": {"home"}, "PREF": {"1"}}, 31 | }}, 32 | "TEL": []*Field{{ 33 | Value: "tel:+44 20 1234 5678", 34 | Params: Params{"TYPE": {"cell", "home"}, "PREF": {"1"}}, 35 | }}, 36 | "ADR": []*Field{{ 37 | Value: ";;1 Trafalgar Square;London;;WC2N;United Kingdom", 38 | Params: Params{"TYPE": {"home"}, "PREF": {"1"}}, 39 | }}, 40 | "URL": []*Field{{ 41 | Value: "http://joebloggs.com", 42 | Params: Params{"TYPE": {"home"}, "PREF": {"1"}}, 43 | }}, 44 | "IMPP": []*Field{{ 45 | Value: "skype:joe.bloggs", 46 | Params: Params{"TYPE": {"home"}, "PREF": {"1"}}, 47 | }}, 48 | "X-SOCIALPROFILE": []*Field{{ 49 | Value: "twitter:https://twitter.com/joebloggs", 50 | Params: Params{"TYPE": {"home"}, "PREF": {"1"}}, 51 | }}, 52 | } 53 | 54 | var testCardGoogle = Card{ 55 | "VERSION": []*Field{{Value: "3.0"}}, 56 | "N": []*Field{{Value: "Bloggs;Joe;;;"}}, 57 | "FN": []*Field{{Value: "Joe Bloggs"}}, 58 | "EMAIL": []*Field{{ 59 | Value: "me@joebloggs.com", 60 | Params: Params{"TYPE": {"INTERNET", "HOME"}}, 61 | }}, 62 | "TEL": []*Field{{ 63 | Value: "+44 20 1234 5678", 64 | Params: Params{"TYPE": {"CELL"}}, 65 | }}, 66 | "ADR": []*Field{{ 67 | Value: ";;1 Trafalgar Square;London;;WC2N;United Kingdom", 68 | Params: Params{"TYPE": {"HOME"}}, 69 | }}, 70 | "URL": []*Field{ 71 | {Value: "http\\://joebloggs.com", Group: "item1"}, 72 | {Value: "http\\://twitter.com/test", Group: "item2"}, 73 | }, 74 | "X-SKYPE": []*Field{{Value: "joe.bloggs"}}, 75 | "X-ABLABEL": []*Field{ 76 | {Value: "_$!!$_", Group: "item1"}, 77 | {Value: "Twitter", Group: "item2"}, 78 | }, 79 | } 80 | 81 | var testCardApple = Card{ 82 | "VERSION": []*Field{{Value: "3.0"}}, 83 | "N": []*Field{{Value: "Bloggs;Joe;;;"}}, 84 | "FN": []*Field{{Value: "Joe Bloggs"}}, 85 | "EMAIL": []*Field{{ 86 | Value: "me@joebloggs.com", 87 | Params: Params{"TYPE": {"INTERNET", "HOME", "pref"}}, 88 | }}, 89 | "TEL": []*Field{{ 90 | Value: "+44 20 1234 5678", 91 | Params: Params{"TYPE": {"CELL", "VOICE", "pref"}}, 92 | }}, 93 | "ADR": []*Field{{ 94 | Value: ";;1 Trafalgar Square;London;;WC2N;United Kingdom", 95 | Params: Params{"TYPE": {"HOME", "pref"}}, 96 | }}, 97 | "URL": []*Field{{ 98 | Value: "http://joebloggs.com", 99 | Params: Params{"TYPE": {"pref"}}, 100 | Group: "item1", 101 | }}, 102 | "X-ABLABEL": []*Field{ 103 | {Value: "_$!!$_", Group: "item1"}, 104 | }, 105 | "IMPP": []*Field{{ 106 | Value: "skype:joe.bloggs", 107 | Params: Params{"X-SERVICE-TYPE": {"Skype"}, "TYPE": {"HOME", "pref"}}, 108 | }}, 109 | "X-SOCIALPROFILE": []*Field{{ 110 | Value: "https://twitter.com/joebloggs", 111 | Params: Params{"TYPE": {"twitter"}}, 112 | }}, 113 | } 114 | 115 | func TestMaybeGet(t *testing.T) { 116 | l := []string{"a", "b", "c"} 117 | 118 | expected := []string{"a", "b", "c", "", ""} 119 | for i, exp := range expected { 120 | if v := maybeGet(l, i); v != exp { 121 | t.Errorf("maybeGet(l, %v): expected %q but got %q", i, exp, v) 122 | } 123 | } 124 | } 125 | 126 | func TestCard(t *testing.T) { 127 | testCardFullName := testCard["FN"][0] 128 | if field := testCard.Get(FieldFormattedName); testCardFullName != field { 129 | t.Errorf("Expected card FN field to be %+v but got %+v", testCardFullName, field) 130 | } 131 | if v := testCard.Value(FieldFormattedName); v != testCardFullName.Value { 132 | t.Errorf("Expected card FN field to be %q but got %q", testCardFullName.Value, v) 133 | } 134 | 135 | if field := testCard.Get("X-IDONTEXIST"); field != nil { 136 | t.Errorf("Expected card X-IDONTEXIST field to be %+v but got %+v", nil, field) 137 | } 138 | if v := testCard.Value("X-IDONTEXIST"); v != "" { 139 | t.Errorf("Expected card X-IDONTEXIST field value to be %q but got %q", "", v) 140 | } 141 | 142 | cardMultipleValues := Card{ 143 | "EMAIL": []*Field{ 144 | {Value: "me@example.org", Params: Params{"TYPE": {"home"}}}, 145 | {Value: "me@example.com", Params: Params{"TYPE": {"work"}}}, 146 | }, 147 | } 148 | expected := []string{"me@example.org", "me@example.com"} 149 | if values := cardMultipleValues.Values(FieldEmail); !reflect.DeepEqual(expected, values) { 150 | t.Errorf("Expected card emails to be %+v but got %+v", expected, values) 151 | } 152 | if values := cardMultipleValues.Values("X-IDONTEXIST"); values != nil { 153 | t.Errorf("Expected card X-IDONTEXIST values to be %+v but got %+v", nil, values) 154 | } 155 | } 156 | 157 | func TestCard_AddValue(t *testing.T) { 158 | card := make(Card) 159 | 160 | name1 := "Akiyama Mio" 161 | card.AddValue("FN", name1) 162 | if values := card.Values("FN"); len(values) != 1 || values[0] != name1 { 163 | t.Errorf("Expected one FN value, got %v", values) 164 | } 165 | 166 | name2 := "Mio Akiyama" 167 | card.AddValue("FN", name2) 168 | if values := card.Values("FN"); len(values) != 2 || values[0] != name1 || values[1] != name2 { 169 | t.Errorf("Expected two FN values, got %v", values) 170 | } 171 | } 172 | 173 | func TestCard_Preferred(t *testing.T) { 174 | if pref := testCard.Preferred("X-IDONTEXIST"); pref != nil { 175 | t.Errorf("Expected card preferred X-IDONTEXIST field to be %+v but got %+v", nil, pref) 176 | } 177 | if v := testCard.PreferredValue("X-IDONTEXIST"); v != "" { 178 | t.Errorf("Expected card preferred X-IDONTEXIST field value to be %q but got %q", "", v) 179 | } 180 | 181 | cards := []Card{ 182 | { 183 | "EMAIL": []*Field{ 184 | {Value: "me@example.org", Params: Params{"TYPE": {"home"}}}, 185 | {Value: "me@example.com", Params: Params{"TYPE": {"work"}, "PREF": {"1"}}}, 186 | }, 187 | }, 188 | { 189 | "EMAIL": []*Field{ 190 | {Value: "me@example.org", Params: Params{"TYPE": {"home"}, "PREF": {"50"}}}, 191 | {Value: "me@example.com", Params: Params{"TYPE": {"work"}, "PREF": {"25"}}}, 192 | }, 193 | }, 194 | // v3.0 195 | { 196 | "EMAIL": []*Field{ 197 | {Value: "me@example.org", Params: Params{"TYPE": {"home"}}}, 198 | {Value: "me@example.com", Params: Params{"TYPE": {"work", "pref"}}}, 199 | }, 200 | }, 201 | } 202 | 203 | for _, card := range cards { 204 | if pref := card.Preferred(FieldEmail); pref != card["EMAIL"][1] { 205 | t.Errorf("Expected card preferred email to be %+v but got %+v", card["EMAIL"][1], pref) 206 | } 207 | if v := card.PreferredValue(FieldEmail); v != "me@example.com" { 208 | t.Errorf("Expected card preferred email to be %q but got %q", "me@example.com", v) 209 | } 210 | } 211 | } 212 | 213 | func TestCard_Name(t *testing.T) { 214 | card := make(Card) 215 | if name := card.Name(); name != nil { 216 | t.Errorf("Expected empty card name to be %+v but got %+v", nil, name) 217 | } 218 | if names := card.Names(); names != nil { 219 | t.Errorf("Expected empty card names to be %+v but got %+v", nil, names) 220 | } 221 | 222 | expectedName := &Name{ 223 | FamilyName: "Doe", 224 | GivenName: "J.", 225 | } 226 | expectedNames := []*Name{expectedName} 227 | card.AddName(expectedName) 228 | if name := card.Name(); !reflect.DeepEqual(expectedName, name) { 229 | t.Errorf("Expected populated card name to be %+v but got %+v", expectedName, name) 230 | } 231 | if names := card.Names(); !reflect.DeepEqual(expectedNames, names) { 232 | t.Errorf("Expected populated card names to be %+v but got %+v", expectedNames, names) 233 | } 234 | } 235 | 236 | func TestCard_Kind(t *testing.T) { 237 | card := make(Card) 238 | 239 | if kind := card.Kind(); kind != KindIndividual { 240 | t.Errorf("Expected kind of empty card to be %q but got %q", KindIndividual, kind) 241 | } 242 | 243 | card.SetKind(KindOrganization) 244 | if kind := card.Kind(); kind != KindOrganization { 245 | t.Errorf("Expected kind of populated card to be %q but got %q", KindOrganization, kind) 246 | } 247 | } 248 | 249 | func TestCard_FormattedNames(t *testing.T) { 250 | card := make(Card) 251 | 252 | expectedNames := []*Field{{Value: ""}} 253 | if names := card.FormattedNames(); !reflect.DeepEqual(expectedNames, names) { 254 | t.Errorf("Expected empty card formatted names to be %+v but got %+v", expectedNames, names) 255 | } 256 | 257 | expectedNames = []*Field{{Value: "Akiyama Mio"}} 258 | card.SetValue(FieldFormattedName, expectedNames[0].Value) 259 | if names := card.FormattedNames(); !reflect.DeepEqual(expectedNames, names) { 260 | t.Errorf("Expected populated card formatted names to be %+v but got %+v", expectedNames, names) 261 | } 262 | } 263 | 264 | func TestCard_Gender(t *testing.T) { 265 | card := make(Card) 266 | 267 | var expectedSex Sex 268 | var expectedIdentity string 269 | if sex, identity := card.Gender(); sex != expectedSex || identity != expectedIdentity { 270 | t.Errorf("Expected gender to be (%q %q) but got (%q %q)", expectedSex, expectedIdentity, sex, identity) 271 | } 272 | 273 | expectedSex = SexFemale 274 | card.SetGender(expectedSex, expectedIdentity) 275 | if sex, identity := card.Gender(); sex != expectedSex || identity != expectedIdentity { 276 | t.Errorf("Expected gender to be (%q %q) but got (%q %q)", expectedSex, expectedIdentity, sex, identity) 277 | } 278 | 279 | expectedSex = SexOther 280 | expectedIdentity = "<3" 281 | card.SetGender(expectedSex, expectedIdentity) 282 | if sex, identity := card.Gender(); sex != expectedSex || identity != expectedIdentity { 283 | t.Errorf("Expected gender to be (%q %q) but got (%q %q)", expectedSex, expectedIdentity, sex, identity) 284 | } 285 | } 286 | 287 | func TestCard_Address(t *testing.T) { 288 | card := make(Card) 289 | 290 | if address := card.Address(); address != nil { 291 | t.Errorf("Expected empty card address to be nil, got %v", address) 292 | } 293 | if addresses := card.Addresses(); addresses != nil { 294 | t.Errorf("Expected empty card addresses to be nil, got %v", addresses) 295 | } 296 | 297 | added := &Address{ 298 | StreetAddress: "1 Trafalgar Square", 299 | Locality: "London", 300 | PostalCode: "WC2N", 301 | Country: "United Kingdom", 302 | } 303 | card.AddAddress(added) 304 | 305 | equal := func(a, b *Address) bool { 306 | if (a == nil && b != nil) || (b == nil && a != nil) { 307 | return false 308 | } 309 | a.Field, b.Field = nil, nil 310 | return reflect.DeepEqual(a, b) 311 | } 312 | 313 | if address := card.Address(); !equal(added, address) { 314 | t.Errorf("Expected address to be %+v but got %+v", added, address) 315 | } 316 | if addresses := card.Addresses(); len(addresses) != 1 || !equal(added, addresses[0]) { 317 | t.Errorf("Expected addresses to be %+v, got %+v", []*Address{added}, addresses) 318 | } 319 | } 320 | 321 | func TestCard_Revision(t *testing.T) { 322 | card := make(Card) 323 | 324 | if rev, err := card.Revision(); err != nil { 325 | t.Fatal("Expected no error when getting revision of an empty card, got:", err) 326 | } else if !rev.IsZero() { 327 | t.Error("Expected a zero time when getting revision of an empty card, got:", rev) 328 | } 329 | 330 | expected := time.Date(1984, time.November, 4, 0, 0, 0, 0, time.UTC) 331 | card.SetRevision(expected) 332 | if rev, err := card.Revision(); err != nil { 333 | t.Fatal("Expected no error when getting revision of a populated card, got:", err) 334 | } else if !rev.Equal(rev) { 335 | t.Errorf("Expected revision to be %v but got %v", expected, rev) 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /card.go: -------------------------------------------------------------------------------- 1 | // Package vcard implements the vCard format, defined in RFC 6350. 2 | package vcard 3 | 4 | import ( 5 | "strconv" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | // MIME type and file extension for VCard, defined in RFC 6350 section 10.1. 11 | const ( 12 | MIMEType = "text/vcard" 13 | Extension = "vcf" 14 | ) 15 | 16 | const timestampLayout = "20060102T150405Z" 17 | 18 | // Card property parameters. 19 | const ( 20 | ParamLanguage = "LANGUAGE" 21 | ParamValue = "VALUE" 22 | ParamPreferred = "PREF" 23 | ParamAltID = "ALTID" 24 | ParamPID = "PID" 25 | ParamType = "TYPE" 26 | ParamMediaType = "MEDIATYPE" 27 | ParamCalendarScale = "CALSCALE" 28 | ParamSortAs = "SORT-AS" 29 | ParamGeolocation = "GEO" 30 | ParamTimezone = "TZ" 31 | ) 32 | 33 | // Card properties. 34 | const ( 35 | // General Properties 36 | FieldSource = "SOURCE" 37 | FieldKind = "KIND" 38 | FieldXML = "XML" 39 | 40 | // Identification Properties 41 | FieldFormattedName = "FN" 42 | FieldName = "N" 43 | FieldNickname = "NICKNAME" 44 | FieldPhoto = "PHOTO" 45 | FieldBirthday = "BDAY" 46 | FieldAnniversary = "ANNIVERSARY" 47 | FieldGender = "GENDER" 48 | 49 | // Delivery Addressing Properties 50 | FieldAddress = "ADR" 51 | 52 | // Communications Properties 53 | FieldTelephone = "TEL" 54 | FieldEmail = "EMAIL" 55 | FieldIMPP = "IMPP" // Instant Messaging and Presence Protocol 56 | FieldLanguage = "LANG" 57 | 58 | // Geographical Properties 59 | FieldTimezone = "TZ" 60 | FieldGeolocation = "GEO" 61 | 62 | // Organizational Properties 63 | FieldTitle = "TITLE" 64 | FieldRole = "ROLE" 65 | FieldLogo = "LOGO" 66 | FieldOrganization = "ORG" 67 | FieldMember = "MEMBER" 68 | FieldRelated = "RELATED" 69 | 70 | // Explanatory Properties 71 | FieldCategories = "CATEGORIES" 72 | FieldNote = "NOTE" 73 | FieldProductID = "PRODID" 74 | FieldRevision = "REV" 75 | FieldSound = "SOUND" 76 | FieldUID = "UID" 77 | FieldClientPIDMap = "CLIENTPIDMAP" 78 | FieldURL = "URL" 79 | FieldVersion = "VERSION" 80 | 81 | // Security Properties 82 | FieldKey = "KEY" 83 | 84 | // Calendar Properties 85 | FieldFreeOrBusyURL = "FBURL" 86 | FieldCalendarAddressURI = "CALADRURI" 87 | FieldCalendarURI = "CALURI" 88 | ) 89 | 90 | func maybeGet(l []string, i int) string { 91 | if i < len(l) { 92 | return l[i] 93 | } 94 | return "" 95 | } 96 | 97 | // A Card is an address book entry. 98 | type Card map[string][]*Field 99 | 100 | // Get returns the first field of the card for the given property. If there is 101 | // no such field, it returns nil. 102 | func (c Card) Get(k string) *Field { 103 | fields := c[k] 104 | if len(fields) == 0 { 105 | return nil 106 | } 107 | return fields[0] 108 | } 109 | 110 | // Add adds the k, f pair to the list of fields. It appends to any existing 111 | // fields. 112 | func (c Card) Add(k string, f *Field) { 113 | c[k] = append(c[k], f) 114 | } 115 | 116 | // Set sets the key k to the single field f. It replaces any existing field. 117 | func (c Card) Set(k string, f *Field) { 118 | c[k] = []*Field{f} 119 | } 120 | 121 | // Preferred returns the preferred field of the card for the given property. 122 | func (c Card) Preferred(k string) *Field { 123 | fields := c[k] 124 | if len(fields) == 0 { 125 | return nil 126 | } 127 | 128 | field := fields[0] 129 | min := 100 130 | for _, f := range fields { 131 | n := 100 132 | if pref := f.Params.Get(ParamPreferred); pref != "" { 133 | n, _ = strconv.Atoi(pref) 134 | } else if f.Params.HasType("pref") { 135 | // Apple Contacts adds "pref" to the TYPE param 136 | n = 1 137 | } 138 | 139 | if n < min { 140 | min = n 141 | field = f 142 | } 143 | } 144 | return field 145 | } 146 | 147 | // Value returns the first field value of the card for the given property. If 148 | // there is no such field, it returns an empty string. 149 | func (c Card) Value(k string) string { 150 | f := c.Get(k) 151 | if f == nil { 152 | return "" 153 | } 154 | return f.Value 155 | } 156 | 157 | // AddValue adds the k, v pair to the list of field values. It appends to any 158 | // existing values. 159 | func (c Card) AddValue(k, v string) { 160 | c.Add(k, &Field{Value: v}) 161 | } 162 | 163 | // SetValue sets the field k to the single value v. It replaces any existing 164 | // value. 165 | func (c Card) SetValue(k, v string) { 166 | c.Set(k, &Field{Value: v}) 167 | } 168 | 169 | // PreferredValue returns the preferred field value of the card. 170 | func (c Card) PreferredValue(k string) string { 171 | f := c.Preferred(k) 172 | if f == nil { 173 | return "" 174 | } 175 | return f.Value 176 | } 177 | 178 | // Values returns a list of values for a given property. 179 | func (c Card) Values(k string) []string { 180 | fields := c[k] 181 | if fields == nil { 182 | return nil 183 | } 184 | 185 | values := make([]string, len(fields)) 186 | for i, f := range fields { 187 | values[i] = f.Value 188 | } 189 | return values 190 | } 191 | 192 | // Kind returns the kind of the object represented by this card. If it isn't 193 | // specified, it returns the default: KindIndividual. 194 | func (c Card) Kind() Kind { 195 | kind := strings.ToLower(c.Value(FieldKind)) 196 | if kind == "" { 197 | return KindIndividual 198 | } 199 | return Kind(kind) 200 | } 201 | 202 | // SetKind sets the kind of the object represented by this card. 203 | func (c Card) SetKind(kind Kind) { 204 | c.SetValue(FieldKind, string(kind)) 205 | } 206 | 207 | // FormattedNames returns formatted names of the card. The length of the result 208 | // is always greater or equal to 1. 209 | func (c Card) FormattedNames() []*Field { 210 | fns := c[FieldFormattedName] 211 | if len(fns) == 0 { 212 | return []*Field{{Value: ""}} 213 | } 214 | return fns 215 | } 216 | 217 | // Names returns names of the card. 218 | func (c Card) Names() []*Name { 219 | ns := c[FieldName] 220 | if ns == nil { 221 | return nil 222 | } 223 | 224 | names := make([]*Name, len(ns)) 225 | for i, n := range ns { 226 | names[i] = newName(n) 227 | } 228 | return names 229 | } 230 | 231 | // Name returns the preferred name of the card. If it isn't specified, it 232 | // returns nil. 233 | func (c Card) Name() *Name { 234 | n := c.Preferred(FieldName) 235 | if n == nil { 236 | return nil 237 | } 238 | return newName(n) 239 | } 240 | 241 | // AddName adds the specified name to the list of names. 242 | func (c Card) AddName(name *Name) { 243 | c.Add(FieldName, name.field()) 244 | } 245 | 246 | // SetName replaces the list of names with the single specified name. 247 | func (c Card) SetName(name *Name) { 248 | c.Set(FieldName, name.field()) 249 | } 250 | 251 | // Gender returns this card's gender. 252 | func (c Card) Gender() (sex Sex, identity string) { 253 | v := c.Value(FieldGender) 254 | parts := strings.SplitN(v, ";", 2) 255 | return Sex(strings.ToUpper(parts[0])), maybeGet(parts, 1) 256 | } 257 | 258 | // SetGender sets this card's gender. 259 | func (c Card) SetGender(sex Sex, identity string) { 260 | v := string(sex) 261 | if identity != "" { 262 | v += ";" + identity 263 | } 264 | c.SetValue(FieldGender, v) 265 | } 266 | 267 | // Addresses returns addresses of the card. 268 | func (c Card) Addresses() []*Address { 269 | adrs := c[FieldAddress] 270 | if adrs == nil { 271 | return nil 272 | } 273 | 274 | addresses := make([]*Address, len(adrs)) 275 | for i, adr := range adrs { 276 | addresses[i] = newAddress(adr) 277 | } 278 | return addresses 279 | } 280 | 281 | // Address returns the preferred address of the card. If it isn't specified, it 282 | // returns nil. 283 | func (c Card) Address() *Address { 284 | adr := c.Preferred(FieldAddress) 285 | if adr == nil { 286 | return nil 287 | } 288 | return newAddress(adr) 289 | } 290 | 291 | // AddAddress adds an address to the list of addresses. 292 | func (c Card) AddAddress(address *Address) { 293 | c.Add(FieldAddress, address.field()) 294 | } 295 | 296 | // SetAddress replaces the list of addresses with the single specified address. 297 | func (c Card) SetAddress(address *Address) { 298 | c.Set(FieldAddress, address.field()) 299 | } 300 | 301 | // Categories returns category information about the card, also known as "tags". 302 | func (c Card) Categories() []string { 303 | return strings.Split(c.PreferredValue(FieldCategories), ",") 304 | } 305 | 306 | // SetCategories sets category information about the card. 307 | func (c Card) SetCategories(categories []string) { 308 | c.SetValue(FieldCategories, strings.Join(categories, ",")) 309 | } 310 | 311 | // Revision returns revision information about the current card. 312 | func (c Card) Revision() (time.Time, error) { 313 | rev := c.Value(FieldRevision) 314 | if rev == "" { 315 | return time.Time{}, nil 316 | } 317 | return time.Parse(timestampLayout, rev) 318 | } 319 | 320 | // SetRevision sets revision information about the current card. 321 | func (c Card) SetRevision(t time.Time) { 322 | c.SetValue(FieldRevision, t.Format(timestampLayout)) 323 | } 324 | 325 | // A field contains a value and some parameters. 326 | type Field struct { 327 | Value string 328 | Params Params 329 | Group string 330 | } 331 | 332 | // Params is a set of field parameters. 333 | type Params map[string][]string 334 | 335 | // Get returns the first value with the key k. It returns an empty string if 336 | // there is no such value. 337 | func (p Params) Get(k string) string { 338 | values := p[k] 339 | if len(values) == 0 { 340 | return "" 341 | } 342 | return values[0] 343 | } 344 | 345 | // Add adds the k, v pair to the list of parameters. It appends to any existing 346 | // values. 347 | func (p Params) Add(k, v string) { 348 | p[k] = append(p[k], v) 349 | } 350 | 351 | // Set sets the parameter k to the single value v. It replaces any existing 352 | // value. 353 | func (p Params) Set(k, v string) { 354 | p[k] = []string{v} 355 | } 356 | 357 | // Types returns the field types. 358 | func (p Params) Types() []string { 359 | types := p[ParamType] 360 | list := make([]string, len(types)) 361 | for i, t := range types { 362 | list[i] = strings.ToLower(t) 363 | } 364 | return list 365 | } 366 | 367 | // HasType returns true if and only if the field have the provided type. 368 | func (p Params) HasType(t string) bool { 369 | for _, tt := range p[ParamType] { 370 | if strings.EqualFold(t, tt) { 371 | return true 372 | } 373 | } 374 | return false 375 | } 376 | 377 | // Kind is an object's kind. 378 | type Kind string 379 | 380 | // Values for FieldKind. 381 | const ( 382 | KindIndividual Kind = "individual" 383 | KindGroup Kind = "group" 384 | KindOrganization Kind = "org" 385 | KindLocation Kind = "location" 386 | ) 387 | 388 | // Values for ParamType. 389 | const ( 390 | // Generic 391 | TypeHome = "home" 392 | TypeWork = "work" 393 | 394 | // For FieldTelephone 395 | TypeText = "text" 396 | TypeVoice = "voice" // Default 397 | TypeFax = "fax" 398 | TypeCell = "cell" 399 | TypeVideo = "video" 400 | TypePager = "pager" 401 | TypeTextPhone = "textphone" 402 | 403 | // For FieldRelated 404 | TypeContact = "contact" 405 | TypeAcquaintance = "acquaintance" 406 | TypeFriend = "friend" 407 | TypeMet = "met" 408 | TypeCoWorker = "co-worker" 409 | TypeColleague = "colleague" 410 | TypeCoResident = "co-resident" 411 | TypeNeighbor = "neighbor" 412 | TypeChild = "child" 413 | TypeParent = "parent" 414 | TypeSibling = "sibling" 415 | TypeSpouse = "spouse" 416 | TypeKin = "kin" 417 | TypeMuse = "muse" 418 | TypeCrush = "crush" 419 | TypeDate = "date" 420 | TypeSweetheart = "sweetheart" 421 | TypeMe = "me" 422 | TypeAgent = "agent" 423 | TypeEmergency = "emergency" 424 | ) 425 | 426 | // Name contains an object's name components. 427 | type Name struct { 428 | *Field 429 | 430 | FamilyName string 431 | GivenName string 432 | AdditionalName string 433 | HonorificPrefix string 434 | HonorificSuffix string 435 | } 436 | 437 | func newName(field *Field) *Name { 438 | components := strings.Split(field.Value, ";") 439 | return &Name{ 440 | field, 441 | maybeGet(components, 0), 442 | maybeGet(components, 1), 443 | maybeGet(components, 2), 444 | maybeGet(components, 3), 445 | maybeGet(components, 4), 446 | } 447 | } 448 | 449 | func (n *Name) field() *Field { 450 | if n.Field == nil { 451 | n.Field = new(Field) 452 | } 453 | n.Field.Value = strings.Join([]string{ 454 | n.FamilyName, 455 | n.GivenName, 456 | n.AdditionalName, 457 | n.HonorificPrefix, 458 | n.HonorificSuffix, 459 | }, ";") 460 | return n.Field 461 | } 462 | 463 | // Sex is an object's biological sex. 464 | type Sex string 465 | 466 | const ( 467 | SexUnspecified Sex = "" 468 | SexFemale Sex = "F" 469 | SexMale Sex = "M" 470 | SexOther Sex = "O" 471 | SexNone Sex = "N" 472 | SexUnknown Sex = "U" 473 | ) 474 | 475 | // An Address is a delivery address. 476 | type Address struct { 477 | *Field 478 | 479 | PostOfficeBox string 480 | ExtendedAddress string // e.g., apartment or suite number 481 | StreetAddress string 482 | Locality string // e.g., city 483 | Region string // e.g., state or province 484 | PostalCode string 485 | Country string 486 | } 487 | 488 | func newAddress(field *Field) *Address { 489 | components := strings.Split(field.Value, ";") 490 | return &Address{ 491 | field, 492 | maybeGet(components, 0), 493 | maybeGet(components, 1), 494 | maybeGet(components, 2), 495 | maybeGet(components, 3), 496 | maybeGet(components, 4), 497 | maybeGet(components, 5), 498 | maybeGet(components, 6), 499 | } 500 | } 501 | 502 | func (a *Address) field() *Field { 503 | if a.Field == nil { 504 | a.Field = new(Field) 505 | } 506 | a.Field.Value = strings.Join([]string{ 507 | a.PostOfficeBox, 508 | a.ExtendedAddress, 509 | a.StreetAddress, 510 | a.Locality, 511 | a.Region, 512 | a.PostalCode, 513 | a.Country, 514 | }, ";") 515 | return a.Field 516 | } 517 | --------------------------------------------------------------------------------