package dav import ( "context" "encoding/xml" "fmt" "io" "net/http" "net/url" "strings" ) // FindPrincipal requires credentials on client; a 401/403 from the server // bubbles up as the returned error. func FindPrincipal(ctx context.Context, client *http.Client, contextURL string) (string, error) { body := ` ` hrefs, err := propFind(ctx, client, contextURL, "0", body) if err != nil { return "", err } for _, href := range hrefs.principalHref() { return resolveReference(contextURL, href), nil } return "", fmt.Errorf("no current-user-principal returned") } func FindScheduleURLs(ctx context.Context, client *http.Client, principalURL string) (inbox, outbox string, err error) { body := ` ` resp, err := propFind(ctx, client, principalURL, "0", body) if err != nil { return "", "", err } for _, r := range resp.Response { for _, ps := range r.Propstat { if !strings.Contains(ps.Status, "200") { continue } for _, p := range ps.Prop.Props { switch p.XMLName.Local { case "schedule-inbox-URL": if h := p.firstHref(); h != "" { inbox = resolveReference(principalURL, h) } case "schedule-outbox-URL": if h := p.firstHref(); h != "" { outbox = resolveReference(principalURL, h) } } } } } return inbox, outbox, nil } // ── raw PROPFIND ───────────────────────────────────────────────────────────── // multistatus is the subset of the DAV:multistatus XML schema we need to read // principal URLs and scheduling hrefs. It is intentionally permissive — extra // elements are ignored, which makes us tolerant of server-specific extensions. type multistatus struct { XMLName xml.Name `xml:"DAV: multistatus"` Response []msResponse `xml:"response"` } type msResponse struct { Href string `xml:"href"` Propstat []propstat `xml:"propstat"` } type propstat struct { Prop prop `xml:"prop"` Status string `xml:"status"` } type prop struct { Props []msProp `xml:",any"` } type msProp struct { XMLName xml.Name Hrefs []string `xml:"href"` // A prop may also contain nested , // which the flat Hrefs slice above captures via xml:"href" descent. } func (p msProp) firstHref() string { if len(p.Hrefs) > 0 { return p.Hrefs[0] } return "" } func (m *multistatus) principalHref() []string { var out []string for _, r := range m.Response { for _, ps := range r.Propstat { if !strings.Contains(ps.Status, "200") { continue } for _, pr := range ps.Prop.Props { if pr.XMLName.Local == "current-user-principal" { if h := pr.firstHref(); h != "" { out = append(out, h) } } } } } return out } // propFind is a small PROPFIND helper tuned for small single-resource probes. // It returns a parsed multistatus; transport-level failures bubble up as err. func propFind(ctx context.Context, client *http.Client, url, depth, body string) (*multistatus, error) { req, err := http.NewRequestWithContext(ctx, "PROPFIND", url, strings.NewReader(body)) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/xml; charset=utf-8") req.Header.Set("Depth", depth) resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() data, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode != http.StatusMultiStatus && resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("PROPFIND returned HTTP %d", resp.StatusCode) } var ms multistatus if err := xml.Unmarshal(data, &ms); err != nil { return nil, fmt.Errorf("invalid multistatus: %w", err) } return &ms, nil } func resolveReference(base, ref string) string { r, err := url.Parse(ref) if err != nil { return ref } b, err := url.Parse(base) if err != nil { return ref } return b.ResolveReference(r).String() }