carddav: update to go-webdav v0.2.0

This commit is contained in:
Simon Ser 2020-02-04 21:28:25 +01:00
parent 5fb5ec9775
commit 7d3d0ee71e
No known key found for this signature in database
GPG Key ID: 0FDE7BE0E88F5E48
3 changed files with 181 additions and 197 deletions

View File

@ -3,11 +3,13 @@ package carddav
import ( import (
"bytes" "bytes"
"errors" "errors"
"fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"os" "path"
"strconv" "strconv"
"strings"
"sync" "sync"
"time" "time"
@ -17,9 +19,8 @@ import (
"golang.org/x/crypto/openpgp" "golang.org/x/crypto/openpgp"
) )
type contextKey string // TODO: use a HTTP error
var errNotFound = errors.New("carddav: not found")
const ClientContextKey = contextKey("client")
var ( var (
cleartextCardProps = []string{vcard.FieldVersion, vcard.FieldProductID, "X-PM-LABEL", "X-PM-GROUP"} cleartextCardProps = []string{vcard.FieldVersion, vcard.FieldProductID, "X-PM-LABEL", "X-PM-GROUP"}
@ -80,52 +81,25 @@ func formatCard(card vcard.Card, privateKey *openpgp.Entity) (*protonmail.Contac
return &contactImport, nil return &contactImport, nil
} }
type addressFileInfo struct { func parseAddressObjectPath(p string) (string, error) {
contact *protonmail.Contact dirname, filename := path.Split(p)
ext := path.Ext(filename)
if dirname != "/" || ext != ".vcf" {
return "", errNotFound
}
return strings.TrimSuffix(filename, ext), nil
} }
func (fi *addressFileInfo) Name() string { func formatAddressObjectPath(id string) string {
return fi.contact.ID return "/" + id + ".vcf"
} }
func (fi *addressFileInfo) Size() int64 { func (b *backend) toAddressObject(contact *protonmail.Contact, req *carddav.AddressDataRequest) (*carddav.AddressObject, error) {
return int64(fi.contact.Size) // TODO: handle req
}
func (fi *addressFileInfo) Mode() os.FileMode {
return os.ModePerm
}
func (fi *addressFileInfo) ModTime() time.Time {
return time.Unix(fi.contact.ModifyTime, 0)
}
func (fi *addressFileInfo) IsDir() bool {
return false
}
func (fi *addressFileInfo) Sys() interface{} {
return nil
}
type addressObject struct {
ab *addressBook
contact *protonmail.Contact
}
func (ao *addressObject) ID() string {
return ao.contact.ID
}
func (ao *addressObject) Stat() (os.FileInfo, error) {
return &addressFileInfo{ao.contact}, nil
}
func (ao *addressObject) Card() (vcard.Card, error) {
card := make(vcard.Card) card := make(vcard.Card)
for _, c := range contact.Cards {
for _, c := range ao.contact.Cards { md, err := c.Read(b.privateKeys)
md, err := c.Read(ao.ab.privateKeys)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -149,80 +123,93 @@ func (ao *addressObject) Card() (vcard.Card, error) {
} }
} }
return card, nil return &carddav.AddressObject{
Path: formatAddressObjectPath(contact.ID),
ModTime: time.Unix(contact.ModifyTime, 0),
// TODO: stronger ETag
ETag: fmt.Sprintf("%x%x", contact.ModifyTime, contact.Size),
Card: card,
}, nil
} }
func (ao *addressObject) SetCard(card vcard.Card) error { type backend struct {
contactImport, err := formatCard(card, ao.ab.privateKeys[0])
if err != nil {
return err
}
contact, err := ao.ab.c.UpdateContact(ao.contact.ID, contactImport)
if err != nil {
return err
}
contact.Cards = contactImport.Cards // Not returned by the server
ao.contact = contact
return nil
}
func (ao *addressObject) Remove() error {
resps, err := ao.ab.c.DeleteContacts([]string{ao.contact.ID})
if err != nil {
return err
}
if len(resps) != 1 {
return errors.New("hydroxide/carddav: expected exactly one response when deleting contact")
}
resp := resps[0]
return resp.Err()
}
type addressBook struct {
c *protonmail.Client c *protonmail.Client
cache map[string]*addressObject cache map[string]*protonmail.Contact
locker sync.Mutex locker sync.Mutex
total int total int
privateKeys openpgp.EntityList privateKeys openpgp.EntityList
} }
func (ab *addressBook) Info() (*carddav.AddressBookInfo, error) { func (b *backend) AddressBook() (*carddav.AddressBook, error) {
return &carddav.AddressBookInfo{ return &carddav.AddressBook{
Path: "/",
Name: "ProtonMail", Name: "ProtonMail",
Description: "ProtonMail contacts", Description: "ProtonMail contacts",
MaxResourceSize: 100 * 1024, MaxResourceSize: 100 * 1024,
}, nil }, nil
} }
func (ab *addressBook) cacheComplete() bool { func (b *backend) cacheComplete() bool {
ab.locker.Lock() b.locker.Lock()
defer ab.locker.Unlock() defer b.locker.Unlock()
return ab.total >= 0 && len(ab.cache) == ab.total return b.total >= 0 && len(b.cache) == b.total
} }
func (ab *addressBook) addressObject(id string) (*addressObject, bool) { func (b *backend) getCache(id string) (*protonmail.Contact, bool) {
ab.locker.Lock() b.locker.Lock()
defer ab.locker.Unlock() contact, ok := b.cache[id]
ao, ok := ab.cache[id] b.locker.Unlock()
return ao, ok return contact, ok
} }
func (ab *addressBook) cacheAddressObject(ao *addressObject) { func (b *backend) putCache(contact *protonmail.Contact) {
ab.locker.Lock() b.locker.Lock()
defer ab.locker.Unlock() b.cache[contact.ID] = contact
ab.cache[ao.contact.ID] = ao b.locker.Unlock()
} }
func (ab *addressBook) ListAddressObjects() ([]carddav.AddressObject, error) { func (b *backend) deleteCache(id string) {
if ab.cacheComplete() { b.locker.Lock()
ab.locker.Lock() delete(b.cache, id)
defer ab.locker.Unlock() b.locker.Unlock()
}
aos := make([]carddav.AddressObject, 0, len(ab.cache)) func (b *backend) GetAddressObject(path string, req *carddav.AddressDataRequest) (*carddav.AddressObject, error) {
for _, ao := range ab.cache { id, err := parseAddressObjectPath(path)
aos = append(aos, ao) if err != nil {
return nil, err
}
contact, ok := b.getCache(id)
if !ok {
if b.cacheComplete() {
return nil, errNotFound
}
contact, err = b.c.GetContact(id)
if apiErr, ok := err.(*protonmail.APIError); ok && apiErr.Code == 13051 {
return nil, errNotFound
} else if err != nil {
return nil, err
}
b.putCache(contact)
}
return b.toAddressObject(contact, req)
}
func (b *backend) ListAddressObjects(req *carddav.AddressDataRequest) ([]carddav.AddressObject, error) {
if b.cacheComplete() {
b.locker.Lock()
defer b.locker.Unlock()
aos := make([]carddav.AddressObject, 0, len(b.cache))
for _, contact := range b.cache {
ao, err := b.toAddressObject(contact, req)
if err != nil {
return nil, err
}
aos = append(aos, *ao)
} }
return aos, nil return aos, nil
@ -230,48 +217,41 @@ func (ab *addressBook) ListAddressObjects() ([]carddav.AddressObject, error) {
// Get a list of all contacts // Get a list of all contacts
// TODO: paging support // TODO: paging support
total, contacts, err := ab.c.ListContacts(0, 0) total, contacts, err := b.c.ListContacts(0, 0)
if err != nil { if err != nil {
return nil, err return nil, err
} }
ab.locker.Lock() b.locker.Lock()
ab.total = total b.total = total
ab.locker.Unlock() b.locker.Unlock()
m := make(map[string]*protonmail.Contact, total)
for _, contact := range contacts { for _, contact := range contacts {
if _, ok := ab.addressObject(contact.ID); !ok { m[contact.ID] = contact
ab.cacheAddressObject(&addressObject{
ab: ab,
contact: contact,
})
}
} }
// Get all contacts cards // Get all contacts cards
var aos []carddav.AddressObject aos := make([]carddav.AddressObject, 0, total)
page := 0 page := 0
for { for {
_, contacts, err := ab.c.ListContactsExport(page, 0) _, contacts, err := b.c.ListContactsExport(page, 0)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if aos == nil { for _, contactExport := range contacts {
aos = make([]carddav.AddressObject, 0, total) contact, ok := m[contactExport.ID]
}
for _, contact := range contacts {
ao, ok := ab.addressObject(contact.ID)
if !ok { if !ok {
ao = &addressObject{ continue
ab: ab,
contact: &protonmail.Contact{ID: contact.ID},
}
ab.cacheAddressObject(ao)
} }
contact.Cards = contactExport.Cards
b.putCache(contact)
ao.contact.Cards = contact.Cards ao, err := b.toAddressObject(contact, req)
aos = append(aos, ao) if err != nil {
return nil, err
}
aos = append(aos, *ao)
} }
if len(aos) >= total || len(contacts) == 0 { if len(aos) >= total || len(contacts) == 0 {
@ -283,79 +263,93 @@ func (ab *addressBook) ListAddressObjects() ([]carddav.AddressObject, error) {
return aos, nil return aos, nil
} }
func (ab *addressBook) GetAddressObject(id string) (carddav.AddressObject, error) { func (b *backend) QueryAddressObjects(query *carddav.AddressBookQuery) ([]carddav.AddressObject, error) {
if ao, ok := ab.addressObject(id); ok { panic("TODO")
return ao, nil
} else if ab.cacheComplete() {
return nil, carddav.ErrNotFound
} }
contact, err := ab.c.GetContact(id) func (b *backend) PutAddressObject(path string, card vcard.Card) (loc string, err error) {
id, err := parseAddressObjectPath(path)
if err != nil { if err != nil {
// TODO: return carddav.ErrNotFound if appropriate return "", err
return nil, err
} }
ao := &addressObject{ contactImport, err := formatCard(card, b.privateKeys[0])
ab: ab,
contact: contact,
}
ab.cacheAddressObject(ao)
return ao, nil
}
func (ab *addressBook) CreateAddressObject(card vcard.Card) (carddav.AddressObject, error) {
contactImport, err := formatCard(card, ab.privateKeys[0])
if err != nil { if err != nil {
return nil, err return "", err
} }
resps, err := ab.c.CreateContacts([]*protonmail.ContactImport{contactImport}) var contact *protonmail.Contact
var req carddav.AddressDataRequest
if _, getErr := b.GetAddressObject(path, &req); getErr == nil {
contact, err = b.c.UpdateContact(id, contactImport)
if err != nil { if err != nil {
return nil, err return "", err
}
} else {
resps, err := b.c.CreateContacts([]*protonmail.ContactImport{contactImport})
if err != nil {
return "", err
} }
if len(resps) != 1 { if len(resps) != 1 {
return nil, errors.New("hydroxide/carddav: expected exactly one response when creating contact") return "", errors.New("hydroxide/carddav: expected exactly one response when creating contact")
} }
resp := resps[0] resp := resps[0]
if err := resp.Err(); err != nil { if err := resp.Err(); err != nil {
return nil, err return "", err
}
contact = resp.Response.Contact
} }
contact := resp.Response.Contact
contact.Cards = contactImport.Cards // Not returned by the server contact.Cards = contactImport.Cards // Not returned by the server
ao := &addressObject{ // TODO: increment b.total if necessary
ab: ab, b.putCache(contact)
contact: contact, return formatAddressObjectPath(contact.ID), nil
}
ab.cacheAddressObject(ao)
return ao, nil
} }
func (ab *addressBook) receiveEvents(events <-chan *protonmail.Event) { func (b *backend) DeleteAddressObject(path string) error {
id, err := parseAddressObjectPath(path)
if err != nil {
return err
}
resps, err := b.c.DeleteContacts([]string{id})
if err != nil {
return err
}
if len(resps) != 1 {
return errors.New("hydroxide/carddav: expected exactly one response when deleting contact")
}
resp := resps[0]
// TODO: decrement b.total if necessary
b.deleteCache(id)
return resp.Err()
}
func (b *backend) receiveEvents(events <-chan *protonmail.Event) {
for event := range events { for event := range events {
ab.locker.Lock() b.locker.Lock()
if event.Refresh&protonmail.EventRefreshContacts != 0 { if event.Refresh&protonmail.EventRefreshContacts != 0 {
ab.cache = make(map[string]*addressObject) b.cache = make(map[string]*protonmail.Contact)
ab.total = -1 b.total = -1
} else if len(event.Contacts) > 0 { } else if len(event.Contacts) > 0 {
for _, eventContact := range event.Contacts { for _, eventContact := range event.Contacts {
switch eventContact.Action { switch eventContact.Action {
case protonmail.EventCreate: case protonmail.EventCreate:
ab.total++ if b.total >= 0 {
b.total++
}
fallthrough fallthrough
case protonmail.EventUpdate: case protonmail.EventUpdate:
ab.cache[eventContact.ID] = &addressObject{ b.cache[eventContact.ID] = eventContact.Contact
ab: ab,
contact: eventContact.Contact,
}
case protonmail.EventDelete: case protonmail.EventDelete:
delete(ab.cache, eventContact.ID) delete(b.cache, eventContact.ID)
ab.total-- if b.total >= 0 {
b.total--
} }
} }
} }
ab.locker.Unlock() }
b.locker.Unlock()
} }
} }
@ -364,16 +358,16 @@ func NewHandler(c *protonmail.Client, privateKeys openpgp.EntityList, events <-c
panic("hydroxide/carddav: no private key available") panic("hydroxide/carddav: no private key available")
} }
ab := &addressBook{ b := &backend{
c: c, c: c,
cache: make(map[string]*addressObject), cache: make(map[string]*protonmail.Contact),
total: -1, total: -1,
privateKeys: privateKeys, privateKeys: privateKeys,
} }
if events != nil { if events != nil {
go ab.receiveEvents(events) go b.receiveEvents(events)
} }
return carddav.NewHandler(ab) return &carddav.Handler{b}
} }

10
go.mod
View File

@ -5,19 +5,17 @@ go 1.13
require ( require (
github.com/boltdb/bolt v1.3.1 github.com/boltdb/bolt v1.3.1
github.com/emersion/go-bcrypt v0.0.0-20170822072041-6e724a1baa63 github.com/emersion/go-bcrypt v0.0.0-20170822072041-6e724a1baa63
github.com/emersion/go-imap v1.0.2 github.com/emersion/go-imap v1.0.3
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342 github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342
github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62 github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62
github.com/emersion/go-message v0.11.1 github.com/emersion/go-message v0.11.1
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b // indirect
github.com/emersion/go-smtp v0.12.1 github.com/emersion/go-smtp v0.12.1
github.com/emersion/go-vcard v0.0.0-20191221110513-5f81fa0d3cc7 github.com/emersion/go-vcard v0.0.0-20191221110513-5f81fa0d3cc7
github.com/emersion/go-webdav v0.1.0 github.com/emersion/go-webdav v0.2.1-0.20200203205455-3ea3818dd842
github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c
github.com/kr/pretty v0.1.0 // indirect github.com/kr/pretty v0.1.0 // indirect
github.com/stretchr/testify v1.4.0 // indirect github.com/stretchr/testify v1.4.0 // indirect
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad golang.org/x/crypto v0.0.0-20200204104054-c9f3fb736b72
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa // indirect golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 // indirect
golang.org/x/sys v0.0.0-20200117145432-59e60aa80a0c // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
) )

24
go.sum
View File

@ -6,14 +6,12 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emersion/go-bcrypt v0.0.0-20170822072041-6e724a1baa63 h1:7aCSuwTBzg7BCPRRaBJD0weKZYdeAykOrY6ktpx8Vvc= github.com/emersion/go-bcrypt v0.0.0-20170822072041-6e724a1baa63 h1:7aCSuwTBzg7BCPRRaBJD0weKZYdeAykOrY6ktpx8Vvc=
github.com/emersion/go-bcrypt v0.0.0-20170822072041-6e724a1baa63/go.mod h1:eRwwJnuLVFtYTC+AI2JDJTMcuQUTYhBIK4I6bC5tpqw= github.com/emersion/go-bcrypt v0.0.0-20170822072041-6e724a1baa63/go.mod h1:eRwwJnuLVFtYTC+AI2JDJTMcuQUTYhBIK4I6bC5tpqw=
github.com/emersion/go-imap v1.0.2 h1:Uf+Bv8SeV067xYBWyo7rLZ7ZbkrAXLSc1G+2IVDp5JU= github.com/emersion/go-imap v1.0.3 h1:5eEee8/DTSIPfliiWqwfvjPGkU8bBtvOy/Wx+eeXzO4=
github.com/emersion/go-imap v1.0.2/go.mod h1:TjT+1ncDso8j/VXeUHcZeQknho5hjyQLqEIybJJjjDI= github.com/emersion/go-imap v1.0.3/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU=
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342 h1:5p1t3e1PomYgLWwEwhwEU5kVBwcyAcVrOpexv8AeZx0= github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342 h1:5p1t3e1PomYgLWwEwhwEU5kVBwcyAcVrOpexv8AeZx0=
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342/go.mod h1:QuMaZcKFDVI0yCrnAbPLfbwllz1wtOrZH8/vZ5yzp4w= github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342/go.mod h1:QuMaZcKFDVI0yCrnAbPLfbwllz1wtOrZH8/vZ5yzp4w=
github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62 h1:4ZAfwfc8aDlj26kkEap1UDSwwDnJp9Ie8Uj1MSXAkPk= github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62 h1:4ZAfwfc8aDlj26kkEap1UDSwwDnJp9Ie8Uj1MSXAkPk=
github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62/go.mod h1:/nybxhI8kXom8Tw6BrHMl42usALvka6meORflnnYwe4= github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62/go.mod h1:/nybxhI8kXom8Tw6BrHMl42usALvka6meORflnnYwe4=
github.com/emersion/go-message v0.10.8 h1:1l1Vb+0By9U1ITTH3FgKfJQWQ9sTI3N1smPe6SS3QXY=
github.com/emersion/go-message v0.10.8/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
github.com/emersion/go-message v0.11.1 h1:0C/S4JIXDTSfXB1vpqdimAYyK4+79fgEAMQ0dSL+Kac= github.com/emersion/go-message v0.11.1 h1:0C/S4JIXDTSfXB1vpqdimAYyK4+79fgEAMQ0dSL+Kac=
github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY= github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
github.com/emersion/go-sasl v0.0.0-20190817083125-240c8404624e h1:ba7YsgX5OV8FjGi5ZWml8Jng6oBrJAb3ahqWMJ5Ce8Q= github.com/emersion/go-sasl v0.0.0-20190817083125-240c8404624e h1:ba7YsgX5OV8FjGi5ZWml8Jng6oBrJAb3ahqWMJ5Ce8Q=
@ -24,12 +22,10 @@ github.com/emersion/go-smtp v0.12.1 h1:1R8BDqrR2HhlGwgFYcOi+BVTvK1bMjAB65QcVpJ5s
github.com/emersion/go-smtp v0.12.1/go.mod h1:SD9V/xa4ndMw77lR3Mf7htkp8RBNYuPh9UeuBs9tpUQ= github.com/emersion/go-smtp v0.12.1/go.mod h1:SD9V/xa4ndMw77lR3Mf7htkp8RBNYuPh9UeuBs9tpUQ=
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe h1:40SWqY0zE3qCi6ZrtTf5OUdNm5lDnGnjRSq9GgmeTrg= github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe h1:40SWqY0zE3qCi6ZrtTf5OUdNm5lDnGnjRSq9GgmeTrg=
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5 h1:n9qx98xiS5V4x2WIpPC2rr9mUM5ri9r/YhCEKbhCHro=
github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5/go.mod h1:WIi9g8OKJQHXtQbx7GExlo6UAFaui9WDMYabJ+Be4WI=
github.com/emersion/go-vcard v0.0.0-20191221110513-5f81fa0d3cc7 h1:SE+tcd+0kn0cT4MqTo66gmkjqWHF1Z+Yha5/rhLs/H8= github.com/emersion/go-vcard v0.0.0-20191221110513-5f81fa0d3cc7 h1:SE+tcd+0kn0cT4MqTo66gmkjqWHF1Z+Yha5/rhLs/H8=
github.com/emersion/go-vcard v0.0.0-20191221110513-5f81fa0d3cc7/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM= github.com/emersion/go-vcard v0.0.0-20191221110513-5f81fa0d3cc7/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
github.com/emersion/go-webdav v0.1.0 h1:d45qVFZebAj8Elzk59pfb4DgIjW3bmXjtzvmD83mjQw= github.com/emersion/go-webdav v0.2.1-0.20200203205455-3ea3818dd842 h1:CkiVEfQsswQP265LwFfylFiMhQVNQx0LrROT6dlv9ZQ=
github.com/emersion/go-webdav v0.1.0/go.mod h1:PDMbYd9nLfHDxQw18jkbpGCCYO8gzByT3M1VnG9L05c= github.com/emersion/go-webdav v0.2.1-0.20200203205455-3ea3818dd842/go.mod h1:5xPS/W9bnqkHMLtgygkgGFvm6uAsbeVRSIZk0ewu4d4=
github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c h1:aY2hhxLhjEAbfXOx2nRJxCXezC6CO2V/yN+OCr1srtk= github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c h1:aY2hhxLhjEAbfXOx2nRJxCXezC6CO2V/yN+OCr1srtk=
github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs= github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
@ -48,17 +44,13 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad h1:Jh8cai0fqIK+f6nG0UgPW5wFk8wmiMhM3AyciDBdtQg= golang.org/x/crypto v0.0.0-20200204104054-c9f3fb736b72 h1:+ELyKg6m8UBf0nPFSqD0mi7zUfwPyXo23HNjMnXPz7w=
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200204104054-c9f3fb736b72/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200117145432-59e60aa80a0c h1:gUYreENmqtjZb2brVfUas1sC6UivSY8XwKwPo8tloLs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0=
golang.org/x/sys v0.0.0-20200117145432-59e60aa80a0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=