summaryrefslogtreecommitdiff
path: root/pim/lib
diff options
context:
space:
mode:
Diffstat (limited to 'pim/lib')
-rw-r--r--pim/lib/cal/backend.go23
-rw-r--r--pim/lib/cal/ical.go26
-rw-r--r--pim/lib/cal/tree.go58
3 files changed, 101 insertions, 6 deletions
diff --git a/pim/lib/cal/backend.go b/pim/lib/cal/backend.go
index 6dcacc2..6ed488c 100644
--- a/pim/lib/cal/backend.go
+++ b/pim/lib/cal/backend.go
@@ -39,3 +39,26 @@ type Backend interface {
type Describer interface {
Describe() string
}
+
+// Identity is implemented by a backend that knows whose calendar this
+// is. In iTIP your identity is the mailto: in your own ATTENDEE line --
+// there is no separate field for it -- so without this nothing can tell
+// which of an event's attendees is you. Aliases are listed because you
+// may be invited at one address and reply from another.
+type Identity interface {
+ Me() []string
+}
+
+// RSVPer is implemented by a backend that can answer an invitation.
+//
+// Accepting is two writes: update your own copy, and tell the organiser.
+// A CalDAV server advertising calendar-auto-schedule does both from one
+// PUT. A backend that cannot do both should not implement this at all,
+// so that writing to partstat fails honestly rather than doing half of
+// it and looking like it worked.
+//
+// recurID is the zero time to answer a whole series, or the start of
+// one occurrence to answer only that.
+type RSVPer interface {
+ RSVP(uid string, recurID time.Time, partstat string) error
+}
diff --git a/pim/lib/cal/ical.go b/pim/lib/cal/ical.go
index f52b333..5477030 100644
--- a/pim/lib/cal/ical.go
+++ b/pim/lib/cal/ical.go
@@ -95,29 +95,43 @@ func LoadDir(dir string) ([]*Event, error) {
return nil, err
}
sort.Strings(names)
- var evs []*Event
+ var cals []*ical.Calendar
for _, name := range names {
f, err := os.Open(name)
if err != nil {
Warnf("%s: %v", name, err)
continue
}
- cal, err := ical.NewDecoder(f).Decode()
+ c, err := ical.NewDecoder(f).Decode()
f.Close()
if err != nil {
Warnf("%s: %v", name, err)
continue
}
- for _, c := range cal.Events() {
- ev, err := newEvent(&c)
+ cals = append(cals, c)
+ }
+ return FromCalendars(cals), nil
+}
+
+// FromCalendars turns decoded iCalendar objects into events, with
+// RECURRENCE-ID overrides attached to the series they belong to.
+//
+// A backend that already holds parsed calendars -- CalDAV hands them
+// straight back from a REPORT -- comes through here rather than writing
+// them to disk first.
+func FromCalendars(cals []*ical.Calendar) []*Event {
+ var evs []*Event
+ for _, c := range cals {
+ for _, e := range c.Events() {
+ ev, err := newEvent(&e)
if err != nil {
- Warnf("%s: %v", name, err)
+ Warnf("%v", err)
continue
}
evs = append(evs, ev)
}
}
- return link(evs), nil
+ return link(evs)
}
// link attaches RECURRENCE-ID events to the series they override.
diff --git a/pim/lib/cal/tree.go b/pim/lib/cal/tree.go
index cd9e9d2..f78f2eb 100644
--- a/pim/lib/cal/tree.go
+++ b/pim/lib/cal/tree.go
@@ -112,6 +112,7 @@ func (s *Server) buildEvents(root *fs.StaticDir, evs []*Event) {
s.file(ed, "attendees", attendeeText(e.Attendees))
}
s.file(ed, "uid", e.UID+"\n")
+ s.addPartstat(ed, e)
s.file(ed, "raw", e.Raw)
}
}
@@ -215,3 +216,60 @@ func attendeeText(as []Attendee) string {
}
return b.String()
}
+
+// addPartstat publishes your own reply status for an event.
+//
+// Reading it says where you stand. Writing it answers the invitation --
+// but only if the backend can actually do both halves of that: update
+// the calendar and tell the organiser. A published .ics can do neither,
+// so there the file is read-only and says why.
+func (s *Server) addPartstat(dir *fs.StaticDir, e *Event) {
+ var me []string
+ if id, ok := s.be.(Identity); ok {
+ me = id.Me()
+ }
+ cur := "" // no identity, or not an attendee
+ for _, a := range e.Attendees {
+ for _, m := range me {
+ if strings.EqualFold(a.Email, m) || strings.EqualFold(a.Name, m) {
+ cur = a.Partstat
+ }
+ }
+ }
+ if cur == "" && len(me) == 0 {
+ cur = "unknown"
+ } else if cur == "" {
+ cur = "not-an-attendee"
+ }
+
+ rsvp, canWrite := s.be.(RSVPer)
+ mode := uint32(0444)
+ if canWrite {
+ mode = 0666
+ }
+ st := s.fsys.NewStat("partstat", s.user, s.user, mode)
+ base := fs.NewStaticFile(st, []byte(cur+"\n"))
+ if !canWrite {
+ dir.AddChild(base)
+ return
+ }
+ uid := e.UID
+ dir.AddChild(&fs.WrappedFile{
+ File: base,
+ WriteF: func(fid uint64, off uint64, data []byte) (uint32, error) {
+ want := strings.ToUpper(strings.TrimSpace(string(data)))
+ switch want {
+ case "ACCEPTED", "DECLINED", "TENTATIVE":
+ default:
+ return 0, fmt.Errorf("partstat: want ACCEPTED, DECLINED or TENTATIVE")
+ }
+ if len(me) == 0 {
+ return 0, fmt.Errorf("partstat: no me= for this calendar")
+ }
+ if err := rsvp.RSVP(uid, time.Time{}, want); err != nil {
+ return 0, err
+ }
+ return uint32(len(data)), nil
+ },
+ })
+}