summaryrefslogtreecommitdiff
path: root/pim/cmd/caldavfs/rsvp.go
diff options
context:
space:
mode:
Diffstat (limited to 'pim/cmd/caldavfs/rsvp.go')
-rw-r--r--pim/cmd/caldavfs/rsvp.go151
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 + `"`
+}