From 3dca5e078a65bed1bda1f926175f84f0088d440f Mon Sep 17 00:00:00 2001 From: Ben Burwell Date: Sun, 29 Dec 2019 23:48:26 -0500 Subject: initial commit --- main.go | 193 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 main.go (limited to 'main.go') diff --git a/main.go b/main.go new file mode 100644 index 0000000..3273867 --- /dev/null +++ b/main.go @@ -0,0 +1,193 @@ +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"` +} -- cgit v1.2.3