2020-01-10 18:37:56 +00:00
|
|
|
package koushincarddav
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
2020-02-12 20:13:51 +00:00
|
|
|
"strings"
|
2020-01-10 18:37:56 +00:00
|
|
|
|
|
|
|
"git.sr.ht/~emersion/koushin"
|
|
|
|
koushinbase "git.sr.ht/~emersion/koushin/plugins/base"
|
|
|
|
"github.com/emersion/go-vcard"
|
|
|
|
"github.com/emersion/go-webdav/carddav"
|
|
|
|
)
|
|
|
|
|
2020-02-05 12:52:52 +00:00
|
|
|
func sanityCheckURL(u *url.URL) error {
|
|
|
|
req, err := http.NewRequest(http.MethodOptions, u.String(), nil)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
resp.Body.Close()
|
|
|
|
|
|
|
|
// Servers might require authentication to perform an OPTIONS request
|
|
|
|
if resp.StatusCode/100 != 2 && resp.StatusCode != http.StatusUnauthorized {
|
|
|
|
return fmt.Errorf("HTTP request failed: %v %v", resp.StatusCode, resp.Status)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-02-11 18:14:05 +00:00
|
|
|
type plugin struct {
|
|
|
|
koushin.GoPlugin
|
|
|
|
url *url.URL
|
|
|
|
homeSetCache map[string]string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *plugin) clientWithAddressBook(session *koushin.Session) (*carddav.Client, *carddav.AddressBook, error) {
|
|
|
|
c, err := newClient(p.url, session)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, fmt.Errorf("failed to create CardDAV client: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
homeSet, ok := p.homeSetCache[session.Username()]
|
|
|
|
if !ok {
|
|
|
|
principal, err := c.FindCurrentUserPrincipal()
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, fmt.Errorf("failed to query CardDAV principal: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
homeSet, err = c.FindAddressBookHomeSet(principal)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, fmt.Errorf("failed to query CardDAV address book home set: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
p.homeSetCache[session.Username()] = homeSet
|
|
|
|
// TODO: evict entries from the cache if it's getting too big
|
|
|
|
}
|
|
|
|
|
|
|
|
addressBooks, err := c.FindAddressBooks(homeSet)
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2020-01-10 18:37:56 +00:00
|
|
|
func newPlugin(srv *koushin.Server) (koushin.Plugin, error) {
|
2020-02-05 12:56:18 +00:00
|
|
|
u, err := srv.Upstream("carddavs", "carddav+insecure", "https", "http+insecure")
|
2020-01-10 18:37:56 +00:00
|
|
|
if _, ok := err.(*koushin.NoUpstreamError); ok {
|
|
|
|
return nil, nil
|
|
|
|
} else if err != nil {
|
|
|
|
return nil, fmt.Errorf("carddav: failed to parse upstream CardDAV server: %v", err)
|
|
|
|
}
|
2020-02-05 12:56:18 +00:00
|
|
|
switch u.Scheme {
|
|
|
|
case "carddavs":
|
|
|
|
u.Scheme = "https"
|
|
|
|
case "carddav+insecure", "http+insecure":
|
2020-01-10 18:37:56 +00:00
|
|
|
u.Scheme = "http"
|
|
|
|
}
|
|
|
|
if u.Scheme == "" {
|
|
|
|
s, err := carddav.Discover(u.Host)
|
|
|
|
if err != nil {
|
|
|
|
srv.Logger().Printf("carddav: failed to discover CardDAV server: %v", err)
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
u, err = url.Parse(s)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("carddav: Discover returned an invalid URL: %v", err)
|
|
|
|
}
|
|
|
|
}
|
2020-02-05 12:52:52 +00:00
|
|
|
|
|
|
|
if err := sanityCheckURL(u); err != nil {
|
|
|
|
return nil, fmt.Errorf("carddav: failed to connect to CardDAV server %q: %v", u, err)
|
|
|
|
}
|
|
|
|
|
2020-01-10 18:37:56 +00:00
|
|
|
srv.Logger().Printf("Configured upstream CardDAV server: %v", u)
|
|
|
|
|
2020-02-11 18:14:05 +00:00
|
|
|
p := &plugin{
|
|
|
|
GoPlugin: koushin.GoPlugin{Name: "carddav"},
|
|
|
|
url: u,
|
|
|
|
homeSetCache: make(map[string]string),
|
|
|
|
}
|
2020-01-10 18:37:56 +00:00
|
|
|
|
2020-02-11 18:14:05 +00:00
|
|
|
registerRoutes(p)
|
2020-02-05 13:58:56 +00:00
|
|
|
|
2020-02-12 20:13:51 +00:00
|
|
|
p.TemplateFuncs(map[string]interface{}{
|
|
|
|
"join": func(l []string, sep string) string {
|
|
|
|
return strings.Join(l, sep)
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
2020-01-10 18:37:56 +00:00
|
|
|
p.Inject("compose.html", func(ctx *koushin.Context, _data koushin.RenderData) error {
|
|
|
|
data := _data.(*koushinbase.ComposeRenderData)
|
|
|
|
|
2020-02-11 18:14:05 +00:00
|
|
|
c, addressBook, err := p.clientWithAddressBook(ctx.Session)
|
2020-02-05 13:58:56 +00:00
|
|
|
if err == errNoAddressBook {
|
2020-01-10 18:37:56 +00:00
|
|
|
return nil
|
2020-02-05 13:58:56 +00:00
|
|
|
} else if err != nil {
|
|
|
|
return err
|
2020-01-10 18:37:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
query := carddav.AddressBookQuery{
|
|
|
|
DataRequest: carddav.AddressDataRequest{
|
|
|
|
Props: []string{vcard.FieldFormattedName, vcard.FieldEmail},
|
|
|
|
},
|
2020-02-12 16:31:14 +00:00
|
|
|
PropFilters: []carddav.PropFilter{{
|
|
|
|
Name: vcard.FieldEmail,
|
|
|
|
}},
|
2020-01-10 18:37:56 +00:00
|
|
|
}
|
|
|
|
addrs, err := c.QueryAddressBook(addressBook.Path, &query)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to query CardDAV addresses: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: cache the results
|
|
|
|
emails := make([]string, 0, len(addrs))
|
|
|
|
for _, addr := range addrs {
|
|
|
|
cardEmails := addr.Card.Values(vcard.FieldEmail)
|
|
|
|
emails = append(emails, cardEmails...)
|
|
|
|
}
|
|
|
|
|
|
|
|
data.Extra["EmailSuggestions"] = emails
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
|
|
|
|
return p.Plugin(), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
koushin.RegisterPluginLoader(func(s *koushin.Server) ([]koushin.Plugin, error) {
|
|
|
|
p, err := newPlugin(s)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if p == nil {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
return []koushin.Plugin{p}, err
|
|
|
|
})
|
|
|
|
}
|