<file_sep># Based on https://git.io/fjkGc

# The full path to the main package is use in the
# imports tool to format imports correctly.
NAMESPACE = github.com/xorcare/caiman

# The name of the file recommended in the standard
# documentation go test -cover and used codecov.io
# to check code coverage.
COVER_FILE ?= coverage.out

# Main targets.
.DEFAULT_GOAL := build

build: ## Build the project binary
	@go build ./cmd/caiman

ci: check build ## Target for integration with ci pipeline
	go get github.com/xorcare/caiman/cmd/caiman
	caiman --help

check: static test build ## Check project with static checks and unit tests

help: ## Print this help
	@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | \
	awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

imports: tools ## Check and fix import section by import rules
	@test -z $$(for d in $$(go list -f {{.Dir}} ./...); do goimports -e -l -local $(NAMESPACE) -w $$d/*.go; done)

install: ## Install the project binary to $GOPATH/bin
	@go install ./cmd/caiman

lint: tools ## Check the project with lint
	@golint -set_exit_status ./...

static: imports vet lint ## Run static checks (lint, imports, vet, ...) all over the project

test: ## Run unit tests
	@go test ./... -count=1 -race
	@go test ./... -count=1 -coverprofile=$(COVER_FILE) -covermode=atomic $d
	@go tool cover -func=$(COVER_FILE) | grep ^total

tools: ## Install all needed tools, e.g. for static checks
	@go install \
	golang.org/x/lint/golint \
	golang.org/x/tools/cmd/goimports

toolsup: ## Update all needed tools, e.g. for static checks
	go mod tidy && \
	go get \
	golang.org/x/lint/golint@latest \
	golang.org/x/tools/cmd/goimports@latest && \
	go mod download && \
	go mod verify
	$(MAKE) tools

vet: ## Check the project with vet
	@go vet ./...

.PHONY: build check ci help imports install lint static test tools toolsup vet
<file_sep>// Copyright © 2019-2020 <NAME>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package converter

import (
	"bytes"
	"encoding/base64"
	"fmt"
	"log"
	"strings"
	"sync"
	"text/template"

	"github.com/emersion/go-vcard"
	"github.com/go-ldap/ldif"
	"github.com/spf13/cast"

	"github.com/xorcare/caiman/internal/config"
)

var _funcMap = map[string]interface{}{
	"Base64Encode": base64Encode,
}

// Converter structure of the converter.
type Converter struct {
	Config    config.Config
	templates map[string]*template.Template
	once      sync.Once
}

// LDIF2vCARD4 converts LDIF to vCard 4.
func (c *Converter) LDIF2vCARD4(l ldif.LDIF) (cards Cards, err error) {
	skipped, successful, nullable := 0, 0, 0
	defer func() {
		log.Println(fmt.Sprintf("total entries %d", len(l.Entries)))
		log.Println(fmt.Sprintf("skipped %d entries because it is nil", nullable))
		log.Println(fmt.Sprintf("skipped %d entries because bad count of filled fields", skipped))
		log.Println(fmt.Sprintf("successfully processed %d entries", successful))
	}()
	for _, v := range l.Entries {
		if v == nil || v.Entry == nil {
			nullable++
			continue
		}
		entry := adapter{entry: *v.Entry}

		card := vcard.Card{}
		for fieldName, fields := range c.Config.Fields {
			fieldName = strings.ToUpper(fieldName)
			for _, field := range fields {
				tpl, err := c.parseTemplate(field.Template)
				if err != nil {
					return nil, err
				}
				buf := bytes.NewBuffer([]byte{})
				err = tpl.Execute(buf, map[string]interface{}{
					"entry": entry,
				})
				if err != nil {
					return nil, err
				}
				add(card, fieldName, vcard.Field{
					Value:  buf.String(),
					Params: field.Params,
					Group:  field.Group,
				})
			}
		}
		if len(card) <= 3 {
			skipped++
			continue
		}
		vcard.ToV4(card)
		cards = append(cards, card)
		successful++
	}

	return cards, nil
}

func (c *Converter) parseTemplate(tpl string) (tmpl *template.Template, err error) {
	c.once.Do(func() {
		c.templates = make(map[string]*template.Template)
	})

	if tmpl, ok := c.templates[tpl]; ok {
		return tmpl.Clone()
	}

	tmpl, err = template.New("").Funcs(_funcMap).Parse(tpl)
	if err != nil {
		return nil, err
	}

	c.templates[tpl] = tmpl

	return tmpl.Clone()
}

func add(card vcard.Card, k string, f vcard.Field) {
	if k != "" && strings.TrimSpace(f.Value) != "" {
		card.Add(k, &f)
	}
}

func base64Encode(content interface{}) (string, error) {
	str, err := cast.ToStringE(content)
	if err != nil {
		return "", err
	}

	return base64.StdEncoding.EncodeToString([]byte(str)), nil
}
<file_sep># caiman

[![Build Status](https://travis-ci.org/xorcare/caiman.svg?branch=master)](https://travis-ci.org/xorcare/caiman)
[![Go Report Card](https://goreportcard.com/badge/github.com/xorcare/caiman)](https://goreportcard.com/report/github.com/xorcare/caiman)

This is a tool for converting [LDIF] ([LDAP]) data into standard [vCard4] data, and
then downloading it to services or applications that are compatible with the
vCard4 standard.

The tool is mainly focused on the [Microsoft AD] product, but can be used with other
LDAPs, with possible limitations.

## Installation

```bash
go get github.com/xorcare/caiman/cmd/caiman
```

## Command line interface, [CLI]

```
caiman is a tool to convert people data from LDAP(LDIF) format to vCard4 contact format

Usage:
  caiman [flags]

Examples:
caiman < person.ldif > person.vcf
caiman --config-file ~/.caiman.yaml < person.ldif > person.vcf
caiman --config-dump > .caiman.yaml
cat person.ldif | caiman > person.vcf
cat person.ldif | caiman | tee person.vcf

Flags:
  -d, --config-dump          print to standard output all configuration values, it prints configuration data in YAML format
  -f, --config-file string   the settings file from which the settings will be loaded
  -h, --help                 help for caiman
```

Example of a command to convert LDAP to vCard:

```
$ caiman < person.ldif > person.vcf 
2019/01/31 21:01:44 total entries 1030
2019/01/31 21:01:44 skipped 1 entries because it is nil
2019/01/31 21:01:44 skipped 5 entries because bad count of filled fields
2019/01/31 21:01:44 successfully processed 1024 entries
```

Example of a command to export data from LDAP:

```
ldapsearch -x -h example.com -D "DOMAIN\user.name" -LL -W -b 'DC=example,DC=com' '(objectClass=person)' | tee person.ldif
```

## License

© <NAME>, 2019-2020

Released under the [BSD 3-Clause License][LIC].

[LIC]:https://github.com/xorcare/caiman/blob/master/LICENSE 'BSD 3-Clause "New" or "Revised" License'
[vCard4]:https://en.wikipedia.org/wiki/VCard#vCard_4.0 'vCard 4.0 its the latest standard, which is built upon the RFC 6350 standard'
[LDIF]:https://tools.ietf.org/html/rfc2849 'The LDAP Data Interchange Format (LDIF) - Technical Specification'
[LDAP]:https://en.wikipedia.org/wiki/LDAP_Data_Interchange_Format 'LDAP Data Interchange Format'
[Microsoft AD]:https://docs.microsoft.com/en-us/azure/active-directory 'Azure Active Directory documentation'
[CLI]:https://en.wikipedia.org/wiki/Command-line_interface 'Command-line interface'
<file_sep>// Copyright © 2019-2020 <NAME>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package cli

import (
	"errors"
	"log"
	"os"

	"github.com/go-ldap/ldif"
	"github.com/spf13/cobra"
	"golang.org/x/crypto/ssh/terminal"

	"github.com/xorcare/caiman/internal/config"
	"github.com/xorcare/caiman/internal/converter"
)

var _config = config.Default()
var _configDump = false
var _configFile = ""

var _caimanCMD = &cobra.Command{
	Use:  "caiman",
	Long: "caiman is a tool to convert people data from LDAP(LDIF) format to vCard4 contact format",
	Example: "caiman < person.ldif > person.vcf" +
		"\ncaiman --config-file ~/.caiman.yaml < person.ldif > person.vcf" +
		"\ncaiman --config-dump > .caiman.yaml" +
		"\ncat person.ldif | caiman > person.vcf" +
		"\ncat person.ldif | caiman | tee person.vcf",
	PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
		log.SetOutput(cmd.ErrOrStderr())
		return nil
	},
	PreRunE: func(cmd *cobra.Command, args []string) (err error) {
		if _configFile == "" {
			return
		}
		file, err := os.Open(_configFile)
		if os.IsNotExist(err) {
			return nil
		} else if err != nil {
			return err
		} else {
			defer file.Close()
		}
		return _config.Decode(file)
	},
	RunE:         exec,
	SilenceUsage: true,
}

func init() {
	_caimanCMD.Flags().StringVarP(
		&_configFile, "config-file", "f", _configFile,
		"the settings file from which the settings will be loaded")
	_caimanCMD.Flags().BoolVarP(
		&_configDump, "config-dump", "d", _configDump,
		"print to standard output all configuration values,"+
			" it prints configuration data in YAML format")

}

func exec(cmd *cobra.Command, _ []string) error {
	if _configDump {
		return _config.Encode(cmd.OutOrStdout())
	}

	if terminal.IsTerminal(0) {
		return errors.New("no piped data")
	}

	dif := ldif.LDIF{}
	if err := ldif.Unmarshal(cmd.InOrStdin(), &dif); err != nil {
		return err
	}

	conv := converter.Converter{Config: _config}
	cards, err := conv.LDIF2vCARD4(dif)
	if err != nil {
		return err
	}

	if err := cards.Encode(cmd.OutOrStdout()); err != nil {
		return err
	}

	return nil
}

// Execute starts command line processing.
func Execute() {
	if err := _caimanCMD.Execute(); err != nil {
		os.Exit(1)
	}
}
<file_sep>// Copyright © 2020 <NAME>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package converter

import (
	"io"

	"github.com/emersion/go-vcard"
)

// Cards its data vCard set with helper methods.
type Cards []vcard.Card

// Encode its encodes data into a vCard text representation.
func (c Cards) Encode(w io.Writer) error {
	encoder := vcard.NewEncoder(w)
	for _, card := range c {
		if err := encoder.Encode(card); err != nil {
			return err
		}
	}

	return nil
}
<file_sep>// Copyright © 2019-2020 <NAME>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package main

import (
	"github.com/xorcare/caiman/cmd/caiman/cli"
)

// Useful links:

// RFC	2849	https://tools.ietf.org/html/rfc2849	The LDAP Data Interchange Format (LDIF) - Technical Specification
// RFC	4525	https://tools.ietf.org/html/rfc4525	Lightweight Directory Access Protocol (LDAP)
// RFC	6350	https://tools.ietf.org/html/rfc6350	vCard Format Specification
// RFC	6351	https://tools.ietf.org/html/rfc6351	xCard: vCard XML Representation
// RFC	7095	https://tools.ietf.org/html/rfc7095	jCard: The JSON Format for vCard

// https://en.wikipedia.org/wiki/VCard#vCard_4.0				vCard
// https://en.wikipedia.org/wiki/LDAP_Data_Interchange_Format	LDAP Data Interchange Format

func main() {
	cli.Execute()
}
<file_sep>// Copyright © 2019-2020 <NAME>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package config

import (
	"io"

	"github.com/emersion/go-vcard"
	"gopkg.in/yaml.v2"
)

var _default = Config{
	Fields: map[string][]Field{
		vcard.FieldEmail: {
			{
				Template: `{{.entry.Attr "mail"}}`,
				Params: vcard.Params{
					vcard.ParamType:      []string{"INTERNET", vcard.TypeWork},
					vcard.ParamPreferred: []string{"1"},
				},
			},
		},
		vcard.FieldName: {
			{
				Template: `{{.entry.Attr "sn"}}{{$givenName := .entry.Attr "givenName"}}{{if ne $givenName ""}};{{$givenName}}{{end}}`,
			},
		},
		vcard.FieldTelephone: {
			{
				Template: `{{.entry.Attr "mobile"}}`,
				Params: vcard.Params{
					vcard.ParamType: []string{
						vcard.TypeText, vcard.TypeVoice, vcard.TypeHome,
					},
					vcard.ParamPreferred: []string{"1"},
				},
			},
			{
				Template: `{{.entry.Attr "telephoneNumber"}}`,
				Params: vcard.Params{
					vcard.ParamType: []string{vcard.TypeWork},
				},
			},
		},
		vcard.FieldTitle: {
			{
				Template: `{{.entry.Attr "title"}}`,
			},
		},
		vcard.FieldOrganization: {
			{
				Template: `{{if .entry.Attr "department" }};{{.entry.Attr "department"}}{{else}}{{end}}`,
			},
		},
		vcard.FieldPhoto: {
			{
				Template: `{{.entry.Attr "jpegPhoto" | Base64Encode}}`,
				Params: vcard.Params{
					"ENCODING":           []string{"b"},
					vcard.ParamType:      []string{"JPEG"},
					vcard.ParamPreferred: []string{"1"},
				},
			},
			{
				Template: `{{.entry.Attr "photo" | Base64Encode}}`,
				Params: vcard.Params{
					"ENCODING":           []string{"b"},
					vcard.ParamPreferred: []string{"2"},
				},
			},
			{
				Template: `{{.entry.Attr "thumbnailPhoto" | Base64Encode}}`,
				Params: vcard.Params{
					"ENCODING": []string{"b"},
				},
			},
		},
	},
}

// Config the basic structure configuration.
type Config struct {
	Fields map[string][]Field
}

// Decode the YAML settings from reader.
func (c *Config) Decode(r io.Reader) error {
	return yaml.NewDecoder(r).Decode(c)
}

// Encode the YAML settings to writer.
func (c Config) Encode(w io.Writer) error {
	return yaml.NewEncoder(w).Encode(c)
}

// Field a field contains a value and some parameters.
type Field struct {
	Group string
	// Params is a set of field parameters.
	Params vcard.Params
	// Template golang text template string.
	Template string
}

// Default returns copy of default config.
func Default() Config {
	return _default
}
<file_sep>module github.com/xorcare/caiman

go 1.13

require (
	github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5
	github.com/go-ldap/ldif v0.0.0-20180918085934-3491d58cdb60
	github.com/kr/pretty v0.1.0 // indirect
	github.com/spf13/cast v1.3.0
	github.com/spf13/cobra v0.0.5
	github.com/stretchr/testify v1.4.0 // indirect
	golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550
	golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f
	golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 // indirect
	golang.org/x/tools v0.0.0-20200125223703-d33eef8e6825
	gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect
	gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
	gopkg.in/ldap.v2 v2.5.1
	gopkg.in/yaml.v2 v2.2.2
)
<file_sep>// Copyright © 2019-2020 <NAME>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package converter

import "gopkg.in/ldap.v2"

// adaptor an entry representation for go templates.
type adapter struct {
	entry ldap.Entry
}

// Attr returns the first value for the named attribute, or "".
func (a adapter) Attr(key string) string {
	return a.entry.GetAttributeValue(key)
}

// Attrs returns the values for the named attribute, or an empty list.
func (a adapter) Attrs(key string) []string {
	return a.entry.GetAttributeValues(key)
}
