diff options
Diffstat (limited to 'pim')
| -rw-r--r-- | pim/cmd/caldavfs/discover.go | 130 | ||||
| -rw-r--r-- | pim/cmd/caldavfs/http.go | 14 | ||||
| -rw-r--r-- | pim/cmd/caldavfs/main.go | 239 | ||||
| -rw-r--r-- | pim/cmd/caldavfs/principal.go | 66 | ||||
| -rw-r--r-- | pim/cmd/icalfs/main.go | 10 | ||||
| -rw-r--r-- | pim/go.mod | 1 | ||||
| -rw-r--r-- | pim/go.sum | 4 | ||||
| -rw-r--r-- | pim/lib/cal/backend.go | 23 | ||||
| -rw-r--r-- | pim/lib/cal/ical.go | 26 | ||||
| -rw-r--r-- | pim/lib/cal/tree.go | 58 | ||||
| -rwxr-xr-x | pim/rc/pimup | 71 | ||||
| -rwxr-xr-x | pim/rc/riostart | 20 |
12 files changed, 656 insertions, 6 deletions
diff --git a/pim/cmd/caldavfs/discover.go b/pim/cmd/caldavfs/discover.go new file mode 100644 index 0000000..2b5fb23 --- /dev/null +++ b/pim/cmd/caldavfs/discover.go @@ -0,0 +1,130 @@ +package main + +import ( + "context" + "fmt" + "net/url" + "os" + "strings" + "time" + + "github.com/emersion/go-webdav" + + "github.com/emersion/go-webdav/caldav" + "pim/lib/cal" +) + +// dial builds an authenticated client and finds the calendar to serve. +// +// The walk is the one CalDAV prescribes: the endpoint tells you your +// principal, the principal tells you where your calendars live, and +// that collection lists them. Naming a calendar by its display name +// beats hardcoding a path, which servers are free to change. +func dial(endpoint, user, pass, want string) (*caldav.Client, caldav.Calendar, bool, error) { + var zero caldav.Calendar + + hc := webdav.HTTPClientWithBasicAuth(nil, user, pass) + c, err := caldav.NewClient(hc, endpoint) + if err != nil { + return nil, zero, false, err + } + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + + principal, err := findPrincipal(endpoint, user, pass) + if err != nil { + return nil, zero, false, fmt.Errorf("principal: %v", err) + } + home, err := c.FindCalendarHomeSet(ctx, principal) + if err != nil { + return nil, zero, false, fmt.Errorf("home set: %v", err) + } + cals, err := c.FindCalendars(ctx, home) + if err != nil { + return nil, zero, false, fmt.Errorf("calendars: %v", err) + } + if len(cals) == 0 { + return nil, zero, false, fmt.Errorf("no calendars under %s", home) + } + + sched := autoSchedule(endpoint, user, pass, home) + cal.Warnf("home %s, auto-schedule %v", home, sched) + + if want == "" { + // No choice made: list them and refuse, rather than pick one + // and have it silently be the wrong calendar. + var names []string + for _, cl := range cals { + names = append(names, cl.Name) + } + return nil, zero, sched, fmt.Errorf("which calendar? -C one of: %s", + strings.Join(names, ", ")) + } + for _, cl := range cals { + if cl.Name == want || cl.Path == want { + return c, cl, sched, nil + } + } + return nil, zero, sched, fmt.Errorf("no calendar named %q", want) +} + +// autoSchedule asks whether the server sends iMIP on our behalf. A +// server that does means writing a PARTSTAT both updates our copy and +// tells the organiser; a server that does not means we must send the +// mail ourselves. +func autoSchedule(endpoint, user, pass, path string) bool { + req, err := newRequest("OPTIONS", endpoint, path) + if err != nil { + return false + } + req.SetBasicAuth(user, pass) + resp, err := httpDo(req) + if err != nil { + return false + } + defer resp.Body.Close() + for _, v := range resp.Header.Values("DAV") { + for _, f := range strings.Split(v, ",") { + if strings.TrimSpace(f) == "calendar-auto-schedule" { + return true + } + } + } + return false +} + +// urlJoin resolves a DAV href against the endpoint. +// +// An href from the server is absolute on that server: it replaces the +// endpoint's path rather than extending it. Appending it instead gives +// /dav/dav/calendars/... which answers 404, and a probe that reads a +// 404 as "no" reports a capability the server actually has. +func urlJoin(endpoint, href string) string { + base, err := url.Parse(endpoint) + if err != nil { + return href + } + ref, err := url.Parse(href) + if err != nil { + return href + } + return base.ResolveReference(ref).String() +} + +// readSecret reads a password from a file, so it stays out of argv where +// ps(1) would show it. +func readSecret(path string) (string, error) { + b, err := os.ReadFile(path) + if err != nil { + return "", err + } + for _, line := range strings.Split(string(b), "\n") { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + return strings.Trim(line, `"'`), nil + } + return "", fmt.Errorf("%s: empty", path) +} diff --git a/pim/cmd/caldavfs/http.go b/pim/cmd/caldavfs/http.go new file mode 100644 index 0000000..a49baf8 --- /dev/null +++ b/pim/cmd/caldavfs/http.go @@ -0,0 +1,14 @@ +package main + +import ( + "net/http" + "time" +) + +var client = &http.Client{Timeout: 2 * time.Minute} + +func newRequest(method, endpoint, path string) (*http.Request, error) { + return http.NewRequest(method, urlJoin(endpoint, path), nil) +} + +func httpDo(req *http.Request) (*http.Response, error) { return client.Do(req) } diff --git a/pim/cmd/caldavfs/main.go b/pim/cmd/caldavfs/main.go new file mode 100644 index 0000000..c3306de --- /dev/null +++ b/pim/cmd/caldavfs/main.go @@ -0,0 +1,239 @@ +// caldav/fs serves a CalDAV calendar as a 9p file system. +// +// It is a backend behind pim/lib/cal, which owns the tree, so everything +// above it -- events/date, events/uuid, ctl, query, alarm, changed -- is +// the same as any other calendar. Only fetching differs, and unlike a +// published .ics this one is a conversation: discovery, then a report +// for the window we care about. +// +// A CalDAV server that advertises calendar-auto-schedule (RFC 6638) +// sends the iMIP for you: writing your PARTSTAT back both updates your +// copy and tells the organiser. That is the whole reason this backend +// can honestly offer rsvp where ical/fs cannot. +package main + +import ( + "context" + "flag" + "fmt" + "os" + "strings" + "sync" + "time" + + "github.com/emersion/go-ical" + "github.com/emersion/go-webdav/caldav" + + "pim/lib/cal" +) + +func fatal(format string, a ...interface{}) { + cal.Warnf(format, a...) + os.Exit(1) +} + +type backend struct { + name string + client *caldav.Client + path string // the calendar collection + window time.Duration + refresh time.Duration + sched bool // server does the scheduling for us + + mu sync.Mutex + evs []*cal.Event + sig string + last time.Time + err string +} + +func (b *backend) Name() string { return b.name } + +// autoschedule is reported so a dry run shows it; it decides whether +// writing a PARTSTAT is enough or whether we must send the iMIP too. +func (b *backend) Sched() bool { return b.sched } + +func (b *backend) Caps() string { + // Writing is not implemented yet, so say so rather than promise it. + // When it is: "read write rsvp" plus "schedule" when the server + // advertises calendar-auto-schedule. + return "read" +} + +func (b *backend) Refresh() time.Duration { return b.refresh } + +func (b *backend) Status() (time.Time, string) { + b.mu.Lock() + defer b.mu.Unlock() + return b.last, b.err +} + +func (b *backend) Describe() string { + s := fmt.Sprintf("collection %s\n", b.path) + s += fmt.Sprintf("autoschedule %v\n", b.sched) + return s +} + +// Sync asks for every event in the window and reports whether the set +// changed. ETags make that cheap to decide: if every object still has +// the etag we saw last time, nothing has moved. +func (b *backend) Sync() (bool, error) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + now := time.Now() + q := &caldav.CalendarQuery{ + CompRequest: caldav.CalendarCompRequest{ + Name: "VCALENDAR", + Props: []string{"VERSION"}, + Comps: []caldav.CalendarCompRequest{{ + Name: "VEVENT", + Props: []string{ + "SUMMARY", "UID", "DTSTART", "DTEND", "DURATION", + "RRULE", "RDATE", "EXDATE", "RECURRENCE-ID", + "LOCATION", "DESCRIPTION", "STATUS", "SEQUENCE", + "ORGANIZER", "ATTENDEE", + }, + }}, + }, + CompFilter: caldav.CompFilter{ + Name: "VCALENDAR", + Comps: []caldav.CompFilter{{ + Name: "VEVENT", + Start: now.Add(-b.window), + End: now.Add(b.window), + }}, + }, + } + objs, err := b.client.QueryCalendar(ctx, b.path, q) + b.note(time.Now(), err) + if err != nil { + return false, err + } + + sig := etagsig(objs) + b.mu.Lock() + same := sig == b.sig && b.evs != nil + b.mu.Unlock() + if same { + return false, nil + } + + cals := make([]*ical.Calendar, 0, len(objs)) + for _, o := range objs { + if o.Data != nil { + cals = append(cals, o.Data) + } + } + evs := cal.FromCalendars(cals) + + b.mu.Lock() + b.evs, b.sig = evs, sig + b.mu.Unlock() + cal.Warnf("%s: %d objects", b.name, len(objs)) + return true, nil +} + +func (b *backend) Events() ([]*cal.Event, error) { + b.mu.Lock() + defer b.mu.Unlock() + return b.evs, nil +} + +func (b *backend) note(t time.Time, err error) { + b.mu.Lock() + defer b.mu.Unlock() + b.last = t + if err != nil { + b.err = err.Error() + } else { + b.err = "" + } +} + +// etagsig summarises the collection: path and etag per object. If the +// server changes nothing, this does not change either. +func etagsig(objs []caldav.CalendarObject) string { + s := make([]string, 0, len(objs)) + for _, o := range objs { + s = append(s, o.Path+" "+o.ETag) + } + // QueryCalendar makes no promise about order + sortStrings(s) + return strings.Join(s, "\n") +} + +func sortStrings(s []string) { + for i := 1; i < len(s); i++ { + for j := i; j > 0 && s[j] < s[j-1]; j-- { + s[j], s[j-1] = s[j-1], s[j] + } + } +} + +func main() { + cal.Argv0 = "caldav/fs" + var ( + endpoint = flag.String("e", "", "caldav endpoint, e.g. https://caldav.fastmail.com/dav/") + user = flag.String("u", "", "account name") + pwfile = flag.String("p", "", "file holding the password (keeps it out of ps)") + which = flag.String("C", "", "which calendar to serve, by display name") + name = flag.String("N", "", "name to report in ctl (default: the display name)") + srv = flag.String("s", "", "service name to post in /srv (default caldav.$user.$pid)") + days = flag.Int("w", 400, "expansion window in days") + poll = flag.Duration("r", 15*time.Minute, "how often to re-query") + dry = flag.Bool("n", false, "load and report, do not serve") + ) + flag.Usage = func() { + fmt.Fprintf(os.Stderr, + "usage: caldav/fs -e endpoint -u user -p pwfile [-C calendar] [-N name] [-s srv] [-w days] [-r poll] [-n]\n") + os.Exit(2) + } + flag.Parse() + + if *endpoint == "" || *user == "" || *pwfile == "" { + flag.Usage() + } + pass, err := readSecret(*pwfile) + if err != nil { + fatal("%v", err) + } + + client, coll, sched, err := dial(*endpoint, *user, pass, *which) + if err != nil { + fatal("%v", err) + } + + who := os.Getenv("user") + if who == "" { + who = "glenda" + } + if *srv == "" { + // as rio(1) and plumb(1) name theirs, so several may run at once + *srv = fmt.Sprintf("caldav.%s.%d", who, os.Getpid()) + } + label := *name + if label == "" { + label = coll.Name + } + + be := &backend{ + name: label, client: client, path: coll.Path, sched: sched, + window: time.Duration(*days) * 24 * time.Hour, + refresh: *poll, + } + + s := cal.New(be, cal.Config{ + User: who, Srv: *srv, Conf: *pwfile, + Window: time.Duration(*days) * 24 * time.Hour, + }) + if *dry { + if err := s.Report(); err != nil { + fatal("%v", err) + } + return + } + if err := s.Serve(); err != nil { + fatal("%v", err) + } +} diff --git a/pim/cmd/caldavfs/principal.go b/pim/cmd/caldavfs/principal.go new file mode 100644 index 0000000..b5d6558 --- /dev/null +++ b/pim/cmd/caldavfs/principal.go @@ -0,0 +1,66 @@ +package main + +import ( + "encoding/xml" + "fmt" + "net/http" + "strings" +) + +// findPrincipal asks the endpoint who we are. +// +// go-webdav does this too, but it resolves the request path with +// path.Join, which drops a trailing slash: a client pointed at /dav/ +// ends up asking about /dav, and Cyrus answers 405 for that exact +// spelling while answering 207 for /dav/. Deeper paths tolerate either, +// so only this first request needs doing by hand. +func findPrincipal(endpoint, user, pass string) (string, error) { + body := `<?xml version="1.0" encoding="utf-8"?>` + + `<d:propfind xmlns:d="DAV:"><d:prop><d:current-user-principal/></d:prop></d:propfind>` + + req, err := http.NewRequest("PROPFIND", endpoint, strings.NewReader(body)) + if err != nil { + return "", err + } + req.Header.Set("Depth", "0") + req.Header.Set("Content-Type", "application/xml; charset=utf-8") + req.SetBasicAuth(user, pass) + + resp, err := client.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusMultiStatus { + return "", fmt.Errorf("%s: %s", endpoint, resp.Status) + } + + // Spelled out rather than using encoding/xml's "a>b" path + // shorthand, which does not carry namespaces through. + type href struct { + Href string `xml:"DAV: href"` + } + type prop struct { + Principal href `xml:"DAV: current-user-principal"` + } + type propstat struct { + Prop prop `xml:"DAV: prop"` + } + type response struct { + Propstats []propstat `xml:"DAV: propstat"` + } + var ms struct { + Responses []response `xml:"DAV: response"` + } + if err := xml.NewDecoder(resp.Body).Decode(&ms); err != nil { + return "", err + } + for _, r := range ms.Responses { + for _, ps := range r.Propstats { + if h := strings.TrimSpace(ps.Prop.Principal.Href); h != "" { + return h, nil + } + } + } + return "", fmt.Errorf("%s: no current-user-principal", endpoint) +} diff --git a/pim/cmd/icalfs/main.go b/pim/cmd/icalfs/main.go index c155252..774a9b1 100644 --- a/pim/cmd/icalfs/main.go +++ b/pim/cmd/icalfs/main.go @@ -52,6 +52,16 @@ func (b *backend) Name() string { // A published feed can only be read. See pim/doc/design.md. func (b *backend) Caps() string { return "read" } +// Me is whose calendar this is, from the config. It makes partstat +// readable -- you can see where you stand -- without making it +// writable, because nothing here can deliver a reply. +func (b *backend) Me() []string { + if b.c == nil { + return nil + } + return b.c.Me +} + // Refresh is how often to re-stat the directory. Whoever writes the // files should poke ctl instead; this only catches a hand edit. func (b *backend) Refresh() time.Duration { return b.poll } @@ -11,6 +11,7 @@ require ( github.com/Plan9-Archive/libauth v0.0.0-20180917063427-d1ca9e94969d // indirect github.com/emersion/go-ical v0.0.0-20250609112844-439c63cef608 // indirect github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect + github.com/emersion/go-webdav v0.7.0 // indirect github.com/fhs/mux9p v0.3.1 // indirect github.com/knusbaum/go9p v1.18.0 // indirect github.com/teambition/rrule-go v1.8.2 // indirect @@ -3,10 +3,14 @@ github.com/Plan9-Archive/libauth v0.0.0-20180917063427-d1ca9e94969d h1:xH/U6K+HYxh1480TkQYRqRO8F2RJsg+R6wFiVJzdldg= github.com/Plan9-Archive/libauth v0.0.0-20180917063427-d1ca9e94969d/go.mod h1:UKp8dv9aeaZoQFWin7eQXtz89iHly1YAFZNn3MCutmQ= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emersion/go-ical v0.0.0-20240127095438-fc1c9d8fb2b6/go.mod h1:BEksegNspIkjCQfmzWgsgbu6KdeJ/4LwUZs7DMBzjzw= github.com/emersion/go-ical v0.0.0-20250609112844-439c63cef608 h1:5XWaET4YAcppq3l1/Yh2ay5VmQjUdq6qhJuucdGbmOY= github.com/emersion/go-ical v0.0.0-20250609112844-439c63cef608/go.mod h1:BEksegNspIkjCQfmzWgsgbu6KdeJ/4LwUZs7DMBzjzw= github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ= github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= +github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM= +github.com/emersion/go-webdav v0.7.0 h1:cp6aBWXBf8Sjzguka9VJarr4XTkGc2IHxXI1Gq3TKpA= +github.com/emersion/go-webdav v0.7.0/go.mod h1:mI8iBx3RAODwX7PJJ7qzsKAKs/vY429YfS2/9wKnDbQ= github.com/fhs/mux9p v0.3.1 h1:x1UswUWZoA9vrA02jfisndCq3xQm+wrQUxUt5N99E08= github.com/fhs/mux9p v0.3.1/go.mod h1:F4hwdenmit0WDoNVT2VMWlLJrBVCp/8UhzJa7scfjEQ= github.com/hanwen/go-fuse v1.0.0/go.mod h1:unqXarDXqzAk0rt98O2tVndEPIpUgLD9+rwFisZH3Ok= 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 + }, + }) +} diff --git a/pim/rc/pimup b/pim/rc/pimup new file mode 100755 index 0000000..1d7b2de --- /dev/null +++ b/pim/rc/pimup @@ -0,0 +1,71 @@ +#!/bin/rc +# pimup -- bring up mail and the calendars, and mount them here. +# +# Safe to run twice: anything already posted in /srv is reused rather +# than started again. Run it from riostart so every window inherits the +# mounts, since a mount only exists in the namespace that made it. +rfork e + +cald=/usr/glenda/lib/cal +log=/tmp/pimup.log +>$log + +# Wait for a server to announce itself and echo the name it posted. +# rc has no return, so this loops on a condition rather than bailing out. +fn waitsrv { + s=() + i=0 + while(~ $#s 0 && test $i -lt 40){ + s=`{sed -n 's|.*serving /srv/||p' $1} + if(~ $#s 0){ + sleep 1 + i=`{echo $i + 1 | bc} + } + } + echo $"s +} + +# ---- mail +if(! test -e /srv/upasfs.$user) + upas/fs -s -f /imaps/imap.fastmail.com/calvin@pobox.com \ + </dev/null >/dev/null >>[2]$log & +sleep 2 +if(test -e /srv/upasfs.$user) + mount /srv/upasfs.$user /mail/fs >>[2]$log + +# ---- mount points for the calendars +if(! test -e /srv/pimroot) + mntgen -s pimroot /mnt/pim </dev/null >/dev/null >[2]/dev/null & +sleep 1 +mount /srv/pimroot /mnt/pim >[2]/dev/null +ls -d /mnt/pim/calendars >/dev/null >[2]/dev/null +if(! test -e /srv/pimcals) + mntgen -s pimcals /mnt/pim/calendars </dev/null >/dev/null >[2]/dev/null & +sleep 1 +mount /srv/pimcals /mnt/pim/calendars >[2]/dev/null + +# ---- calendars. Go's getpid() on plan9 is a thread id, so the name rc +# knows as $apid is not the one in /srv: read it out of the log. +fn cal { + name=$1 + l=$2 + shift + shift + if(! test -f /mnt/pim/calendars/$name/ctl){ + >$l + $* >[2]$l </dev/null >/dev/null & + s=`{waitsrv $l} + if(~ $#s 0) + echo 'pimup: '^$name^' did not start, see '^$l >>$log + if(! ~ $#s 0) + mount /srv/^$"s /mnt/pim/calendars/$name >>[2]$log + } +} + +cal work /tmp/work.log ical/fs -N work -d $cald +cal local /tmp/local.log ical/fs -N local -d /tmp/callocal +if(test -f $cald/fastmail.pw) + cal fastmail /tmp/dav.log caldav/fs -e https://caldav.fastmail.com/dav/ \ + -u calvin@pobox.com -p $cald/fastmail.pw -C Calendar -N fastmail + +exit 0 diff --git a/pim/rc/riostart b/pim/rc/riostart new file mode 100755 index 0000000..7384503 --- /dev/null +++ b/pim/rc/riostart @@ -0,0 +1,20 @@ +#!/bin/rc +# riostart -- what rio brings up. +# +# rio does not run this by itself: it is the argument to -i. +# rio -i riostart +rfork e + +# servers and mounts first, so every window below inherits them +pimup + +window 0,0,161,117 stats -lmisce +window bar + +# a shell on the serial console, when there is one +~ $#console 0 || window -scroll console + +window -r 170 0 900 640 acme +window -r 170 650 1000 1000 cal9 + +window -miny 130 |
