diff --git a/caldav/caldav.go b/caldav/caldav.go index 385564f..5ca8b24 100644 --- a/caldav/caldav.go +++ b/caldav/caldav.go @@ -3,9 +3,34 @@ // CalDAV is defined in RFC 4791. package caldav +import ( + "time" +) + type Calendar struct { Path string Name string Description string MaxResourceSize int64 } + +type CalendarCompRequest struct { + Name string + + AllProps bool + Props []string + + AllComps bool + Comps []CalendarCompRequest +} + +type CalendarQuery struct { + Comp CalendarCompRequest +} + +type CalendarObject struct { + Path string + ModTime time.Time + ETag string + Data []byte +} diff --git a/caldav/client.go b/caldav/client.go index a092db0..f7606a8 100644 --- a/caldav/client.go +++ b/caldav/client.go @@ -3,6 +3,8 @@ package caldav import ( "fmt" "net/http" + "strconv" + "time" "github.com/emersion/go-webdav" "github.com/emersion/go-webdav/internal" @@ -102,3 +104,98 @@ func (c *Client) FindCalendars(calendarHomeSet string) ([]Calendar, error) { return l, nil } + +func encodeCalendarCompReq(c *CalendarCompRequest) (*comp, error) { + encoded := comp{Name: c.Name} + + if c.AllProps { + encoded.Allprop = &struct{}{} + } + for _, name := range c.Props { + encoded.Prop = append(encoded.Prop, prop{Name: name}) + } + + if c.AllComps { + encoded.Allcomp = &struct{}{} + } + for _, child := range c.Comps { + encodedChild, err := encodeCalendarCompReq(&child) + if err != nil { + return nil, err + } + encoded.Comp = append(encoded.Comp, *encodedChild) + } + + return &encoded, nil +} + +func encodeCalendarReq(c *CalendarCompRequest) (*internal.Prop, error) { + compReq, err := encodeCalendarCompReq(c) + if err != nil { + return nil, err + } + + calDataReq := calendarDataReq{Comp: compReq} + + getLastModReq := internal.NewRawXMLElement(internal.GetLastModifiedName, nil, nil) + getETagReq := internal.NewRawXMLElement(internal.GetETagName, nil, nil) + return internal.EncodeProp(&calDataReq, getLastModReq, getETagReq) +} + +func decodeCalendarObjectList(ms *internal.Multistatus) ([]CalendarObject, error) { + addrs := make([]CalendarObject, 0, len(ms.Responses)) + for _, resp := range ms.Responses { + path, err := resp.Path() + if err != nil { + return nil, err + } + + var calData calendarDataResp + if err := resp.DecodeProp(&calData); err != nil { + return nil, err + } + + var getLastMod internal.GetLastModified + if err := resp.DecodeProp(&getLastMod); err != nil && !internal.IsNotFound(err) { + return nil, err + } + + var getETag internal.GetETag + if err := resp.DecodeProp(&getETag); err != nil && !internal.IsNotFound(err) { + return nil, err + } + etag, err := strconv.Unquote(getETag.ETag) + if err != nil { + return nil, fmt.Errorf("carddav: failed to unquote ETag: %v", err) + } + + addrs = append(addrs, CalendarObject{ + Path: path, + ModTime: time.Time(getLastMod.LastModified), + ETag: etag, + Data: calData.Data, + }) + } + + return addrs, nil +} + +func (c *Client) QueryCalendar(calendar string, query *CalendarQuery) ([]CalendarObject, error) { + propReq, err := encodeCalendarReq(&query.Comp) + if err != nil { + return nil, err + } + + calendarQuery := calendarQuery{Prop: propReq} + req, err := c.ic.NewXMLRequest("REPORT", calendar, &calendarQuery) + if err != nil { + return nil, err + } + + ms, err := c.ic.DoMultiStatus(req) + if err != nil { + return nil, err + } + + return decodeCalendarObjectList(ms) +} diff --git a/caldav/elements.go b/caldav/elements.go index ab019c2..8f632a3 100644 --- a/caldav/elements.go +++ b/caldav/elements.go @@ -85,7 +85,7 @@ type prop struct { } // Response variant of https://tools.ietf.org/html/rfc4791#section-9.6 -type calendarData struct { +type calendarDataResp struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav calendar-data"` Data []byte `xml:",chardata"` } diff --git a/internal/server.go b/internal/server.go index 9532bac..347aed2 100644 --- a/internal/server.go +++ b/internal/server.go @@ -109,6 +109,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if href != nil { w.Header().Set("Location", (*url.URL)(href).String()) } + // TODO: http.StatusNoContent if the resource already existed w.WriteHeader(http.StatusCreated) } case http.MethodDelete: