From 1bd930f0438ebce5fd0e27aca0f5d5e1c5bcc750 Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Wed, 5 Feb 2020 14:58:56 +0100 Subject: plugins/carddav: add basic contacts view --- plugins/base/imap.go | 2 +- plugins/carddav/carddav.go | 52 ++++++++++++++ plugins/carddav/plugin.go | 42 ++--------- plugins/carddav/public/address-book.html | 34 +++++++++ plugins/carddav/public/address-object.html | 22 ++++++ plugins/carddav/routes.go | 108 +++++++++++++++++++++++++++++ 6 files changed, 223 insertions(+), 37 deletions(-) create mode 100644 plugins/carddav/carddav.go create mode 100644 plugins/carddav/public/address-book.html create mode 100644 plugins/carddav/public/address-object.html create mode 100644 plugins/carddav/routes.go (limited to 'plugins') diff --git a/plugins/base/imap.go b/plugins/base/imap.go index 1ffa774..5de5735 100755 --- a/plugins/base/imap.go +++ b/plugins/base/imap.go @@ -289,7 +289,7 @@ func searchCriteriaHeader(k, v string) *imap.SearchCriteria { } } -func searchCriteriaOr(criteria... *imap.SearchCriteria) *imap.SearchCriteria { +func searchCriteriaOr(criteria ...*imap.SearchCriteria) *imap.SearchCriteria { or := criteria[0] for _, c := range criteria[1:] { or = &imap.SearchCriteria{ diff --git a/plugins/carddav/carddav.go b/plugins/carddav/carddav.go new file mode 100644 index 0000000..55c76b4 --- /dev/null +++ b/plugins/carddav/carddav.go @@ -0,0 +1,52 @@ +package koushincarddav + +import ( + "fmt" + "net/http" + "net/url" + + "git.sr.ht/~emersion/koushin" + "github.com/emersion/go-webdav/carddav" +) + +var errNoAddressBook = fmt.Errorf("carddav: no address book found") + +type authRoundTripper struct { + upstream http.RoundTripper + session *koushin.Session +} + +func (rt *authRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + rt.session.SetHTTPBasicAuth(req) + return rt.upstream.RoundTrip(req) +} + +func getAddressBook(u *url.URL, session *koushin.Session) (*carddav.Client, *carddav.AddressBook, error) { + rt := authRoundTripper{ + upstream: http.DefaultTransport, + session: session, + } + c, err := carddav.NewClient(&http.Client{Transport: &rt}, u.String()) + if err != nil { + return nil, nil, fmt.Errorf("failed to create CardDAV client: %v", err) + } + + principal, err := c.FindCurrentUserPrincipal() + if err != nil { + return nil, nil, fmt.Errorf("failed to query CardDAV principal: %v", err) + } + + addressBookHomeSet, err := c.FindAddressBookHomeSet(principal) + if err != nil { + return nil, nil, fmt.Errorf("failed to query CardDAV address book home set: %v", err) + } + + addressBooks, err := c.FindAddressBooks(addressBookHomeSet) + if err != nil { + return nil, nil, fmt.Errorf("failed to query CardDAV address books: %v", err) + } + if len(addressBooks) == 0 { + return nil, nil, errNoAddressBook + } + return c, &addressBooks[0], nil +} diff --git a/plugins/carddav/plugin.go b/plugins/carddav/plugin.go index 317a0d0..99e5f62 100644 --- a/plugins/carddav/plugin.go +++ b/plugins/carddav/plugin.go @@ -30,20 +30,9 @@ func sanityCheckURL(u *url.URL) error { return nil } -type authRoundTripper struct { - upstream http.RoundTripper - session *koushin.Session -} - -func (rt *authRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - rt.session.SetHTTPBasicAuth(req) - return rt.upstream.RoundTrip(req) -} - func newPlugin(srv *koushin.Server) (koushin.Plugin, error) { u, err := srv.Upstream("carddavs", "carddav+insecure", "https", "http+insecure") if _, ok := err.(*koushin.NoUpstreamError); ok { - srv.Logger().Print("carddav: no upstream server provided") return nil, nil } else if err != nil { return nil, fmt.Errorf("carddav: failed to parse upstream CardDAV server: %v", err) @@ -74,36 +63,17 @@ func newPlugin(srv *koushin.Server) (koushin.Plugin, error) { p := koushin.GoPlugin{Name: "carddav"} + registerRoutes(&p, u) + p.Inject("compose.html", func(ctx *koushin.Context, _data koushin.RenderData) error { data := _data.(*koushinbase.ComposeRenderData) - rt := authRoundTripper{ - upstream: http.DefaultTransport, - session: ctx.Session, - } - c, err := carddav.NewClient(&http.Client{Transport: &rt}, u.String()) - if err != nil { - return fmt.Errorf("failed to create CardDAV client: %v", err) - } - - principal, err := c.FindCurrentUserPrincipal() - if err != nil { - return fmt.Errorf("failed to query CardDAV principal: %v", err) - } - - addressBookHomeSet, err := c.FindAddressBookHomeSet(principal) - if err != nil { - return fmt.Errorf("failed to query CardDAV address book home set: %v", err) - } - - addressBooks, err := c.FindAddressBooks(addressBookHomeSet) - if err != nil { - return fmt.Errorf("failed to query CardDAV address books: %v", err) - } - if len(addressBooks) == 0 { + c, addressBook, err := getAddressBook(u, ctx.Session) + if err == errNoAddressBook { return nil + } else if err != nil { + return err } - addressBook := addressBooks[0] query := carddav.AddressBookQuery{ DataRequest: carddav.AddressDataRequest{ diff --git a/plugins/carddav/public/address-book.html b/plugins/carddav/public/address-book.html new file mode 100644 index 0000000..f521564 --- /dev/null +++ b/plugins/carddav/public/address-book.html @@ -0,0 +1,34 @@ +{{template "head.html"}} + +

koushin

+ +

+ Back +

+ +

Contacts: {{.AddressBook.Name}}

+ +
+ + +
+ +{{if .AddressObjects}} + +{{else}} +

No contact.

+{{end}} + +{{template "foot.html"}} diff --git a/plugins/carddav/public/address-object.html b/plugins/carddav/public/address-object.html new file mode 100644 index 0000000..2c96fd4 --- /dev/null +++ b/plugins/carddav/public/address-object.html @@ -0,0 +1,22 @@ +{{template "head.html"}} + +

koushin

+ +

+ Back +

+ +{{$fn := .AddressObject.Card.Value "FN"}} + +

Contact: {{$fn}}

+ + + +{{template "foot.html"}} diff --git a/plugins/carddav/routes.go b/plugins/carddav/routes.go new file mode 100644 index 0000000..82b729e --- /dev/null +++ b/plugins/carddav/routes.go @@ -0,0 +1,108 @@ +package koushincarddav + +import ( + "fmt" + "net/http" + "net/url" + + "git.sr.ht/~emersion/koushin" + "github.com/emersion/go-vcard" + "github.com/emersion/go-webdav/carddav" +) + +type AddressBookRenderData struct { + koushin.BaseRenderData + AddressBook *carddav.AddressBook + AddressObjects []carddav.AddressObject + Query string +} + +type AddressObjectRenderData struct { + koushin.BaseRenderData + AddressObject *carddav.AddressObject +} + +func registerRoutes(p *koushin.GoPlugin, u *url.URL) { + p.GET("/contacts", func(ctx *koushin.Context) error { + queryText := ctx.QueryParam("query") + + c, addressBook, err := getAddressBook(u, ctx.Session) + if err != nil { + return err + } + + query := carddav.AddressBookQuery{ + DataRequest: carddav.AddressDataRequest{ + Props: []string{ + vcard.FieldFormattedName, + vcard.FieldEmail, + vcard.FieldUID, + }, + }, + } + + if queryText != "" { + query.PropFilters = []carddav.PropFilter{ + { + Name: vcard.FieldFormattedName, + TextMatches: []carddav.TextMatch{{Text: queryText}}, + }, + { + Name: vcard.FieldEmail, + TextMatches: []carddav.TextMatch{{Text: queryText}}, + }, + } + } + + addrs, err := c.QueryAddressBook(addressBook.Path, &query) + if err != nil { + return fmt.Errorf("failed to query CardDAV addresses: %v", err) + } + + return ctx.Render(http.StatusOK, "address-book.html", &AddressBookRenderData{ + BaseRenderData: *koushin.NewBaseRenderData(ctx), + AddressBook: addressBook, + AddressObjects: addrs, + Query: queryText, + }) + }) + + p.GET("/contacts/:uid", func(ctx *koushin.Context) error { + uid := ctx.Param("uid") + + c, addressBook, err := getAddressBook(u, ctx.Session) + if err != nil { + return err + } + + query := carddav.AddressBookQuery{ + DataRequest: carddav.AddressDataRequest{ + Props: []string{ + vcard.FieldFormattedName, + vcard.FieldEmail, + vcard.FieldUID, + }, + }, + PropFilters: []carddav.PropFilter{{ + Name: vcard.FieldUID, + TextMatches: []carddav.TextMatch{{ + Text: uid, + MatchType: carddav.MatchEquals, + }}, + }}, + } + addrs, err := c.QueryAddressBook(addressBook.Path, &query) + if err != nil { + return fmt.Errorf("failed to query CardDAV address: %v", err) + } + if len(addrs) != 1 { + return fmt.Errorf("expected exactly one address object with UID %q, got %v", uid, len(addrs)) + } + addr := &addrs[0] + + return ctx.Render(http.StatusOK, "address-object.html", &AddressObjectRenderData{ + BaseRenderData: *koushin.NewBaseRenderData(ctx), + AddressObject: addr, + }) + }) +} -- cgit v1.2.3