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) }