aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSimon Ser <contact@emersion.fr>2020-02-05 14:58:56 +0100
committerSimon Ser <contact@emersion.fr>2020-02-05 14:58:56 +0100
commit1bd930f0438ebce5fd0e27aca0f5d5e1c5bcc750 (patch)
tree2ed420695bf816e34adf38c02d06998a154be918
parent3263a89185e27031dbde7007eb4b71db4cd3c54f (diff)
downloadalps-1bd930f0438ebce5fd0e27aca0f5d5e1c5bcc750.tar.gz
alps-1bd930f0438ebce5fd0e27aca0f5d5e1c5bcc750.zip
plugins/carddav: add basic contacts view
-rwxr-xr-xplugins/base/imap.go2
-rw-r--r--plugins/carddav/carddav.go52
-rw-r--r--plugins/carddav/plugin.go42
-rw-r--r--plugins/carddav/public/address-book.html34
-rw-r--r--plugins/carddav/public/address-object.html22
-rw-r--r--plugins/carddav/routes.go108
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}}
+ &lt;<a href="/compose?to={{$email}}">{{$email}}</a>&gt;
+ {{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,
+ })
+ })
+}