summaryrefslogtreecommitdiff
path: root/pim/cmd/caldavfs/discover.go
diff options
context:
space:
mode:
Diffstat (limited to 'pim/cmd/caldavfs/discover.go')
-rw-r--r--pim/cmd/caldavfs/discover.go130
1 files changed, 130 insertions, 0 deletions
diff --git a/pim/cmd/caldavfs/discover.go b/pim/cmd/caldavfs/discover.go
new file mode 100644
index 0000000..2b5fb23
--- /dev/null
+++ b/pim/cmd/caldavfs/discover.go
@@ -0,0 +1,130 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "net/url"
+ "os"
+ "strings"
+ "time"
+
+ "github.com/emersion/go-webdav"
+
+ "github.com/emersion/go-webdav/caldav"
+ "pim/lib/cal"
+)
+
+// dial builds an authenticated client and finds the calendar to serve.
+//
+// The walk is the one CalDAV prescribes: the endpoint tells you your
+// principal, the principal tells you where your calendars live, and
+// that collection lists them. Naming a calendar by its display name
+// beats hardcoding a path, which servers are free to change.
+func dial(endpoint, user, pass, want string) (*caldav.Client, caldav.Calendar, bool, error) {
+ var zero caldav.Calendar
+
+ hc := webdav.HTTPClientWithBasicAuth(nil, user, pass)
+ c, err := caldav.NewClient(hc, endpoint)
+ if err != nil {
+ return nil, zero, false, err
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
+ defer cancel()
+
+ principal, err := findPrincipal(endpoint, user, pass)
+ if err != nil {
+ return nil, zero, false, fmt.Errorf("principal: %v", err)
+ }
+ home, err := c.FindCalendarHomeSet(ctx, principal)
+ if err != nil {
+ return nil, zero, false, fmt.Errorf("home set: %v", err)
+ }
+ cals, err := c.FindCalendars(ctx, home)
+ if err != nil {
+ return nil, zero, false, fmt.Errorf("calendars: %v", err)
+ }
+ if len(cals) == 0 {
+ return nil, zero, false, fmt.Errorf("no calendars under %s", home)
+ }
+
+ sched := autoSchedule(endpoint, user, pass, home)
+ cal.Warnf("home %s, auto-schedule %v", home, sched)
+
+ if want == "" {
+ // No choice made: list them and refuse, rather than pick one
+ // and have it silently be the wrong calendar.
+ var names []string
+ for _, cl := range cals {
+ names = append(names, cl.Name)
+ }
+ return nil, zero, sched, fmt.Errorf("which calendar? -C one of: %s",
+ strings.Join(names, ", "))
+ }
+ for _, cl := range cals {
+ if cl.Name == want || cl.Path == want {
+ return c, cl, sched, nil
+ }
+ }
+ return nil, zero, sched, fmt.Errorf("no calendar named %q", want)
+}
+
+// autoSchedule asks whether the server sends iMIP on our behalf. A
+// server that does means writing a PARTSTAT both updates our copy and
+// tells the organiser; a server that does not means we must send the
+// mail ourselves.
+func autoSchedule(endpoint, user, pass, path string) bool {
+ req, err := newRequest("OPTIONS", endpoint, path)
+ if err != nil {
+ return false
+ }
+ req.SetBasicAuth(user, pass)
+ resp, err := httpDo(req)
+ if err != nil {
+ return false
+ }
+ defer resp.Body.Close()
+ for _, v := range resp.Header.Values("DAV") {
+ for _, f := range strings.Split(v, ",") {
+ if strings.TrimSpace(f) == "calendar-auto-schedule" {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+// urlJoin resolves a DAV href against the endpoint.
+//
+// An href from the server is absolute on that server: it replaces the
+// endpoint's path rather than extending it. Appending it instead gives
+// /dav/dav/calendars/... which answers 404, and a probe that reads a
+// 404 as "no" reports a capability the server actually has.
+func urlJoin(endpoint, href string) string {
+ base, err := url.Parse(endpoint)
+ if err != nil {
+ return href
+ }
+ ref, err := url.Parse(href)
+ if err != nil {
+ return href
+ }
+ return base.ResolveReference(ref).String()
+}
+
+// readSecret reads a password from a file, so it stays out of argv where
+// ps(1) would show it.
+func readSecret(path string) (string, error) {
+ b, err := os.ReadFile(path)
+ if err != nil {
+ return "", err
+ }
+ for _, line := range strings.Split(string(b), "\n") {
+ line = strings.TrimSpace(line)
+ if line == "" || strings.HasPrefix(line, "#") {
+ continue
+ }
+ return strings.Trim(line, `"'`), nil
+ }
+ return "", fmt.Errorf("%s: empty", path)
+}