@@ 12,7 12,7 @@ attendees when it detects changes.
- The collection can be either a directory full of calendar events or a WebDAV calendar
- A database of state is maintained to provide resiliance and record invitation state
-- Email responses (accept/decline/reschedule) are understood and update the database
+- Email responses (accept/decline/reschedule) are understood and update the calendar
- Can be run as a service or on-demand
- Emails can be handled as either piped data, or file handles
- Default behavior for reacting to changes configured in config file (email everyone, only changed, no-one) for running as server
@@ 3,11 3,14 @@ package main
import (
"fmt"
"io"
+ "io/fs"
+ "path/filepath"
+ "regexp"
+ "strings"
"github.com/arran4/golang-ical"
"github.com/cosiner/flag"
- //"github.com/jordan-wright/email"
"os"
)
@@ 19,8 22,6 @@ import (
// TODO -D -- debug (verbose output)
// TODO -e -- input is an email
// TODO -d <path> -- database path
-// TODO -c <path> -- calendar path
-// TODO Replace plain text DB with real embedded DB
func main() {
processFlags(os.Args...)
}
@@ 56,21 57,33 @@ func processFlags(args ...string) {
panic(err)
}
}
- cal, err := ics.ParseCalendar(in)
- if err != nil {
+ if err := send.parseSend(in); err != nil {
// TODO handle panic
panic(err)
}
- sendInvites(cal)
return
}
// drop through to operate on calendar
-}
-
-func sendInvites(cal *ics.Calendar) error {
- fmt.Println(cal.Serialize())
- return nil
+ filepath.WalkDir(send.CalPath, func(p string, d fs.DirEntry, err error) error {
+ if err != nil {
+ return nil
+ }
+ if strings.HasPrefix(p, ".") {
+ return nil
+ }
+ if !d.IsDir() && strings.HasSuffix(d.Name(), ".ics") {
+ in, err := os.Open(p)
+ if err != nil {
+ return err
+ }
+ if err = send.parseSend(in); err != nil {
+ return err
+ }
+ }
+ return nil
+ })
+ return
}
/****************************************************************************/
@@ 112,7 125,7 @@ type Send struct {
DbPath string `names:"-d" usage:"the path to the database"`
CalPath string `names:"-c" usage:"the path to the calendar"`
//NoPersist bool `names:"-Z" usage:"do not record actions to database (but maybe send emails)"`
- //EmailInput bool `names:"-e" usage:"input data is an email containing an event"`
+ mailInput bool `names:"-e" usage:"input data is an email containing an RSVP response"`
}
func (t *Send) Metadata() map[string]flag.Flag {
@@ 151,12 164,13 @@ func (t *Send) Metadata() map[string]fla
"-C": {
Desc: "Send emails to only attendees who have been added or removed from an event.",
},
- //"-e": {
- // Desc: `
- // Usefel only with -f; ignored otherwise. The input is expected to be an email with
- // either an ICS in the body, or one or more ICSes as attachments.
- // `,
- //},
+ "-e": {
+ Desc: `
+ Usefel only with -f; ignored otherwise. The input is expected to be an email with
+ an RSVP response either in the body, or as an attachment. The ICS file will be
+ updated with the RSVP response.
+ `,
+ },
"-d": {
Desc: `
The database is a tsv file in the form of:
@@ 173,3 187,52 @@ func (t *Send) Metadata() map[string]fla
},
}
}
+
+func (s Send) parseSend(in io.Reader) error {
+ cal, err := ics.ParseCalendar(in)
+ if err != nil {
+ return err
+ }
+ s.sendInvites(cal)
+ return nil
+}
+
+// TODO optionally send
+func (s Send) sendInvites(cal *ics.Calendar) error {
+ var valid = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
+ for _, e := range cal.Events() {
+ id := e.Id()
+ if len(id) > 37 {
+ id = id[0:17] + "..." + id[len(id)-17:]
+ }
+ if len(e.Attendees()) == 0 {
+ if s.Debug {
+ fmt.Printf("%s has no attendees\n", id)
+ }
+ continue
+ }
+ as := make([]string, 0, len(e.Attendees()))
+ for _, a := range e.Attendees() {
+ email := strings.TrimPrefix(a.Email(), "MAILTO:")
+ if valid.MatchString(email) {
+ as = append(as, email)
+ }
+ }
+ if len(as) == 0 {
+ if s.Debug {
+ fmt.Printf("%s has no valid attendees\n", id)
+ }
+ continue
+ }
+ if s.Debug {
+ fmt.Printf("%s: %s\n", id, strings.Join(as, ", "))
+ }
+ if !s.NoEmail {
+ // TODO actually send
+ }
+ if !s.NOP {
+ // TODO record in DB
+ }
+ }
+ return nil
+}