diff options
Diffstat (limited to 'pim/cmd/caldavfs/rsvp.go')
| -rw-r--r-- | pim/cmd/caldavfs/rsvp.go | 151 |
1 files changed, 151 insertions, 0 deletions
diff --git a/pim/cmd/caldavfs/rsvp.go b/pim/cmd/caldavfs/rsvp.go new file mode 100644 index 0000000..47d7425 --- /dev/null +++ b/pim/cmd/caldavfs/rsvp.go @@ -0,0 +1,151 @@ +package main + +import ( + "bytes" + "context" + "fmt" + "net/http" + "strings" + "time" + + "github.com/emersion/go-ical" + + "pim/lib/cal" +) + +// RSVP answers an invitation. +// +// Accepting is two writes -- update your own copy, and tell the +// organiser -- and a server advertising calendar-auto-schedule does +// both from this one PUT. Without that we would have to send the iMIP +// ourselves, which is why Caps only claims schedule when the server +// said so. +// +// The etag goes back as If-Match. If somebody else has touched the +// event since we synced, the server refuses and we say so, rather than +// overwriting a change we never saw. +func (b *backend) RSVP(uid string, recurID time.Time, partstat string) error { + b.mu.Lock() + ref, ok := b.obj[uid] + me := b.me + b.mu.Unlock() + if !ok { + return fmt.Errorf("rsvp: no object for %s; try refresh", uid) + } + if len(me) == 0 { + return fmt.Errorf("rsvp: no identity for this calendar") + } + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + + obj, err := b.client.GetCalendarObject(ctx, ref.path) + if err != nil { + return fmt.Errorf("rsvp: fetch: %v", err) + } + if obj.Data == nil { + return fmt.Errorf("rsvp: %s is empty", ref.path) + } + // Match against the version we just read, not the one the last + // report mentioned: those can differ, and the point of If-Match is + // to guard the edit we actually made. + if obj.ETag != "" { + ref.etag = obj.ETag + } + + n := 0 + for _, ev := range obj.Data.Events() { + if id, err := ev.Props.Text(ical.PropUID); err != nil || id != uid { + continue + } + if !recurID.IsZero() { + // answering one occurrence, not the series + got, err := ev.Props.DateTime(ical.PropRecurrenceID, time.Local) + if err != nil || !got.Equal(recurID) { + continue + } + } + n += setPartstat(ev.Props, me, partstat) + } + if n == 0 { + return fmt.Errorf("rsvp: %s does not list any of %s as an attendee", + uid, strings.Join(me, ", ")) + } + + if err := b.put(ref, obj.Data); err != nil { + return err + } + // our copy is stale the moment the server accepts it + if _, err := b.Sync(); err != nil { + cal.Warnf("rsvp: resync: %v", err) + } + return nil +} + +// setPartstat rewrites our own ATTENDEE line and leaves every other one +// alone, which is what iTIP wants in a REPLY. +func setPartstat(props ical.Props, me []string, want string) int { + n := 0 + for _, p := range props.Values(ical.PropAttendee) { + addr := strings.TrimPrefix(p.Value, "mailto:") + cn := p.Params.Get(ical.ParamCommonName) + mine := false + for _, m := range me { + if strings.EqualFold(addr, m) || strings.EqualFold(cn, m) { + mine = true + } + } + if !mine { + continue + } + q := p + q.Params.Set(ical.ParamParticipationStatus, want) + props.Set(&q) + n++ + } + return n +} + +// put writes the object back, refusing to clobber a newer version. +func (b *backend) put(ref objref, c *ical.Calendar) error { + var buf bytes.Buffer + if err := ical.NewEncoder(&buf).Encode(c); err != nil { + return fmt.Errorf("rsvp: encode: %v", err) + } + req, err := http.NewRequest("PUT", urlJoin(b.endpoint, ref.path), bytes.NewReader(buf.Bytes())) + if err != nil { + return err + } + req.Header.Set("Content-Type", "text/calendar; charset=utf-8") + if e := quoteETag(ref.etag); e != "" { + req.Header.Set("If-Match", e) + } + req.SetBasicAuth(b.user, b.pass) + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + switch resp.StatusCode { + case http.StatusOK, http.StatusCreated, http.StatusNoContent: + return nil + case http.StatusPreconditionFailed: + return fmt.Errorf("rsvp: the event changed on the server; refresh and try again") + default: + return fmt.Errorf("rsvp: put: %s", resp.Status) + } +} + +// quoteETag puts back the quotes go-webdav strips when it parses the +// header. If-Match wants an entity-tag, and a bare hex string is not +// one: the server answers 412 and it reads exactly like a lost race. +func quoteETag(s string) string { + if s == "" { + return "" + } + if strings.HasPrefix(s, `"`) || strings.HasPrefix(s, "W/") { + return s + } + return `"` + s + `"` +} |
