package main import ( "encoding/xml" "fmt" "io/ioutil" "log" "net/http" "os" "os/exec" "path" "strings" "github.com/BurntSushi/toml" "github.com/emersion/go-vcard" "github.com/google/shlex" "github.com/kyoh86/xdg" ) func main() { if err := run(); err != nil { log.Fatal(err) } } func run() error { cfg, err := loadConfig() if err != nil { return err } book := &AddressBook{ Endpoint: cfg.Endpoint, Username: cfg.Username, } pw, err := cfg.getPassword() if err != nil { return fmt.Errorf("could not get password: %w", err) } book.Password = pw var c CardDAVClient if len(os.Args) < 2 { return fmt.Errorf("insufficient command line arguments") } resp, err := c.Report(book, os.Args[1]) if err != nil { return fmt.Errorf("could not fetch completions: %w", err) } body, err := ioutil.ReadAll(resp.Body) if err != nil { return fmt.Errorf("could not read response: %w", err) } defer resp.Body.Close() var ms Multistatus if err := xml.Unmarshal(body, &ms); err != nil { return fmt.Errorf("could not unmarshal XML: %w", err) } for _, resp := range ms.Responses { decoder := vcard.NewDecoder(strings.NewReader(resp.Propstat.Prop.AddressData)) card, err := decoder.Decode() if err != nil { return fmt.Errorf("could not decode vcard: %w", err) } emails := card.Values(vcard.FieldEmail) fn := card.Get(vcard.FieldFormattedName) if len(emails) == 0 { continue } if fn == nil { fn = &vcard.Field{} } for _, email := range emails { fmt.Printf("%s\t%s\n", email, fn.Value) } } return nil } type config struct { Endpoint string `toml:"endpoint"` Username string `toml:"username"` Password string `toml:"password"` PasswordCmd string `toml:"password-cmd"` } func (c config) getPassword() (string, error) { if c.PasswordCmd != "" { cmd, err := c.getPasswordCmd() if err != nil { return "", err } stdout, err := cmd.StdoutPipe() if err != nil { return "", fmt.Errorf("could not set up stdout pipe: %w", err) } if err := cmd.Start(); err != nil { return "", fmt.Errorf("could not start password-cmd: %w", err) } res, err := ioutil.ReadAll(stdout) if err != nil { return "", fmt.Errorf("could not read stdout: %w", err) } if err := cmd.Wait(); err != nil { return "", fmt.Errorf("could not run password-cmd: %w", err) } return strings.TrimSpace(string(res)), nil } return c.Password, nil } func (c config) getPasswordCmd() (*exec.Cmd, error) { parts, err := shlex.Split(c.PasswordCmd) if err != nil { return nil, fmt.Errorf("could not lex password-cmd: %w", err) } if len(parts) < 1 { return nil, fmt.Errorf("empty command") } if len(parts) > 1 { return exec.Command(parts[0], parts[1:]...), nil } return exec.Command(parts[0]), nil } func loadConfig() (*config, error) { filename := path.Join(xdg.ConfigHome(), "cdc", "config") var cfg config if _, err := toml.DecodeFile(filename, &cfg); err != nil { return nil, fmt.Errorf("could not read config: %w", err) } return &cfg, nil } type AddressBook struct { Endpoint string Username string Password string } type CardDAVClient struct { hc http.Client } func (c *CardDAVClient) Report(book *AddressBook, search string) (*http.Response, error) { req, err := http.NewRequest("REPORT", book.Endpoint, strings.NewReader(` `+search+` `+search+` `)) if err != nil { return nil, err } req.Header.Add("Depth", "1") req.Header.Add("Content-Type", "text/xml; charset=\"utf-8\"") if book.Username != "" || book.Password != "" { req.SetBasicAuth(book.Username, book.Password) } return c.hc.Do(req) } type Multistatus struct { Responses []Response `xml:"response"` } type Response struct { HREF string `xml:"href"` Propstat Propstat `xml:"propstat"` } type Propstat struct { Prop Prop `xml:"prop"` } type Prop struct { GetETag string `xml:"getetag"` AddressData string `xml:"address-data"` }