diff options
Diffstat (limited to 'main.go')
-rw-r--r-- | main.go | 193 |
1 files changed, 193 insertions, 0 deletions
@@ -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(`<?xml version="1.0" encoding="utf-8"?> +<C:addressbook-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:carddav"> +<D:prop> + <D:getetag /> + <C:address-data> + <C:prop name="FN" /> + <C:prop name="EMAIL" /> + </C:address-data> +</D:prop> +<C:filter test="anyof"> + <C:prop-filter name="EMAIL"> + <C:text-match collation="i;unicode-casemap" match-type="contains">`+search+`</C:text-match> + </C:prop-filter> + <C:prop-filter name="FN"> + <C:text-match collation="i;unicode-casemap" match-type="contains">`+search+`</C:text-match> + </C:prop-filter> +</C:filter> +</C:addressbook-query>`)) + 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"` +} |