Process calendar directory; parse emails.
2 files changed, 82 insertions(+), 19 deletions(-)

M README.md
M main.go
M README.md +1 -1
@@ 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

          
M main.go +81 -18
@@ 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
+}