package calendar import ( "bytes" "fmt" "io" "net/mail" "regexp" "strings" "time" ics "github.com/arran4/golang-ical" ) type Reply struct { MimeType string Params map[string]string CalendarText io.ReadWriter PlainText io.ReadWriter Organizers []string } func (cr *Reply) AddOrganizer(o string) { cr.Organizers = append(cr.Organizers, o) } // CreateReply parses a ics request and return a ics reply (RFC 2446, Section 3.2.3) func CreateReply(reader io.Reader, from *mail.Address, partstat string) (*Reply, error) { cr := Reply{ MimeType: "text/calendar", Params: map[string]string{ "charset": "UTF-8", "method": "REPLY", }, CalendarText: &bytes.Buffer{}, PlainText: &bytes.Buffer{}, } var ( status ics.ParticipationStatus action string ) switch partstat { case "accept": status = ics.ParticipationStatusAccepted action = "accepted" case "accept-tentative": status = ics.ParticipationStatusTentative action = "tentatively accepted" case "decline": status = ics.ParticipationStatusDeclined action = "declined" default: return nil, fmt.Errorf("participation status %s is not implemented", partstat) } name := from.Name if name == "" { name = from.Address } fmt.Fprintf(cr.PlainText, "%s has %s this invitation.", name, action) invite, err := parse(reader) if err != nil { return nil, err } if ok := invite.request(); !ok { return nil, fmt.Errorf("no reply is requested") } // update invite as a reply reply := invite reply.SetMethod(ics.MethodReply) reply.SetProductId("aerc") // check all events for _, vevent := range reply.Events() { e := event{vevent} // check if we should answer if err := e.isReplyRequested(from.Address); err != nil { return nil, err } // make sure we send our reply to the meeting organizer if organizer := e.GetProperty(ics.ComponentPropertyOrganizer); organizer != nil { cr.AddOrganizer(organizer.Value) } // update attendee participation status e.updateAttendees(status, from.Address) // update timestamp e.SetDtStampTime(time.Now()) // remove any subcomponents of event e.Components = nil } // keep only timezone and event components reply.clean() if len(reply.Events()) == 0 { return nil, fmt.Errorf("no events to respond to") } if err := reply.SerializeTo(cr.CalendarText); err != nil { return nil, err } return &cr, nil } type calendar struct { *ics.Calendar } func parse(reader io.Reader) (*calendar, error) { // fix capitalized mailto for parsing of ics file var sb strings.Builder io.Copy(&sb, reader) re := regexp.MustCompile("MAILTO:(.+@)") str := re.ReplaceAllString(sb.String(), "mailto:${1}") // parse calendar invite, err := ics.ParseCalendar(strings.NewReader(str)) if err != nil { return nil, err } return &calendar{invite}, nil } func (cal *calendar) request() (ok bool) { ok = false for i := range cal.CalendarProperties { if cal.CalendarProperties[i].IANAToken == string(ics.PropertyMethod) { if cal.CalendarProperties[i].Value == string(ics.MethodRequest) { ok = true return } } } return } func (cal *calendar) clean() { var clean []ics.Component for _, comp := range cal.Components { switch comp.(type) { case *ics.VTimezone, *ics.VEvent: clean = append(clean, comp) default: continue } } cal.Components = clean } type event struct { *ics.VEvent } func (e *event) isReplyRequested(from string) error { var present bool = false var rsvp bool = false for _, a := range e.Attendees() { if a.Email() == from { present = true if r, ok := a.ICalParameters[string(ics.ParameterRsvp)]; ok { if len(r) > 0 && strings.ToLower(r[0]) == "true" { rsvp = true } } } } if !present { return fmt.Errorf("we are not invited") } if !rsvp { return fmt.Errorf("we don't have to rsvp") } return nil } func (e *event) updateAttendees(status ics.ParticipationStatus, from string) { var clean []ics.IANAProperty for _, prop := range e.Properties { if prop.IANAToken == string(ics.ComponentPropertyAttendee) { att := ics.Attendee{IANAProperty: prop} if att.Email() != from { continue } prop.ICalParameters[string(ics.ParameterParticipationStatus)] = []string{string(status)} delete(prop.ICalParameters, string(ics.ParameterRsvp)) } clean = append(clean, prop) } e.Properties = clean }