1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
|
package main
import (
"pim/lib/cal"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
)
// A Cal is one calendar the server keeps current.
//
// The config is per-user, in ndb's attribute-pair syntax, one calendar
// per line:
//
// cal=work me=you@work.example
// cal=home me=you@home.example,alias@home.example
//
// In iTIP your identity is the mailto: in your own ATTENDEE line -- there
// is no separate field for it -- so a calendar has to be told whose it
// is before anything can reply on its behalf. Aliases are listed because
// you may be invited at one address and send from another.
//
// Fetching is not this server's business. A subscribed calendar is a
// file somebody else wrote; see pim/fetch(1).
//
// It lives in $home/lib/pim by default. The urls of private calendars
// are secrets, which is the other reason they belong in a file rather
// than in argv where ps(1) would show them.
type Cal struct {
Name string
Refresh time.Duration // how often to re-stat, if the config says
File string
Me []string // the addresses that count as you on this calendar
mu sync.Mutex
last time.Time
err string
}
func (c *Cal) status() (time.Time, string) {
c.mu.Lock()
defer c.mu.Unlock()
return c.last, c.err
}
func (c *Cal) note(t time.Time, err error) {
c.mu.Lock()
defer c.mu.Unlock()
c.last = t
if err != nil {
c.err = err.Error()
} else {
c.err = ""
}
}
// readConfig parses the calendar list. Blank lines and lines beginning
// with # are ignored; everything else is a tuple of attr=value pairs.
func readConfig(path, dir string) ([]*Cal, error) {
b, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var cals []*Cal
for n, line := range strings.Split(string(b), "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
c := &Cal{Refresh: 15 * time.Minute}
for _, f := range strings.Fields(line) {
k, v, ok := strings.Cut(f, "=")
if !ok {
return nil, fmt.Errorf("%s:%d: %q is not attr=value", path, n+1, f)
}
v = strings.Trim(v, `"'`)
switch k {
case "cal":
c.Name = v
case "url":
// fetching moved out; the url belongs to the script
// that writes the file
cal.Warnf("%s:%d: url= is ignored, see pim/fetch", path, n+1)
case "me":
for _, a := range strings.Split(v, ",") {
if a = strings.TrimSpace(a); a != "" {
c.Me = append(c.Me, a)
}
}
case "refresh":
d, err := time.ParseDuration(v)
if err != nil || d <= 0 {
return nil, fmt.Errorf("%s:%d: bad refresh %q", path, n+1, v)
}
c.Refresh = d
default:
return nil, fmt.Errorf("%s:%d: unknown attribute %q", path, n+1, k)
}
}
if c.Name == "" {
return nil, fmt.Errorf("%s:%d: no cal= name", path, n+1)
}
c.File = filepath.Join(dir, c.Name+".ics")
cals = append(cals, c)
}
if len(cals) == 0 {
return nil, fmt.Errorf("%s: no calendars", path)
}
return cals, nil
}
// pick selects one calendar by name. A server serves exactly one; the
// config lists them all so that whatever starts them has a single place
// to read.
func pick(cals []*Cal, name string) (*Cal, error) {
if name == "" {
if len(cals) == 1 {
return cals[0], nil
}
var names []string
for _, c := range cals {
names = append(names, c.Name)
}
return nil, fmt.Errorf("which calendar? -N one of: %s",
strings.Join(names, " "))
}
for _, c := range cals {
if c.Name == name {
return c, nil
}
}
return nil, fmt.Errorf("no calendar named %q", name)
}
|