diff options
author | Simon Ser <contact@emersion.fr> | 2020-02-05 14:58:56 +0100 |
---|---|---|
committer | Simon Ser <contact@emersion.fr> | 2020-02-05 14:58:56 +0100 |
commit | 1bd930f0438ebce5fd0e27aca0f5d5e1c5bcc750 (patch) | |
tree | 2ed420695bf816e34adf38c02d06998a154be918 | |
parent | 3263a89185e27031dbde7007eb4b71db4cd3c54f (diff) | |
download | alps-1bd930f0438ebce5fd0e27aca0f5d5e1c5bcc750.tar.gz alps-1bd930f0438ebce5fd0e27aca0f5d5e1c5bcc750.zip |
plugins/carddav: add basic contacts view
-rwxr-xr-x | plugins/base/imap.go | 2 | ||||
-rw-r--r-- | plugins/carddav/carddav.go | 52 | ||||
-rw-r--r-- | plugins/carddav/plugin.go | 42 | ||||
-rw-r--r-- | plugins/carddav/public/address-book.html | 34 | ||||
-rw-r--r-- | plugins/carddav/public/address-object.html | 22 | ||||
-rw-r--r-- | plugins/carddav/routes.go | 108 |
6 files changed, 223 insertions, 37 deletions
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"}} + +<h1>koushin</h1> + +<p> + <a href="/">Back</a> +</p> + +<h2>Contacts: {{.AddressBook.Name}}</h2> + +<form method="get" action=""> + <input type="search" name="query" value="{{.Query}}"> + <input type="submit" value="Search"> +</form> + +{{if .AddressObjects}} + <ul> + {{range .AddressObjects}} + <li> + <a href="/contacts/{{.Card.Value "UID" | pathescape}}"> + {{.Card.Value "FN"}} + </a> + {{$email := .Card.PreferredValue "EMAIL"}} + {{if $email}} + <<a href="/compose?to={{$email}}">{{$email}}</a>> + {{end}} + </li> + {{end}} + </ul> +{{else}} + <p>No contact.</p> +{{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"}} + +<h1>koushin</h1> + +<p> + <a href="/contacts">Back</a> +</p> + +{{$fn := .AddressObject.Card.Value "FN"}} + +<h2>Contact: {{$fn}}</h2> + +<ul> + <li><strong>Name</strong>: {{$fn}}</li> + {{range .AddressObject.Card.Values "EMAIL"}} + <li><strong>E-mail</strong>: + <a href="/compose?to={{.}}">{{.}}</a> + </li> + {{end}} +</ul> + +{{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, + }) + }) +} |