From d7191461eb1271bcd83c900bbbbd15b3c4f70de3 Mon Sep 17 00:00:00 2001 From: Cyrille Nofficial Date: Mon, 18 Apr 2022 12:47:47 +0200 Subject: [PATCH] feat: implement caldav search --- calendar/calendar.go | 98 +++- calendar/calendar_test.go | 132 +++++- cmd/domogeek/domogeek.go | 44 +- go.mod | 1 + go.sum | 5 + vendor/github.com/dolanor/caldav-go/LICENSE | 22 + .../dolanor/caldav-go/caldav/client.go | 191 ++++++++ .../caldav/entities/calendar_data.go | 51 ++ .../caldav/entities/calendar_query.go | 59 +++ .../caldav-go/caldav/entities/filter.go | 62 +++ .../caldav-go/caldav/entities/multistatus.go | 23 + .../dolanor/caldav-go/caldav/entities/prop.go | 23 + .../dolanor/caldav-go/caldav/request.go | 56 +++ .../dolanor/caldav-go/caldav/response.go | 40 ++ .../dolanor/caldav-go/caldav/server.go | 30 ++ .../caldav-go/caldav/values/component_name.go | 8 + .../caldav-go/caldav/values/datetime.go | 31 ++ .../caldav-go/caldav/values/human_boolean.go | 8 + .../caldav-go/caldav/values/text_collation.go | 8 + .../dolanor/caldav-go/http/client.go | 53 +++ .../dolanor/caldav-go/http/request.go | 39 ++ .../dolanor/caldav-go/http/response.go | 18 + .../dolanor/caldav-go/http/server.go | 48 ++ .../icalendar/components/calendar.go | 87 ++++ .../caldav-go/icalendar/components/event.go | 172 +++++++ .../icalendar/components/timezone.go | 40 ++ .../dolanor/caldav-go/icalendar/marshal.go | 196 ++++++++ .../icalendar/properties/interfaces.go | 29 ++ .../caldav-go/icalendar/properties/names.go | 31 ++ .../icalendar/properties/property.go | 177 +++++++ .../dolanor/caldav-go/icalendar/reflect.go | 78 ++++ .../dolanor/caldav-go/icalendar/unmarshal.go | 365 +++++++++++++++ .../caldav-go/icalendar/values/cal_scale.go | 7 + .../caldav-go/icalendar/values/comment.go | 33 ++ .../caldav-go/icalendar/values/contact.go | 139 ++++++ .../dolanor/caldav-go/icalendar/values/csv.go | 24 + .../caldav-go/icalendar/values/datetime.go | 265 +++++++++++ .../caldav-go/icalendar/values/duration.go | 132 ++++++ .../values/event_access_classification.go | 17 + .../icalendar/values/event_status.go | 13 + .../dolanor/caldav-go/icalendar/values/geo.go | 69 +++ .../caldav-go/icalendar/values/location.go | 78 ++++ .../caldav-go/icalendar/values/method.go | 7 + .../icalendar/values/recurrence_rule.go | 434 ++++++++++++++++++ .../icalendar/values/time_transparency.go | 12 + .../dolanor/caldav-go/icalendar/values/url.go | 49 ++ .../dolanor/caldav-go/utils/error.go | 37 ++ .../dolanor/caldav-go/webdav/client.go | 119 +++++ .../dolanor/caldav-go/webdav/depth.go | 9 + .../caldav-go/webdav/entities/error.go | 18 + .../caldav-go/webdav/entities/multistatus.go | 23 + .../dolanor/caldav-go/webdav/entities/prop.go | 32 ++ .../caldav-go/webdav/entities/propfind.go | 20 + .../dolanor/caldav-go/webdav/request.go | 56 +++ .../dolanor/caldav-go/webdav/response.go | 54 +++ .../dolanor/caldav-go/webdav/server.go | 28 ++ vendor/modules.txt | 13 + 57 files changed, 3893 insertions(+), 20 deletions(-) create mode 100644 vendor/github.com/dolanor/caldav-go/LICENSE create mode 100644 vendor/github.com/dolanor/caldav-go/caldav/client.go create mode 100644 vendor/github.com/dolanor/caldav-go/caldav/entities/calendar_data.go create mode 100644 vendor/github.com/dolanor/caldav-go/caldav/entities/calendar_query.go create mode 100644 vendor/github.com/dolanor/caldav-go/caldav/entities/filter.go create mode 100644 vendor/github.com/dolanor/caldav-go/caldav/entities/multistatus.go create mode 100644 vendor/github.com/dolanor/caldav-go/caldav/entities/prop.go create mode 100644 vendor/github.com/dolanor/caldav-go/caldav/request.go create mode 100644 vendor/github.com/dolanor/caldav-go/caldav/response.go create mode 100644 vendor/github.com/dolanor/caldav-go/caldav/server.go create mode 100644 vendor/github.com/dolanor/caldav-go/caldav/values/component_name.go create mode 100644 vendor/github.com/dolanor/caldav-go/caldav/values/datetime.go create mode 100644 vendor/github.com/dolanor/caldav-go/caldav/values/human_boolean.go create mode 100644 vendor/github.com/dolanor/caldav-go/caldav/values/text_collation.go create mode 100644 vendor/github.com/dolanor/caldav-go/http/client.go create mode 100644 vendor/github.com/dolanor/caldav-go/http/request.go create mode 100644 vendor/github.com/dolanor/caldav-go/http/response.go create mode 100644 vendor/github.com/dolanor/caldav-go/http/server.go create mode 100644 vendor/github.com/dolanor/caldav-go/icalendar/components/calendar.go create mode 100644 vendor/github.com/dolanor/caldav-go/icalendar/components/event.go create mode 100644 vendor/github.com/dolanor/caldav-go/icalendar/components/timezone.go create mode 100644 vendor/github.com/dolanor/caldav-go/icalendar/marshal.go create mode 100644 vendor/github.com/dolanor/caldav-go/icalendar/properties/interfaces.go create mode 100644 vendor/github.com/dolanor/caldav-go/icalendar/properties/names.go create mode 100644 vendor/github.com/dolanor/caldav-go/icalendar/properties/property.go create mode 100644 vendor/github.com/dolanor/caldav-go/icalendar/reflect.go create mode 100644 vendor/github.com/dolanor/caldav-go/icalendar/unmarshal.go create mode 100644 vendor/github.com/dolanor/caldav-go/icalendar/values/cal_scale.go create mode 100644 vendor/github.com/dolanor/caldav-go/icalendar/values/comment.go create mode 100644 vendor/github.com/dolanor/caldav-go/icalendar/values/contact.go create mode 100644 vendor/github.com/dolanor/caldav-go/icalendar/values/csv.go create mode 100644 vendor/github.com/dolanor/caldav-go/icalendar/values/datetime.go create mode 100644 vendor/github.com/dolanor/caldav-go/icalendar/values/duration.go create mode 100644 vendor/github.com/dolanor/caldav-go/icalendar/values/event_access_classification.go create mode 100644 vendor/github.com/dolanor/caldav-go/icalendar/values/event_status.go create mode 100644 vendor/github.com/dolanor/caldav-go/icalendar/values/geo.go create mode 100644 vendor/github.com/dolanor/caldav-go/icalendar/values/location.go create mode 100644 vendor/github.com/dolanor/caldav-go/icalendar/values/method.go create mode 100644 vendor/github.com/dolanor/caldav-go/icalendar/values/recurrence_rule.go create mode 100644 vendor/github.com/dolanor/caldav-go/icalendar/values/time_transparency.go create mode 100644 vendor/github.com/dolanor/caldav-go/icalendar/values/url.go create mode 100644 vendor/github.com/dolanor/caldav-go/utils/error.go create mode 100644 vendor/github.com/dolanor/caldav-go/webdav/client.go create mode 100644 vendor/github.com/dolanor/caldav-go/webdav/depth.go create mode 100644 vendor/github.com/dolanor/caldav-go/webdav/entities/error.go create mode 100644 vendor/github.com/dolanor/caldav-go/webdav/entities/multistatus.go create mode 100644 vendor/github.com/dolanor/caldav-go/webdav/entities/prop.go create mode 100644 vendor/github.com/dolanor/caldav-go/webdav/entities/propfind.go create mode 100644 vendor/github.com/dolanor/caldav-go/webdav/request.go create mode 100644 vendor/github.com/dolanor/caldav-go/webdav/response.go create mode 100644 vendor/github.com/dolanor/caldav-go/webdav/server.go diff --git a/calendar/calendar.go b/calendar/calendar.go index 5ffab2d..c6b5bf0 100644 --- a/calendar/calendar.go +++ b/calendar/calendar.go @@ -1,12 +1,73 @@ package calendar import ( + "fmt" + "github.com/dolanor/caldav-go/caldav" + "github.com/dolanor/caldav-go/caldav/entities" + "github.com/dolanor/caldav-go/icalendar/components" + "log" "math" + "net/http" + "strings" "time" ) +type Caldav interface { + QueryEvents(path string, query *entities.CalendarQuery) (events []*components.Event, oerr error) +} + type Calendar struct { - Location *time.Location + Location *time.Location + cdav Caldav + caldavPath string + caldavSummaryPattern string +} + +func NewCaldav(caldavUrl, caldavPath string) (Caldav, error) { + // create a reference to your CalDAV-compliant server + server, _ := caldav.NewServer(caldavUrl) + // create a CalDAV client to speak to the server + var client = caldav.NewClient(server, http.DefaultClient) + // start executing requests! + err := client.ValidateServer(caldavPath) + if err != nil { + return nil, fmt.Errorf("bad caldav configuration, unable to validate connexion: %w", err) + } + return client, nil +} + +type Option func(calendar *Calendar) + +func WithCaldav(cdav Caldav) Option { + return func(calendar *Calendar) { + calendar.cdav = cdav + } +} + +func WithCaldavSummaryPattern(caldavSummaryPattern string) Option { + return func(calendar *Calendar) { + calendar.caldavSummaryPattern = caldavSummaryPattern + } +} + +func WithCaldavPath(caldavPath string) Option { + return func(calendar *Calendar) { + calendar.caldavPath = caldavPath + } +} + +func New(location *time.Location, opts ...Option) *Calendar { + c := &Calendar{ + location, + nil, + "", + "", + } + + for _, opt := range opts { + opt(c) + } + return c } func (cal *Calendar) GetEasterDay(year int) time.Time { @@ -72,20 +133,24 @@ func (cal *Calendar) GetHolidays(year int) *[]time.Time { return &joursFeries } -func (cal *Calendar) GetHolidaysSet(year int) *map[time.Time]bool { +func (cal *Calendar) GetHolidaysSet(year int) map[time.Time]bool { holidays := cal.GetHolidays(year) result := make(map[time.Time]bool, len(*holidays)) for _, h := range *holidays { result[h] = true } - return &result + return result } -func(cal *Calendar) IsHoliday(date time.Time) bool{ +func (cal *Calendar) IsHoliday(date time.Time) bool { h := cal.GetHolidaysSet(date.Year()) d := date.In(cal.Location) day := time.Date(d.Year(), d.Month(), d.Day(), 0, 0, 0, 0, cal.Location) - return (*h)[day] + caldavHolidays, err := cal.IsHolidaysFromCaldav(day) + if err != nil { + log.Printf("unable to check holidays from caldav: %v", err) + } + return h[day] || caldavHolidays } func (cal *Calendar) IsWorkingDay(date time.Time) bool { @@ -96,6 +161,27 @@ func (cal *Calendar) IsWorkingDayToday() bool { return cal.IsWorkingDay(time.Now()) } -func (cal *Calendar) IsWeekDay(day time.Time) bool{ +func (cal *Calendar) IsWeekDay(day time.Time) bool { return day.Weekday() >= time.Monday && day.Weekday() <= time.Friday } + +func (cal *Calendar) IsHolidaysFromCaldav(day time.Time) (bool, error) { + if cal.cdav == nil { + return false, nil + } + query, err := entities.NewEventRangeQuery(day.UTC(), day.UTC().Add(23*time.Hour+59*time.Minute)) + if err != nil { + return false, fmt.Errorf("unable to build events range query: %v", err) + } + events, err := cal.cdav.QueryEvents(cal.caldavPath, query) + if err != nil { + return false, fmt.Errorf("unable list events from caldav: %v", err) + } + + for _, evt := range events { + if strings.Contains(evt.Summary, cal.caldavSummaryPattern) { + return true, nil + } + } + return false, nil +} diff --git a/calendar/calendar_test.go b/calendar/calendar_test.go index 1d143c8..f5f871b 100644 --- a/calendar/calendar_test.go +++ b/calendar/calendar_test.go @@ -1,6 +1,9 @@ package calendar import ( + "github.com/dolanor/caldav-go/caldav/entities" + "github.com/dolanor/caldav-go/icalendar/components" + "github.com/dolanor/caldav-go/icalendar/values" "testing" "time" ) @@ -48,7 +51,7 @@ func TestCalendar_GetHolidays(t *testing.T) { time.Date(2020, time.December, 25, 0, 0, 0, 0, loc): true, } - c := Calendar{loc} + c := New(loc) holidays := c.GetHolidays(2020) if len(*holidays) != len(expectedHolidays) { t.Errorf("bad number of holidays, %d but %d are expected", len(*holidays), len(expectedHolidays)) @@ -80,13 +83,13 @@ func TestCalendar_GetHolidaysSet(t *testing.T) { time.Date(2020, time.December, 25, 0, 0, 0, 0, loc), } - c := Calendar{loc} + c := New(loc) holidays := c.GetHolidaysSet(2020) - if len(*holidays) != len(expectedHolidays) { - t.Errorf("bad number of holidays, %d but %d are expected", len(*holidays), len(expectedHolidays)) + if len(holidays) != len(expectedHolidays) { + t.Errorf("bad number of holidays, %d but %d are expected", len(holidays), len(expectedHolidays)) } for _, h := range expectedHolidays { - if !(*holidays)[h] { + if !(holidays)[h] { t.Errorf("%v is not a holiday", h) } } @@ -112,10 +115,10 @@ func TestCalendar_IsHolidays(t *testing.T) { time.Date(2020, time.December, 25, 0, 0, 0, 0, loc), } - c := Calendar{loc} + c := New(loc) holidays := c.GetHolidaysSet(2020) - if len(*holidays) != len(expectedHolidays) { - t.Errorf("bad number of holidays, %d but %d are expected", len(*holidays), len(expectedHolidays)) + if len(holidays) != len(expectedHolidays) { + t.Errorf("bad number of holidays, %d but %d are expected", len(holidays), len(expectedHolidays)) } for _, h := range expectedHolidays { if !c.IsHoliday(h) { @@ -133,7 +136,7 @@ func TestCalendar_IsWorkingDay(t *testing.T) { t.Errorf("unable to load time location: %v", err) t.Fail() } - c := Calendar{loc} + c := New(loc) if c.IsWorkingDay(time.Date(2019, time.January, 01, 0, 0, 0, 0, loc)) { t.Error("1st january is not a working day") @@ -164,3 +167,114 @@ func TestCalendar_IsWorkingDay(t *testing.T) { t.Error("Sunday should not be a working day") } } + +type MockCaldav struct { + events []*components.Event +} + +func (m *MockCaldav) QueryEvents(_ string, _ *entities.CalendarQuery) ([]*components.Event, error) { + return m.events, nil +} + +func TestCalendar_IsHolidaysFromCaldav(t *testing.T) { + loc, err := time.LoadLocation("Europe/Paris") + if err != nil { + t.Errorf("unable to load time location: %v", err) + t.Fail() + } + type fields struct { + Location *time.Location + cdav *MockCaldav + caldavPath string + caldavSummaryPattern string + } + type args struct { + day time.Time + } + tests := []struct { + name string + fields fields + args args + want bool + wantErr bool + }{ + { + name: "Holidays in events", + fields: fields{ + Location: loc, + cdav: &MockCaldav{ + events: []*components.Event{ + { + UID: "1", + DateStart: values.NewDateTime(time.Date(2022, time.April, 16, 0, 0, 0, 0, loc)), + DateEnd: values.NewDateTime(time.Date(2022, time.April, 17, 0, 0, 0, 0, loc)), + Summary: "Holidays", + }, + }, + }, + caldavPath: "my_calendar/", + caldavSummaryPattern: "Holidays", + }, + args: args{ + day: time.Date(2022, time.April, 16, 0, 0, 0, 0, loc), + }, + want: true, + wantErr: false, + }, + { + name: "Not Holidays in events", + fields: fields{ + Location: loc, + cdav: &MockCaldav{ + events: []*components.Event{ + { + UID: "1", + DateStart: values.NewDateTime(time.Date(2022, time.April, 16, 0, 0, 0, 0, loc)), + DateEnd: values.NewDateTime(time.Date(2022, time.April, 17, 0, 0, 0, 0, loc)), + Summary: "Another event", + }, + }, + }, + caldavPath: "my_calendar/", + caldavSummaryPattern: "Holidays", + }, + args: args{ + day: time.Date(2022, time.April, 16, 0, 0, 0, 0, loc), + }, + want: false, + wantErr: false, + }, + { + name: "No events", + fields: fields{ + Location: loc, + cdav: &MockCaldav{}, + caldavPath: "my_calendar/", + caldavSummaryPattern: "Holidays", + }, + args: args{ + day: time.Date(2022, time.April, 15, 0, 0, 0, 0, loc), + }, + want: false, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cal := New( + loc, + WithCaldav(tt.fields.cdav), + WithCaldavPath(tt.fields.caldavPath), + WithCaldavSummaryPattern(tt.fields.caldavSummaryPattern), + ) + got, err := cal.IsHolidaysFromCaldav(tt.args.day) + if (err != nil) != tt.wantErr { + t.Errorf("IsHolidaysFromCaldav() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("IsHolidaysFromCaldav() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/cmd/domogeek/domogeek.go b/cmd/domogeek/domogeek.go index 4260b91..0361c6b 100644 --- a/cmd/domogeek/domogeek.go +++ b/cmd/domogeek/domogeek.go @@ -6,6 +6,9 @@ import ( "encoding/json" "flag" "fmt" + "os" + "os/signal" + "syscall" "github.com/hellofresh/health-go/v4" "github.com/prometheus/client_golang/prometheus" @@ -17,7 +20,8 @@ import ( ) var ( - cal calendar.Calendar + cal *calendar.Calendar + location *time.Location calCounter *prometheus.CounterVec calSummary *prometheus.SummaryVec calHistogram *prometheus.HistogramVec @@ -28,7 +32,7 @@ func init() { if err != nil { log.Fatalf("unable to load time location: %v", err) } - cal = calendar.Calendar{Location: loc} + location = loc calCounter = promauto.NewCounterVec(prometheus.CounterOpts{ Namespace: "domogeek", @@ -93,11 +97,25 @@ func (c *CalendarHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func main() { var port int var host string + var caldavUrl, caldavPath, caldavSummaryPattern string flag.StringVar(&host, "host", "", "host to listen, default all addresses") flag.IntVar(&port, "port", 8080, "port to listen") + flag.StringVar(&caldavUrl, "caldav-url", "", "caldav url to use to read holidays events") + flag.StringVar(&caldavPath, "caldav-path", "", "caldav path to use to read holidays events") + flag.StringVar(&caldavSummaryPattern, "caldav-summary-pattern", "Holidays", "Summary pattern that matches holidays event") flag.Parse() + cdav, err := calendar.NewCaldav(caldavUrl, caldavPath) + if err != nil { + log.Fatalf("unable to init caldav instance") + } + cal = calendar.New(location, + calendar.WithCaldav(cdav), + calendar.WithCaldavPath(caldavPath), + calendar.WithCaldavSummaryPattern(caldavSummaryPattern), + ) + addr := fmt.Sprintf("%s:%d", host, port) log.Printf("start server on %s", addr) @@ -117,9 +135,25 @@ func main() { Check: func(ctx context.Context) error { return nil }, - }, - )) + }), + health.WithChecks(health.Config{ + Name: "caldav", + Timeout: 5 * time.Second, + SkipOnErr: false, + Check: func(ctx context.Context) error { + _, err := cal.IsHolidaysFromCaldav(time.Now()) + return err + }, + }), + ) http.Handle("/status", healthz.Handler()) - log.Fatal(http.ListenAndServe(addr, nil)) + signChan := make(chan os.Signal, 1) + go func() { + log.Fatal(http.ListenAndServe(addr, nil)) + }() + + signal.Notify(signChan, syscall.SIGTERM) + <-signChan + log.Printf("exit on sigterm") } diff --git a/go.mod b/go.mod index 8900013..bb6493c 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module domogeek go 1.18 require ( + github.com/dolanor/caldav-go v0.2.1 github.com/hellofresh/health-go/v4 v4.5.0 github.com/prometheus/client_golang v1.12.1 ) diff --git a/go.sum b/go.sum index be7054d..a5cdee5 100644 --- a/go.sum +++ b/go.sum @@ -62,6 +62,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dolanor/caldav-go v0.2.1 h1:wbARF+WKIryMOApYdX6CnFKY25Kz3ChPerfFc/R3Txk= +github.com/dolanor/caldav-go v0.2.1/go.mod h1:0A9uEq2TN7U1eNh11hHAZ0FA7jyuFSh5hXeaoFPC0fc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -226,9 +228,11 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= @@ -645,6 +649,7 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= diff --git a/vendor/github.com/dolanor/caldav-go/LICENSE b/vendor/github.com/dolanor/caldav-go/LICENSE new file mode 100644 index 0000000..20efd1b --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/vendor/github.com/dolanor/caldav-go/caldav/client.go b/vendor/github.com/dolanor/caldav-go/caldav/client.go new file mode 100644 index 0000000..54804ef --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/caldav/client.go @@ -0,0 +1,191 @@ +package caldav + +import ( + "fmt" + "log" + "net/http" + "strings" + + cent "github.com/dolanor/caldav-go/caldav/entities" + "github.com/dolanor/caldav-go/icalendar/components" + "github.com/dolanor/caldav-go/utils" + "github.com/dolanor/caldav-go/webdav" + "github.com/dolanor/caldav-go/webdav/entities" +) + +var _ = log.Print + +// a client for making WebDAV requests +type Client webdav.Client + +// downcasts the client to the WebDAV interface +func (c *Client) WebDAV() *webdav.Client { + return (*webdav.Client)(c) +} + +// returns the embedded CalDAV server reference +func (c *Client) Server() *Server { + return (*Server)(c.WebDAV().Server()) +} + +// fetches a list of CalDAV features supported by the server +// returns an error if the server does not support DAV +func (c *Client) Features(path string) ([]string, error) { + var cfeatures []string + if features, err := c.WebDAV().Features(path); err != nil { + return cfeatures, utils.NewError(c.Features, "unable to detect features", c, err) + } else { + for _, feature := range features { + if strings.HasPrefix(feature, "calendar-") { + cfeatures = append(cfeatures, feature) + } + } + return cfeatures, nil + } +} + +// fetches a list of CalDAV features and checks if a certain one is supported by the server +// returns an error if the server does not support DAV +func (c *Client) SupportsFeature(name string, path string) (bool, error) { + if features, err := c.Features(path); err != nil { + return false, utils.NewError(c.SupportsFeature, "feature detection failed", c, err) + } else { + var test = fmt.Sprintf("calendar-%s", name) + for _, feature := range features { + if feature == test { + return true, nil + } + } + return false, nil + } +} + +// fetches a list of CalDAV features and checks if a certain one is supported by the server +// returns an error if the server does not support DAV +func (c *Client) ValidateServer(path string) error { + if found, err := c.SupportsFeature("access", path); err != nil { + return utils.NewError(c.SupportsFeature, "feature detection failed", c, err) + } else if !found { + return utils.NewError(c.SupportsFeature, "calendar access feature missing", c, nil) + } else { + return nil + } +} + +// creates a new calendar collection on a given path +func (c *Client) MakeCalendar(path string) error { + if req, err := c.Server().NewRequest("MKCALENDAR", path); err != nil { + return utils.NewError(c.MakeCalendar, "unable to create request", c, err) + } else if resp, err := c.Do(req); err != nil { + return utils.NewError(c.MakeCalendar, "unable to execute request", c, err) + } else if resp.StatusCode != http.StatusCreated { + err := new(entities.Error) + resp.Decode(err) + msg := fmt.Sprintf("unexpected server response %s", resp.Status) + return utils.NewError(c.MakeCalendar, msg, c, err) + } else { + return nil + } +} + +// creates or updates one or more events on the remote CalDAV server +func (c *Client) PutEvents(path string, events ...*components.Event) error { + if len(events) <= 0 { + return utils.NewError(c.PutEvents, "no calendar events provided", c, nil) + } else if cal := components.NewCalendar(events...); events[0] == nil { + return utils.NewError(c.PutEvents, "icalendar event must not be nil", c, nil) + } else if err := c.PutCalendars(path, cal); err != nil { + return utils.NewError(c.PutEvents, "unable to put calendar", c, err) + } + return nil +} + +// creates or updates one or more calendars on the remote CalDAV server +func (c *Client) PutCalendars(path string, calendars ...*components.Calendar) error { + if req, err := c.Server().NewRequest("PUT", path, calendars); err != nil { + return utils.NewError(c.PutCalendars, "unable to encode request", c, err) + } else if resp, err := c.Do(req); err != nil { + return utils.NewError(c.PutCalendars, "unable to execute request", c, err) + } else if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusNoContent { + err := new(entities.Error) + resp.WebDAV().Decode(err) + msg := fmt.Sprintf("unexpected server response %s", resp.Status) + return utils.NewError(c.PutCalendars, msg, c, err) + } + return nil +} + +// attempts to fetch an event on the remote CalDAV server +func (c *Client) GetEvents(path string) ([]*components.Event, error) { + cal := new(components.Calendar) + if req, err := c.Server().NewRequest("GET", path); err != nil { + return nil, utils.NewError(c.GetEvents, "unable to create request", c, err) + } else if resp, err := c.Do(req); err != nil { + return nil, utils.NewError(c.GetEvents, "unable to execute request", c, err) + } else if resp.StatusCode != http.StatusOK { + err := new(entities.Error) + resp.WebDAV().Decode(err) + msg := fmt.Sprintf("unexpected server response %s", resp.Status) + return nil, utils.NewError(c.GetEvents, msg, c, err) + } else if err := resp.Decode(cal); err != nil { + return nil, utils.NewError(c.GetEvents, "unable to decode response", c, err) + } else { + return cal.Events, nil + } +} + +// attempts to fetch an event on the remote CalDAV server +func (c *Client) QueryEvents(path string, query *cent.CalendarQuery) (events []*components.Event, oerr error) { + ms := new(cent.Multistatus) + if req, err := c.Server().WebDAV().NewRequest("REPORT", path, query); err != nil { + oerr = utils.NewError(c.QueryEvents, "unable to create request", c, err) + } else if req.Http().Native().Header.Set("Depth", string(webdav.Depth1)); false { + } else if resp, err := c.WebDAV().Do(req); err != nil { + oerr = utils.NewError(c.QueryEvents, "unable to execute request", c, err) + } else if resp.StatusCode == http.StatusNotFound { + return // no events if not found + } else if resp.StatusCode != webdav.StatusMulti { + err := new(entities.Error) + msg := fmt.Sprintf("unexpected server response %s", resp.Status) + resp.Decode(err) + oerr = utils.NewError(c.QueryEvents, msg, c, err) + } else if err := resp.Decode(ms); err != nil { + msg := "unable to decode response" + oerr = utils.NewError(c.QueryEvents, msg, c, err) + } else { + for i, r := range ms.Responses { + for j, p := range r.PropStats { + if p.Prop == nil || p.Prop.CalendarData == nil { + continue + } else if cal, err := p.Prop.CalendarData.CalendarComponent(); err != nil { + msg := fmt.Sprintf("unable to decode property %d of response %d", j, i) + oerr = utils.NewError(c.QueryEvents, msg, c, err) + return + } else { + events = append(events, cal.Events...) + } + } + } + } + return +} + +// executes a CalDAV request +func (c *Client) Do(req *Request) (*Response, error) { + if resp, err := c.WebDAV().Do((*webdav.Request)(req)); err != nil { + return nil, utils.NewError(c.Do, "unable to execute CalDAV request", c, err) + } else { + return NewResponse(resp), nil + } +} + +// creates a new client for communicating with an WebDAV server +func NewClient(server *Server, native *http.Client) *Client { + return (*Client)(webdav.NewClient((*webdav.Server)(server), native)) +} + +// creates a new client for communicating with a WebDAV server +// uses the default HTTP client from net/http +func NewDefaultClient(server *Server) *Client { + return NewClient(server, http.DefaultClient) +} diff --git a/vendor/github.com/dolanor/caldav-go/caldav/entities/calendar_data.go b/vendor/github.com/dolanor/caldav-go/caldav/entities/calendar_data.go new file mode 100644 index 0000000..b532fca --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/caldav/entities/calendar_data.go @@ -0,0 +1,51 @@ +package entities + +import ( + "encoding/xml" + "github.com/dolanor/caldav-go/caldav/values" + "github.com/dolanor/caldav-go/icalendar" + "github.com/dolanor/caldav-go/icalendar/components" + "github.com/dolanor/caldav-go/utils" + "strings" +) + +// a CalDAV calendar data object +type CalendarData struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav calendar-data"` + Component *Component `xml:",omitempty"` + RecurrenceSetLimit *RecurrenceSetLimit `xml:",omitempty"` + ExpandRecurrenceSet *ExpandRecurrenceSet `xml:",omitempty"` + Content string `xml:",chardata"` +} + +func (c *CalendarData) CalendarComponent() (*components.Calendar, error) { + cal := new(components.Calendar) + if content := strings.TrimSpace(c.Content); content == "" { + return nil, utils.NewError(c.CalendarComponent, "no calendar data to decode", c, nil) + } else if err := icalendar.Unmarshal(content, cal); err != nil { + return nil, utils.NewError(c.CalendarComponent, "decoding calendar data failed", c, err) + } else { + return cal, nil + } +} + +// an iCalendar specifier for returned calendar data +type Component struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav comp"` + Properties []*PropertyName `xml:",omitempty"` + Components []*Component `xml:",omitempty"` +} + +// used to restrict recurring event data to a particular time range +type RecurrenceSetLimit struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav limit-recurrence-set"` + StartTime *values.DateTime `xml:"start,attr"` + EndTime *values.DateTime `xml:"end,attr"` +} + +// used to expand recurring events into individual calendar event data +type ExpandRecurrenceSet struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav expand"` + StartTime *values.DateTime `xml:"start,attr"` + EndTime *values.DateTime `xml:"end,attr"` +} diff --git a/vendor/github.com/dolanor/caldav-go/caldav/entities/calendar_query.go b/vendor/github.com/dolanor/caldav-go/caldav/entities/calendar_query.go new file mode 100644 index 0000000..e16b4a2 --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/caldav/entities/calendar_query.go @@ -0,0 +1,59 @@ +package entities + +import ( + "encoding/xml" + "github.com/dolanor/caldav-go/caldav/values" + "github.com/dolanor/caldav-go/utils" + "github.com/dolanor/caldav-go/webdav/entities" + "time" +) + +// a CalDAV calendar query object +type CalendarQuery struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav calendar-query"` + Prop *Prop `xml:",omitempty"` + AllProp *entities.AllProp `xml:",omitempty"` + Filter *Filter `xml:",omitempty"` +} + +// creates a new CalDAV query for iCalendar events from a particular time range +func NewEventRangeQuery(start, end time.Time) (*CalendarQuery, error) { + + var err error + var dtstart, dtend *values.DateTime + if dtstart, err = values.NewDateTime("start", start); err != nil { + return nil, utils.NewError(NewEventRangeQuery, "unable to encode start time", start, err) + } else if dtend, err = values.NewDateTime("end", end); err != nil { + return nil, utils.NewError(NewEventRangeQuery, "unable to encode end time", end, err) + } + + // construct the query object + query := new(CalendarQuery) + + // request all calendar data + query.Prop = new(Prop) + query.Prop.CalendarData = new(CalendarData) + + // expand recurring events + query.Prop.CalendarData.ExpandRecurrenceSet = new(ExpandRecurrenceSet) + query.Prop.CalendarData.ExpandRecurrenceSet.StartTime = dtstart + query.Prop.CalendarData.ExpandRecurrenceSet.EndTime = dtend + + // filter down calendar data to only iCalendar data + query.Filter = new(Filter) + query.Filter.ComponentFilter = new(ComponentFilter) + query.Filter.ComponentFilter.Name = values.CalendarComponentName + + // filter down iCalendar data to only events + query.Filter.ComponentFilter.ComponentFilter = new(ComponentFilter) + query.Filter.ComponentFilter.ComponentFilter.Name = values.EventComponentName + + // filter down the events to only those that fall within the time range + query.Filter.ComponentFilter.ComponentFilter.TimeRange = new(TimeRange) + query.Filter.ComponentFilter.ComponentFilter.TimeRange.StartTime = dtstart + query.Filter.ComponentFilter.ComponentFilter.TimeRange.EndTime = dtend + + // return the event query + return query, nil + +} diff --git a/vendor/github.com/dolanor/caldav-go/caldav/entities/filter.go b/vendor/github.com/dolanor/caldav-go/caldav/entities/filter.go new file mode 100644 index 0000000..1c49c7d --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/caldav/entities/filter.go @@ -0,0 +1,62 @@ +package entities + +import ( + "encoding/xml" + "github.com/dolanor/caldav-go/caldav/values" + "github.com/dolanor/caldav-go/icalendar/properties" +) + +// a CalDAV query filter entity +type Filter struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav filter"` + ComponentFilter *ComponentFilter `xml:",omitempty"` +} + +// used to filter down calendar components, such as VCALENDAR > VEVENT +type ComponentFilter struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav comp-filter"` + Name values.ComponentName `xml:"name,attr"` + ComponentFilter *ComponentFilter `xml:",omitempty"` + TimeRange *TimeRange `xml:",omitempty"` + PropertyFilter *PropertyFilter `xml:",omitempty"` + ParameterFilter *ParameterFilter `xml:",omitempty"` +} + +// used to restrict component filters to a particular time range +type TimeRange struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav time-range"` + StartTime *values.DateTime `xml:"start,attr"` + EndTime *values.DateTime `xml:"end,attr"` +} + +// used to restrict component filters to a property value +type PropertyFilter struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav prop-filter"` + Name properties.PropertyName `xml:"name,attr"` + TextMatch *TextMatch `xml:",omitempty"` + ParameterFilter *ParameterFilter `xml:",omitempty"` +} + +// used to restrict component filters to a parameter value +type ParameterFilter struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav param-filter"` + Name properties.ParameterName `xml:"name,attr"` + TextMatch *TextMatch `xml:",omitempty"` +} + +// used to match properties by text value +type TextMatch struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav text-match"` + Collation values.TextCollation `xml:"collation,attr,omitempty"` + NegateCondition values.HumanBoolean `xml:"attr,negate-condition,omitempty"` + Content string `xml:",innerxml"` +} + +// creates a new CalDAV property value matcher +func NewPropertyMatcher(name properties.PropertyName, content string) *PropertyFilter { + pf := new(PropertyFilter) + pf.Name = name + pf.TextMatch = new(TextMatch) + pf.TextMatch.Content = content + return pf +} diff --git a/vendor/github.com/dolanor/caldav-go/caldav/entities/multistatus.go b/vendor/github.com/dolanor/caldav-go/caldav/entities/multistatus.go new file mode 100644 index 0000000..512877e --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/caldav/entities/multistatus.go @@ -0,0 +1,23 @@ +package entities + +import "encoding/xml" + +// metadata about a property +type PropStat struct { + XMLName xml.Name `xml:"propstat"` + Status string `xml:"status"` + Prop *Prop `xml:",omitempty"` +} + +// a multistatus response entity +type Response struct { + XMLName xml.Name `xml:"response"` + Href string `xml:"href"` + PropStats []*PropStat `xml:"propstat,omitempty"` +} + +// a request to find properties on an an entity or collection +type Multistatus struct { + XMLName xml.Name `xml:"DAV: multistatus"` + Responses []*Response `xml:"response,omitempty"` +} diff --git a/vendor/github.com/dolanor/caldav-go/caldav/entities/prop.go b/vendor/github.com/dolanor/caldav-go/caldav/entities/prop.go new file mode 100644 index 0000000..19f49ad --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/caldav/entities/prop.go @@ -0,0 +1,23 @@ +package entities + +import ( + "encoding/xml" + "github.com/dolanor/caldav-go/webdav/entities" +) + +// a CalDAV Property resource +type Prop struct { + XMLName xml.Name `xml:"DAV: prop"` + GetContentType string `xml:"getcontenttype,omitempty"` + DisplayName string `xml:"displayname,omitempty"` + CalendarData *CalendarData `xml:",omitempty"` + ResourceType *entities.ResourceType `xml:",omitempty"` + CTag string `xml:"http://calendarserver.org/ns/ getctag,omitempty"` + ETag string `xml:"http://calendarserver.org/ns/ getetag,omitempty"` +} + +// used to restrict properties returned in calendar data +type PropertyName struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav prop"` + Name string `xml:"name,attr"` +} diff --git a/vendor/github.com/dolanor/caldav-go/caldav/request.go b/vendor/github.com/dolanor/caldav-go/caldav/request.go new file mode 100644 index 0000000..9594f43 --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/caldav/request.go @@ -0,0 +1,56 @@ +package caldav + +import ( + "bytes" + "github.com/dolanor/caldav-go/http" + "github.com/dolanor/caldav-go/icalendar" + "github.com/dolanor/caldav-go/utils" + "github.com/dolanor/caldav-go/webdav" + "io" + "io/ioutil" + "log" + "strings" +) + +var _ = log.Print + +// an CalDAV request object +type Request webdav.Request + +// downcasts the request to the WebDAV interface +func (r *Request) WebDAV() *webdav.Request { + return (*webdav.Request)(r) +} + +// creates a new CalDAV request object +func NewRequest(method string, urlstr string, icaldata ...interface{}) (*Request, error) { + if buffer, err := icalToReadCloser(icaldata...); err != nil { + return nil, utils.NewError(NewRequest, "unable to encode icalendar data", icaldata, err) + } else if r, err := http.NewRequest(method, urlstr, buffer); err != nil { + return nil, utils.NewError(NewRequest, "unable to create request", urlstr, err) + } else { + if buffer != nil { + // set the content type to XML if we have a body + r.Native().Header.Set("Content-Type", "text/calendar; charset=UTF-8") + } + return (*Request)(r), nil + } +} + +func icalToReadCloser(icaldata ...interface{}) (io.ReadCloser, error) { + var buffer []string + for _, icaldatum := range icaldata { + if encoded, err := icalendar.Marshal(icaldatum); err != nil { + return nil, utils.NewError(icalToReadCloser, "unable to encode as icalendar data", icaldatum, err) + } else { + // log.Printf("OUT: %+v", encoded) + buffer = append(buffer, encoded) + } + } + if len(buffer) > 0 { + var encoded = strings.Join(buffer, "\n") + return ioutil.NopCloser(bytes.NewBuffer([]byte(encoded))), nil + } else { + return nil, nil + } +} diff --git a/vendor/github.com/dolanor/caldav-go/caldav/response.go b/vendor/github.com/dolanor/caldav-go/caldav/response.go new file mode 100644 index 0000000..e504297 --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/caldav/response.go @@ -0,0 +1,40 @@ +package caldav + +import ( + "github.com/dolanor/caldav-go/icalendar" + "github.com/dolanor/caldav-go/utils" + "github.com/dolanor/caldav-go/webdav" + "io/ioutil" + "log" +) + +var _ = log.Print + +// a WebDAV response object +type Response webdav.Response + +// downcasts the response to the WebDAV interface +func (r *Response) WebDAV() *webdav.Response { + return (*webdav.Response)(r) +} + +// decodes a CalDAV iCalendar response into the provided interface +func (r *Response) Decode(into interface{}) error { + if body := r.Body; body == nil { + return nil + } else if encoded, err := ioutil.ReadAll(body); err != nil { + return utils.NewError(r.Decode, "unable to read response body", r, err) + } else { + // log.Printf("IN: %+v", string(encoded)) + if err := icalendar.Unmarshal(string(encoded), into); err != nil { + return utils.NewError(r.Decode, "unable to decode response body", r, err) + } else { + return nil + } + } +} + +// creates a new WebDAV response object +func NewResponse(response *webdav.Response) *Response { + return (*Response)(response) +} diff --git a/vendor/github.com/dolanor/caldav-go/caldav/server.go b/vendor/github.com/dolanor/caldav-go/caldav/server.go new file mode 100644 index 0000000..6453f23 --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/caldav/server.go @@ -0,0 +1,30 @@ +package caldav + +import ( + "github.com/dolanor/caldav-go/utils" + "github.com/dolanor/caldav-go/webdav" +) + +// a server that accepts CalDAV requests +type Server webdav.Server + +// NewServer creates a reference to a CalDAV server. +// host is the url to access the server, stopping at the port: +// https://user:password@host:port/ +func NewServer(host string) (*Server, error) { + if s, err := webdav.NewServer(host); err != nil { + return nil, utils.NewError(NewServer, "unable to create WebDAV server", host, err) + } else { + return (*Server)(s), nil + } +} + +// downcasts the server to the WebDAV interface +func (s *Server) WebDAV() *webdav.Server { + return (*webdav.Server)(s) +} + +// creates a new CalDAV request object +func (s *Server) NewRequest(method string, path string, icaldata ...interface{}) (*Request, error) { + return NewRequest(method, s.WebDAV().Http().AbsUrlStr(path), icaldata...) +} diff --git a/vendor/github.com/dolanor/caldav-go/caldav/values/component_name.go b/vendor/github.com/dolanor/caldav-go/caldav/values/component_name.go new file mode 100644 index 0000000..7e949dd --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/caldav/values/component_name.go @@ -0,0 +1,8 @@ +package values + +type ComponentName string + +const ( + CalendarComponentName ComponentName = "VCALENDAR" + EventComponentName = "VEVENT" +) diff --git a/vendor/github.com/dolanor/caldav-go/caldav/values/datetime.go b/vendor/github.com/dolanor/caldav-go/caldav/values/datetime.go new file mode 100644 index 0000000..1a92a8c --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/caldav/values/datetime.go @@ -0,0 +1,31 @@ +package values + +import ( + "encoding/xml" + "errors" + "github.com/dolanor/caldav-go/icalendar/values" + "time" +) + +// a representation of a date and time for iCalendar +type DateTime struct { + name string + t time.Time +} + +// creates a new caldav datetime representation, must be in UTC +func NewDateTime(name string, t time.Time) (*DateTime, error) { + if t.Location() != time.UTC { + return nil, errors.New("CalDAV datetime must be in UTC") + } else { + return &DateTime{name: name, t: t.Truncate(time.Second)}, nil + } +} + +// encodes the datetime value for the iCalendar specification +func (d *DateTime) MarshalXMLAttr(name xml.Name) (xml.Attr, error) { + layout := values.UTCDateTimeFormatString + value := d.t.Format(layout) + attr := xml.Attr{Name: name, Value: value} + return attr, nil +} diff --git a/vendor/github.com/dolanor/caldav-go/caldav/values/human_boolean.go b/vendor/github.com/dolanor/caldav-go/caldav/values/human_boolean.go new file mode 100644 index 0000000..a97a929 --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/caldav/values/human_boolean.go @@ -0,0 +1,8 @@ +package values + +type HumanBoolean string + +const ( + YesHumanBoolean HumanBoolean = "yes" + NoHumanBoolean = "no" +) diff --git a/vendor/github.com/dolanor/caldav-go/caldav/values/text_collation.go b/vendor/github.com/dolanor/caldav-go/caldav/values/text_collation.go new file mode 100644 index 0000000..5ae9a94 --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/caldav/values/text_collation.go @@ -0,0 +1,8 @@ +package values + +type TextCollation string + +const ( + OctetTextCollation TextCollation = "i;octet" + ASCIICaseMapCollation = "i;ascii-casemap" +) diff --git a/vendor/github.com/dolanor/caldav-go/http/client.go b/vendor/github.com/dolanor/caldav-go/http/client.go new file mode 100644 index 0000000..2f17c06 --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/http/client.go @@ -0,0 +1,53 @@ +package http + +import ( + "github.com/dolanor/caldav-go/utils" + "net/http" +) + +// a client for making HTTP requests +type Client struct { + native *http.Client + server *Server + requestHeaders map[string]string +} + +func (c *Client) SetHeader(key string, value string) { + if c.requestHeaders == nil { + c.requestHeaders = map[string]string{} + } + c.requestHeaders[key] = value +} + +// downcasts to the native HTTP interface +func (c *Client) Native() *http.Client { + return c.native +} + +// returns the embedded HTTP server reference +func (c *Client) Server() *Server { + return c.server +} + +// executes an HTTP request +func (c *Client) Do(req *Request) (*Response, error) { + for key, value := range c.requestHeaders { + req.Header.Add(key, value) + } + if resp, err := c.Native().Do((*http.Request)(req)); err != nil { + return nil, utils.NewError(c.Do, "unable to execute HTTP request", c, err) + } else { + return NewResponse(resp), nil + } +} + +// creates a new client for communicating with an HTTP server +func NewClient(server *Server, native *http.Client) *Client { + return &Client{server: server, native: native} +} + +// creates a new client for communicating with a server +// uses the default HTTP client from net/http +func NewDefaultClient(server *Server) *Client { + return NewClient(server, http.DefaultClient) +} diff --git a/vendor/github.com/dolanor/caldav-go/http/request.go b/vendor/github.com/dolanor/caldav-go/http/request.go new file mode 100644 index 0000000..5538ddc --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/http/request.go @@ -0,0 +1,39 @@ +package http + +import ( + "github.com/dolanor/caldav-go/utils" + "io" + "net/http" +) + +// an HTTP request object +type Request http.Request + +// downcasts the request to the native HTTP interface +func (r *Request) Native() *http.Request { + return (*http.Request)(r) +} + +// creates a new HTTP request object +func NewRequest(method string, urlstr string, body ...io.ReadCloser) (*Request, error) { + + var err error + var r = new(http.Request) + + if len(body) > 0 && body[0] != nil { + r, err = http.NewRequest(method, urlstr, body[0]) + } else { + r, err = http.NewRequest(method, urlstr, nil) + } + + if err != nil { + return nil, utils.NewError(NewRequest, "unable to create request", urlstr, err) + } else if auth := r.URL.User; auth != nil { + pass, _ := auth.Password() + r.SetBasicAuth(auth.Username(), pass) + r.URL.User = nil + } + + return (*Request)(r), nil + +} diff --git a/vendor/github.com/dolanor/caldav-go/http/response.go b/vendor/github.com/dolanor/caldav-go/http/response.go new file mode 100644 index 0000000..ed40102 --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/http/response.go @@ -0,0 +1,18 @@ +package http + +import ( + "net/http" +) + +// an HTTP response object +type Response http.Response + +// downcasts the response to the native HTTP interface +func (r *Response) Native() *http.Response { + return (*http.Response)(r) +} + +// creates a new HTTP response object +func NewResponse(response *http.Response) *Response { + return (*Response)(response) +} diff --git a/vendor/github.com/dolanor/caldav-go/http/server.go b/vendor/github.com/dolanor/caldav-go/http/server.go new file mode 100644 index 0000000..93098d4 --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/http/server.go @@ -0,0 +1,48 @@ +package http + +import ( + "github.com/dolanor/caldav-go/utils" + "io" + "log" + "net/url" + spath "path" + "strings" +) + +var _ = log.Print + +// a server that accepts HTTP requests +type Server struct { + baseUrl *url.URL +} + +// creates a reference to an http server +func NewServer(baseUrlStr string) (*Server, error) { + var err error + var s = new(Server) + if s.baseUrl, err = url.Parse(baseUrlStr); err != nil { + return nil, utils.NewError(NewServer, "unable to parse server base url", baseUrlStr, err) + } else { + return s, nil + } +} + +// converts a path name to an absolute URL +func (s *Server) UserInfo() *url.Userinfo { + return s.baseUrl.User +} + +// converts a path name to an absolute URL +func (s *Server) AbsUrlStr(path string) string { + uri := *s.baseUrl + uri.Path = spath.Join(uri.Path, path) + if strings.HasSuffix(path, "/") { + uri.Path = uri.Path + "/" + } + return uri.String() +} + +// creates a new HTTP request object +func (s *Server) NewRequest(method string, path string, body ...io.ReadCloser) (*Request, error) { + return NewRequest(method, s.AbsUrlStr(path), body...) +} diff --git a/vendor/github.com/dolanor/caldav-go/icalendar/components/calendar.go b/vendor/github.com/dolanor/caldav-go/icalendar/components/calendar.go new file mode 100644 index 0000000..5688f35 --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/icalendar/components/calendar.go @@ -0,0 +1,87 @@ +package components + +import ( + "fmt" + "github.com/dolanor/caldav-go/icalendar/values" + "github.com/dolanor/caldav-go/utils" + "time" +) + +type Calendar struct { + + // specifies the identifier corresponding to the highest version number or the minimum and maximum + // range of the iCalendar specification that is required in order to interpret the iCalendar object. + Version string `ical:",2.0"` + + // specifies the identifier for the product that created the iCalendar object + ProductId string `ical:"prodid,-//dolanor/caldav-go//NONSGML v1.0.0//EN"` + + // specifies the text value that uniquely identifies the "VTIMEZONE" calendar component. + TimeZoneId string `ical:"tzid,omitempty"` + + // defines the iCalendar object method associated with the calendar object. + Method values.Method `ical:",omitempty"` + + // defines the calendar scale used for the calendar information specified in the iCalendar object. + CalScale values.CalScale `ical:",omitempty"` + + // defines the different timezones used by the various components nested within + TimeZones []*TimeZone `ical:",omitempty"` + + // unique events to be stored together in the icalendar file + Events []*Event `ical:",omitempty"` +} + +func (c *Calendar) UseTimeZone(location *time.Location) *TimeZone { + tz := NewDynamicTimeZone(location) + c.TimeZones = append(c.TimeZones, tz) + c.TimeZoneId = tz.Id + return tz +} + +func (c *Calendar) UsingTimeZone() bool { + return len(c.TimeZoneId) > 0 +} + +func (c *Calendar) UsingGlobalTimeZone() bool { + return c.UsingTimeZone() && c.TimeZoneId[0] == '/' +} + +func (c *Calendar) ValidateICalValue() error { + + for i, e := range c.Events { + + if e == nil { + continue // skip nil events + } + + if err := e.ValidateICalValue(); err != nil { + msg := fmt.Sprintf("event %d failed validation", i) + return utils.NewError(c.ValidateICalValue, msg, c, err) + } + + if e.DateStart == nil && c.Method == "" { + msg := fmt.Sprintf("no value for method and no start date defined on event %d", i) + return utils.NewError(c.ValidateICalValue, msg, c, nil) + } + + } + + if c.UsingTimeZone() && !c.UsingGlobalTimeZone() { + for i, t := range c.TimeZones { + if t == nil || t.Id != c.TimeZoneId { + msg := fmt.Sprintf("timezone ID does not match timezone %d", i) + return utils.NewError(c.ValidateICalValue, msg, c, nil) + } + } + } + + return nil + +} + +func NewCalendar(events ...*Event) *Calendar { + cal := new(Calendar) + cal.Events = events + return cal +} diff --git a/vendor/github.com/dolanor/caldav-go/icalendar/components/event.go b/vendor/github.com/dolanor/caldav-go/icalendar/components/event.go new file mode 100644 index 0000000..6e9cd96 --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/icalendar/components/event.go @@ -0,0 +1,172 @@ +package components + +import ( + "github.com/dolanor/caldav-go/icalendar/values" + "github.com/dolanor/caldav-go/utils" + "time" +) + +type Event struct { + + // defines the persistent, globally unique identifier for the calendar component. + UID string `ical:",required"` + + // indicates the date/time that the instance of the iCalendar object was created. + DateStamp *values.DateTime `ical:"dtstamp,required"` + + // specifies when the calendar component begins. + DateStart *values.DateTime `ical:"dtstart,required"` + + // specifies the date and time that a calendar component ends. + DateEnd *values.DateTime `ical:"dtend,omitempty"` + + // specifies a positive duration of time. + Duration *values.Duration `ical:",omitempty"` + + // defines the access classification for a calendar component. + AccessClassification values.EventAccessClassification `ical:"class,omitempty"` + + // specifies the date and time that the calendar information was created by the calendar user agent in the + // calendar store. + // Note: This is analogous to the creation date and time for a file in the file system. + Created *values.DateTime `ical:",omitempty"` + + // provides a more complete description of the calendar component, than that provided by the Summary property. + Description string `ical:",omitempty"` + + // specifies information related to the global position for the activity specified by a calendar component. + Geo *values.Geo `ical:",omitempty"` + + // specifies the date and time that the information associated with the calendar component was last revised in the + // calendar store. + // Note: This is analogous to the modification date and time for a file in the file system. + LastModified *values.DateTime `ical:"last-modified,omitempty"` + + // defines the intended venue for the activity defined by a calendar component. + Location *values.Location `ical:",omitempty"` + + // defines the organizer for a calendar component. + Organizer *values.OrganizerContact `ical:",omitempty"` + + // defines the relative priority for a calendar component. + Priority int `ical:",omitempty"` + + // defines the revision sequence number of the calendar component within a sequence of revisions. + Sequence int `ical:",omitempty"` + + // efines the overall status or confirmation for the calendar component. + Status values.EventStatus `ical:",omitempty"` + + // defines a short summary or subject for the calendar component. + Summary string `ical:",omitempty"` + + // defines whether an event is transparent or not to busy time searches. + values.TimeTransparency `ical:"transp,omitempty"` + + // defines a Uniform Resource Locator (URL) associated with the iCalendar object. + Url *values.Url `ical:",omitempty"` + + // used in conjunction with the "UID" and "SEQUENCE" property to identify a specific instance of a recurring + // event calendar component. The property value is the effective value of the DateStart property of the + // recurrence instance. + RecurrenceId *values.DateTime `ical:"recurrence_id,omitempty"` + + // defines a rule or repeating pattern for recurring events, to-dos, or time zone definitions. + RecurrenceRules []*values.RecurrenceRule `ical:",omitempty"` + + // property provides the capability to associate a document object with a calendar component. + Attachment *values.Url `ical:"attach,omitempty"` + + // defines an "Attendee" within a calendar component. + Attendees []*values.AttendeeContact `ical:",omitempty"` + + // defines the categories for a calendar component. + Categories *values.CSV `ical:",omitempty"` + + // specifies non-processing information intended to provide a comment to the calendar user. + Comments []values.Comment `ical:",omitempty"` + + // used to represent contact information or alternately a reference to contact information associated with the calendar component. + ContactInfo *values.CSV `ical:"contact,omitempty"` + + // defines the list of date/time exceptions for a recurring calendar component. + *values.ExceptionDateTimes `ical:",omitempty"` + + // defines the list of date/times for a recurrence set. + *values.RecurrenceDateTimes `ical:",omitempty"` + + // used to represent a relationship or reference between one calendar component and another. + RelatedTo *values.Url `ical:"related-to,omitempty"` + + // defines the equipment or resources anticipated for an activity specified by a calendar entity. + Resources *values.CSV `ical:",omitempty"` +} + +// validates the event internals +func (e *Event) ValidateICalValue() error { + + if e.UID == "" { + return utils.NewError(e.ValidateICalValue, "the UID value must be set", e, nil) + } + + if e.DateStart == nil { + return utils.NewError(e.ValidateICalValue, "event start date must be set", e, nil) + } + + if e.DateEnd == nil && e.Duration == nil { + return utils.NewError(e.ValidateICalValue, "event end date or duration must be set", e, nil) + } + + if e.DateEnd != nil && e.Duration != nil { + return utils.NewError(e.ValidateICalValue, "event end date and duration are mutually exclusive fields", e, nil) + } + + return nil + +} + +// adds one or more recurrence rule to the event +func (e *Event) AddRecurrenceRules(r ...*values.RecurrenceRule) { + e.RecurrenceRules = append(e.RecurrenceRules, r...) +} + +// adds one or more recurrence rule exception to the event +func (e *Event) AddRecurrenceExceptions(d ...*values.DateTime) { + if e.ExceptionDateTimes == nil { + e.ExceptionDateTimes = new(values.ExceptionDateTimes) + } + *e.ExceptionDateTimes = append(*e.ExceptionDateTimes, d...) +} + +// checks to see if the event is a recurrence +func (e *Event) IsRecurrence() bool { + return e.RecurrenceId != nil +} + +// checks to see if the event is a recurrence override +func (e *Event) IsOverride() bool { + return e.IsRecurrence() && !e.RecurrenceId.Equals(e.DateStart) +} + +// creates a new iCalendar event with no end time +func NewEvent(uid string, start time.Time) *Event { + e := new(Event) + e.UID = uid + e.DateStamp = values.NewDateTime(time.Now().UTC()) + e.DateStart = values.NewDateTime(start) + return e +} + +// creates a new iCalendar event that lasts a certain duration +func NewEventWithDuration(uid string, start time.Time, duration time.Duration) *Event { + e := NewEvent(uid, start) + e.Duration = values.NewDuration(duration) + return e +} + +// creates a new iCalendar event that has an explicit start and end time +func NewEventWithEnd(uid string, start time.Time, end time.Time) *Event { + e := NewEvent(uid, start) + e.DateEnd = values.NewDateTime(end) + return e +} diff --git a/vendor/github.com/dolanor/caldav-go/icalendar/components/timezone.go b/vendor/github.com/dolanor/caldav-go/icalendar/components/timezone.go new file mode 100644 index 0000000..957fd65 --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/icalendar/components/timezone.go @@ -0,0 +1,40 @@ +package components + +import ( + "fmt" + "github.com/dolanor/caldav-go/icalendar/values" + "net/url" + "time" +) + +type TimeZone struct { + + // defines the persistent, globally unique identifier for the calendar component. + Id string `ical:"tzid,required"` + + // the location name, as defined by the standards body + ExtLocationName string `ical:"x-lic-location,omitempty"` + + // defines a Uniform Resource Locator (URL) associated with the iCalendar object. + Url *values.Url `ical:"tzurl,omitempty"` + + // specifies the date and time that the information associated with the calendar component was last revised in the + // calendar store. + // Note: This is analogous to the modification date and time for a file in the file system. + LastModified *values.DateTime `ical:"last-modified,omitempty"` + + // TODO need to figure out how to handle standard and daylight savings time + +} + +func NewDynamicTimeZone(location *time.Location) *TimeZone { + t := new(TimeZone) + t.Id = location.String() + t.ExtLocationName = location.String() + t.Url = values.NewUrl(url.URL{ + Scheme: "http", + Host: "tzurl.org", + Path: fmt.Sprintf("/zoneinfo/%s", t.Id), + }) + return t +} diff --git a/vendor/github.com/dolanor/caldav-go/icalendar/marshal.go b/vendor/github.com/dolanor/caldav-go/icalendar/marshal.go new file mode 100644 index 0000000..7b93d07 --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/icalendar/marshal.go @@ -0,0 +1,196 @@ +package icalendar + +import ( + "fmt" + "github.com/dolanor/caldav-go/icalendar/properties" + "github.com/dolanor/caldav-go/utils" + "log" + "reflect" + "strings" +) + +const ( + Newline = "\r\n" +) + +var _ = log.Print + +type encoder func(reflect.Value) (string, error) + +func tagAndJoinValue(v reflect.Value, in []string) (string, error) { + if tag, err := extractTagFromValue(v); err != nil { + return "", utils.NewError(tagAndJoinValue, "unable to extract tag from value", v, err) + } else { + var out []string + out = append(out, properties.MarshalProperty(properties.NewProperty("begin", tag))) + out = append(out, in...) + out = append(out, properties.MarshalProperty(properties.NewProperty("end", tag))) + return strings.Join(out, Newline), nil + } +} + +func marshalCollection(v reflect.Value) (string, error) { + + var out []string + + for i, n := 0, v.Len(); i < n; i++ { + vi := v.Index(i).Interface() + if encoded, err := Marshal(vi); err != nil { + msg := fmt.Sprintf("unable to encode interface at index %d", i) + return "", utils.NewError(marshalCollection, msg, vi, err) + } else if encoded != "" { + out = append(out, encoded) + } + } + + return strings.Join(out, Newline), nil + +} + +func marshalStruct(v reflect.Value) (string, error) { + + var out []string + + // iterate over all fields + vtype := v.Type() + n := vtype.NumField() + + for i := 0; i < n; i++ { + + // keep a reference to the field value and definition + fv := v.Field(i) + fs := vtype.Field(i) + + // use the field definition to extract out property defaults + p := properties.PropertyFromStructField(fs) + if p == nil { + continue // skip explicitly ignored fields and private members + } + + fi := fv.Interface() + + // some fields are not properties, but actually nested objects. + // detect those early using the property and object encoder... + if _, ok := fi.(properties.CanEncodeValue); !ok && !isInvalidOrEmptyValue(fv) { + if encoded, err := encode(fv, objectEncoder); err != nil { + msg := fmt.Sprintf("unable to encode field %s", fs.Name) + return "", utils.NewError(marshalStruct, msg, v.Interface(), err) + } else if encoded != "" { + // encoding worked! no need to process as a property + out = append(out, encoded) + continue + } + } + + // now check to see if the field value overrides the defaults... + if !isInvalidOrEmptyValue(fv) { + // first, check the field value interface for overrides... + if overrides, err := properties.PropertyFromInterface(fi); err != nil { + msg := fmt.Sprintf("field %s failed validation", fs.Name) + return "", utils.NewError(marshalStruct, msg, v.Interface(), err) + } else if p.Merge(overrides); p.Value == "" { + // then, if we couldn't find an override from the interface, + // try the simple string encoder... + if p.Value, err = stringEncoder(fv); err != nil { + msg := fmt.Sprintf("unable to encode field %s", fs.Name) + return "", utils.NewError(marshalStruct, msg, v.Interface(), err) + } + } + } + + // make sure we have a value by this point + if !p.HasNameAndValue() { + if p.OmitEmpty { + continue + } else if p.DefaultValue != "" { + p.Value = p.DefaultValue + } else if p.Required { + msg := fmt.Sprintf("missing value for required field %s", fs.Name) + return "", utils.NewError(Marshal, msg, v.Interface(), nil) + } + } + + // encode in the property + out = append(out, properties.MarshalProperty(p)) + + } + + // wrap the fields in the enclosing struct tags + return tagAndJoinValue(v, out) + +} + +func objectEncoder(v reflect.Value) (string, error) { + + // decompose the value into its interface parts + v = dereferencePointerValue(v) + + // encode the value based off of its type + switch v.Kind() { + case reflect.Slice: + fallthrough + case reflect.Array: + return marshalCollection(v) + case reflect.Struct: + return marshalStruct(v) + } + + return "", nil + +} + +func stringEncoder(v reflect.Value) (string, error) { + return fmt.Sprintf("%v", v.Interface()), nil +} + +func propertyEncoder(v reflect.Value) (string, error) { + + vi := v.Interface() + if p, err := properties.PropertyFromInterface(vi); err != nil { + + // return early if interface fails its own validation + return "", err + + } else if p.HasNameAndValue() { + + // if an interface encodes its own name and value, it's a property + return properties.MarshalProperty(p), nil + + } + + return "", nil + +} + +func encode(v reflect.Value, encoders ...encoder) (string, error) { + + for _, encode := range encoders { + if encoded, err := encode(v); err != nil { + return "", err + } else if encoded != "" { + return encoded, nil + } + } + + return "", nil + +} + +// converts an iCalendar component into its string representation +func Marshal(target interface{}) (string, error) { + + // don't do anything with invalid interfaces + v := reflect.ValueOf(target) + if isInvalidOrEmptyValue(v) { + return "", utils.NewError(Marshal, "unable to marshal empty or invalid values", target, nil) + } + + if encoded, err := encode(v, propertyEncoder, objectEncoder, stringEncoder); err != nil { + return "", err + } else if encoded == "" { + return "", utils.NewError(Marshal, "unable to encode interface, all methods exhausted", v.Interface(), nil) + } else { + return encoded, nil + } + +} diff --git a/vendor/github.com/dolanor/caldav-go/icalendar/properties/interfaces.go b/vendor/github.com/dolanor/caldav-go/icalendar/properties/interfaces.go new file mode 100644 index 0000000..1d31533 --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/icalendar/properties/interfaces.go @@ -0,0 +1,29 @@ +package properties + +type CanValidateValue interface { + ValidateICalValue() error +} + +type CanDecodeValue interface { + DecodeICalValue(string) error +} + +type CanDecodeParams interface { + DecodeICalParams(Params) error +} + +type CanEncodeTag interface { + EncodeICalTag() (string, error) +} + +type CanEncodeValue interface { + EncodeICalValue() (string, error) +} + +type CanEncodeName interface { + EncodeICalName() (PropertyName, error) +} + +type CanEncodeParams interface { + EncodeICalParams() (Params, error) +} diff --git a/vendor/github.com/dolanor/caldav-go/icalendar/properties/names.go b/vendor/github.com/dolanor/caldav-go/icalendar/properties/names.go new file mode 100644 index 0000000..21dec9c --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/icalendar/properties/names.go @@ -0,0 +1,31 @@ +package properties + +import "strings" + +type PropertyName string + +const ( + UIDPropertyName PropertyName = "UID" + CommentPropertyName = "COMMENT" + OrganizerPropertyName = "ORGANIZER" + AttendeePropertyName = "ATTENDEE" + ExceptionDateTimesPropertyName = "EXDATE" + RecurrenceDateTimesPropertyName = "RDATE" + RecurrenceRulePropertyName = "RRULE" + LocationPropertyName = "LOCATION" +) + +type ParameterName string + +const ( + CanonicalNameParameterName ParameterName = "CN" + TimeZoneIdPropertyName = "TZID" + ValuePropertyName = "VALUE" + AlternateRepresentationName = "ALTREP" +) + +type Params map[ParameterName]string + +func (p PropertyName) Equals(test string) bool { + return strings.EqualFold(string(p), test) +} diff --git a/vendor/github.com/dolanor/caldav-go/icalendar/properties/property.go b/vendor/github.com/dolanor/caldav-go/icalendar/properties/property.go new file mode 100644 index 0000000..0a1a710 --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/icalendar/properties/property.go @@ -0,0 +1,177 @@ +package properties + +import ( + "fmt" + "github.com/dolanor/caldav-go/utils" + "log" + "reflect" + "strings" +) + +var _ = log.Print + +var propNameSanitizer = strings.NewReplacer( + "_", "-", + ":", "\\:", +) + +var propValueSanitizer = strings.NewReplacer( + "\"", "'", + "\\", "\\\\", + "\n", "\\n", +) + +var propNameDesanitizer = strings.NewReplacer( + "-", "_", + "\\:", ":", +) + +var propValueDesanitizer = strings.NewReplacer( + "'", "\"", + "\\\\", "\\", + "\\n", "\n", +) + +type Property struct { + Name PropertyName + Value, DefaultValue string + Params Params + OmitEmpty, Required bool +} + +func (p *Property) HasNameAndValue() bool { + return p.Name != "" && p.Value != "" +} + +func (p *Property) Merge(override *Property) { + if override.Name != "" { + p.Name = override.Name + } + if override.Value != "" { + p.Value = override.Value + } + if override.Params != nil { + p.Params = override.Params + } +} + +func PropertyFromStructField(fs reflect.StructField) (p *Property) { + + ftag := fs.Tag.Get("ical") + if fs.PkgPath != "" || ftag == "-" { + return + } + + p = new(Property) + + // parse the field tag + if ftag != "" { + tags := strings.Split(ftag, ",") + p.Name = PropertyName(tags[0]) + if len(tags) > 1 { + if tags[1] == "omitempty" { + p.OmitEmpty = true + } else if tags[1] == "required" { + p.Required = true + } else { + p.DefaultValue = tags[1] + } + } + } + + // make sure we have a name + if p.Name == "" { + p.Name = PropertyName(fs.Name) + } + + p.Name = PropertyName(strings.ToUpper(string(p.Name))) + + return + +} + +func MarshalProperty(p *Property) string { + name := strings.ToUpper(propNameSanitizer.Replace(string(p.Name))) + value := propValueSanitizer.Replace(p.Value) + keys := []string{name} + for name, value := range p.Params { + name = ParameterName(strings.ToUpper(propNameSanitizer.Replace(string(name)))) + value = propValueSanitizer.Replace(value) + if strings.ContainsAny(value, " :") { + keys = append(keys, fmt.Sprintf("%s=\"%s\"", name, value)) + } else { + keys = append(keys, fmt.Sprintf("%s=%s", name, value)) + } + } + name = strings.Join(keys, ";") + return fmt.Sprintf("%s:%s", name, value) +} + +func PropertyFromInterface(target interface{}) (p *Property, err error) { + + var ierr error + if va, ok := target.(CanValidateValue); ok { + if ierr = va.ValidateICalValue(); ierr != nil { + err = utils.NewError(PropertyFromInterface, "interface failed validation", target, ierr) + return + } + } + + p = new(Property) + + if enc, ok := target.(CanEncodeName); ok { + if p.Name, ierr = enc.EncodeICalName(); ierr != nil { + err = utils.NewError(PropertyFromInterface, "interface failed name encoding", target, ierr) + return + } + } + + if enc, ok := target.(CanEncodeParams); ok { + if p.Params, ierr = enc.EncodeICalParams(); ierr != nil { + err = utils.NewError(PropertyFromInterface, "interface failed params encoding", target, ierr) + return + } + } + + if enc, ok := target.(CanEncodeValue); ok { + if p.Value, ierr = enc.EncodeICalValue(); ierr != nil { + err = utils.NewError(PropertyFromInterface, "interface failed value encoding", target, ierr) + return + } + } + + return + +} + +func UnmarshalProperty(line string) *Property { + nvp := strings.SplitN(line, ":", 2) + prop := new(Property) + if len(nvp) > 1 { + prop.Value = strings.TrimSpace(nvp[1]) + } + npp := strings.Split(nvp[0], ";") + if len(npp) > 1 { + prop.Params = make(map[ParameterName]string, 0) + for i := 1; i < len(npp); i++ { + var key, value string + kvp := strings.Split(npp[i], "=") + key = strings.TrimSpace(kvp[0]) + key = propNameDesanitizer.Replace(key) + if len(kvp) > 1 { + value = strings.TrimSpace(kvp[1]) + value = propValueDesanitizer.Replace(value) + value = strings.Trim(value, "\"") + } + prop.Params[ParameterName(key)] = value + } + } + prop.Name = PropertyName(strings.TrimSpace(npp[0])) + prop.Name = PropertyName(propNameDesanitizer.Replace(string(prop.Name))) + prop.Value = propValueDesanitizer.Replace(prop.Value) + return prop +} + +func NewProperty(name, value string) *Property { + return &Property{Name: PropertyName(name), Value: value} +} diff --git a/vendor/github.com/dolanor/caldav-go/icalendar/reflect.go b/vendor/github.com/dolanor/caldav-go/icalendar/reflect.go new file mode 100644 index 0000000..61c57d2 --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/icalendar/reflect.go @@ -0,0 +1,78 @@ +package icalendar + +import ( + "fmt" + "github.com/dolanor/caldav-go/icalendar/properties" + "github.com/dolanor/caldav-go/utils" + "log" + "reflect" + "strings" +) + +var _ = log.Print + +func isInvalidOrEmptyValue(v reflect.Value) bool { + if !v.IsValid() { + return true + } + switch v.Kind() { + case reflect.Array, reflect.Map, reflect.Slice, reflect.String: + return v.Len() == 0 + case reflect.Bool: + return !v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Interface, reflect.Ptr: + return v.IsNil() + } + return false +} + +func newValue(in reflect.Value) (out reflect.Value, isArrayElement bool) { + + typ := in.Type() + kind := typ.Kind() + + for { + if kind == reflect.Array || kind == reflect.Slice { + isArrayElement = true + } else if kind != reflect.Ptr { + break + } + typ = typ.Elem() + kind = typ.Kind() + } + + out = reflect.New(typ) + return + +} + +func dereferencePointerValue(v reflect.Value) reflect.Value { + for (v.Kind() == reflect.Interface || v.Kind() == reflect.Ptr) && v.Elem().IsValid() { + v = v.Elem() + } + return v +} + +func extractTagFromValue(v reflect.Value) (string, error) { + + vdref := dereferencePointerValue(v) + vtemp, _ := newValue(vdref) + + if encoder, ok := vtemp.Interface().(properties.CanEncodeTag); ok { + if tag, err := encoder.EncodeICalTag(); err != nil { + return "", utils.NewError(extractTagFromValue, "unable to extract tag from interface", v.Interface(), err) + } else { + return strings.ToUpper(tag), nil + } + } else { + typ := vtemp.Elem().Type() + return strings.ToUpper(fmt.Sprintf("v%s", typ.Name())), nil + } + +} diff --git a/vendor/github.com/dolanor/caldav-go/icalendar/unmarshal.go b/vendor/github.com/dolanor/caldav-go/icalendar/unmarshal.go new file mode 100644 index 0000000..6ec0d14 --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/icalendar/unmarshal.go @@ -0,0 +1,365 @@ +package icalendar + +import ( + "fmt" + "github.com/dolanor/caldav-go/icalendar/properties" + "github.com/dolanor/caldav-go/utils" + "log" + "reflect" + "regexp" + "strconv" + "strings" +) + +var _ = log.Print +var splitter = regexp.MustCompile("\r?\n") + +type token struct { + name string + components map[string][]*token + properties map[properties.PropertyName][]*properties.Property +} + +func tokenize(encoded string) (*token, error) { + if encoded = strings.TrimSpace(encoded); encoded == "" { + return nil, utils.NewError(tokenize, "no content to tokenize", encoded, nil) + } + return tokenizeSlice(splitter.Split(encoded, -1)) +} + +func tokenizeSlice(slice []string, name ...string) (*token, error) { + + tok := new(token) + size := len(slice) + + if len(name) > 0 { + tok.name = name[0] + } else if size <= 0 { + return nil, utils.NewError(tokenizeSlice, "token has no content", slice, nil) + } + + tok.properties = make(map[properties.PropertyName][]*properties.Property, 0) + tok.components = make(map[string][]*token, 0) + + for i := 0; i < size; i++ { + + // Handle iCalendar's space-indented line break format + // See: https://www.ietf.org/rfc/rfc2445.txt section 4.1 + // "a long line can be split between any two characters by inserting a CRLF immediately followed by a single + // linear white space character" + line := slice[i] + for ; i < size-1 && strings.HasPrefix(slice[i+1], " "); i++ { + next := slice[i+1] + line += next[1:len(next)] + } + + prop := properties.UnmarshalProperty(line) + + if prop.Name.Equals("begin") { + for j := i; j < size; j++ { + end := strings.Replace(line, "BEGIN", "END", 1) + if slice[j] == end { + if component, err := tokenizeSlice(slice[i+1:j], prop.Value); err != nil { + msg := fmt.Sprintf("unable to tokenize %s component", prop.Value) + return nil, utils.NewError(tokenizeSlice, msg, slice, err) + } else { + existing, _ := tok.components[prop.Value] + tok.components[prop.Value] = append(existing, component) + i = j + break + } + } + } + } else if existing, ok := tok.properties[prop.Name]; ok { + tok.properties[prop.Name] = []*properties.Property{prop} + } else { + tok.properties[prop.Name] = append(existing, prop) + } + + } + + return tok, nil + +} + +func hydrateInterface(v reflect.Value, prop *properties.Property) (bool, error) { + + // unable to decode into empty values + if isInvalidOrEmptyValue(v) { + return false, nil + } + + var i = v.Interface() + var hasValue = false + + // decode a value if possible + if decoder, ok := i.(properties.CanDecodeValue); ok { + if err := decoder.DecodeICalValue(prop.Value); err != nil { + return false, utils.NewError(hydrateInterface, "error decoding property value", v, err) + } else { + hasValue = true + } + } + + // decode any params, if supported + if len(prop.Params) > 0 { + if decoder, ok := i.(properties.CanDecodeParams); ok { + if err := decoder.DecodeICalParams(prop.Params); err != nil { + return false, utils.NewError(hydrateInterface, "error decoding property parameters", v, err) + } + } + } + + // finish with any validation + if validator, ok := i.(properties.CanValidateValue); ok { + if err := validator.ValidateICalValue(); err != nil { + return false, utils.NewError(hydrateInterface, "error validating property value", v, err) + } + } + + return hasValue, nil + +} + +func hydrateLiteral(v reflect.Value, prop *properties.Property) (reflect.Value, error) { + + literal := dereferencePointerValue(v) + + switch literal.Kind() { + case reflect.Bool: + if i, err := strconv.ParseBool(prop.Value); err != nil { + return literal, utils.NewError(hydrateLiteral, "unable to decode bool "+prop.Value, literal.Interface(), err) + } else { + literal.SetBool(i) + } + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if i, err := strconv.ParseInt(prop.Value, 10, 64); err != nil { + return literal, utils.NewError(hydrateLiteral, "unable to decode int "+prop.Value, literal.Interface(), err) + } else { + literal.SetInt(i) + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + if i, err := strconv.ParseUint(prop.Value, 10, 64); err != nil { + return literal, utils.NewError(hydrateLiteral, "unable to decode uint "+prop.Value, literal.Interface(), err) + } else { + literal.SetUint(i) + } + case reflect.Float32, reflect.Float64: + if i, err := strconv.ParseFloat(prop.Value, 64); err != nil { + return literal, utils.NewError(hydrateLiteral, "unable to decode float "+prop.Value, literal.Interface(), err) + } else { + literal.SetFloat(i) + } + case reflect.String: + literal.SetString(prop.Value) + default: + return literal, utils.NewError(hydrateLiteral, "unable to decode value as literal "+prop.Value, literal.Interface(), nil) + } + + return literal, nil + +} + +func hydrateProperty(v reflect.Value, prop *properties.Property) error { + + // check to see if the interface handles it's own hydration + if handled, err := hydrateInterface(v, prop); err != nil { + return utils.NewError(hydrateProperty, "unable to hydrate interface", v, err) + } else if handled { + return nil // exit early if handled by the interface + } + + // if we got here, we need to create a new instance to + // set into the property. + var vnew, varr = newValue(v) + var vlit bool + + // check to see if the new value handles it's own hydration + if handled, err := hydrateInterface(vnew, prop); err != nil { + return utils.NewError(hydrateProperty, "unable to hydrate new interface value", vnew, err) + } else if vlit = !handled; vlit { + // if not, treat it as a literal + if vnewlit, err := hydrateLiteral(vnew, prop); err != nil { + return utils.NewError(hydrateProperty, "unable to hydrate new literal value", vnew, err) + } else if _, err := hydrateInterface(vnewlit, prop); err != nil { + return utils.NewError(hydrateProperty, "unable to hydrate new literal interface value", vnewlit, err) + } + } + + // now we can set the value + vnewval := dereferencePointerValue(vnew) + voldval := dereferencePointerValue(v) + + // make sure we can set the new value into the provided pointer + + if varr { + // for arrays, append the new value into the array structure + if !voldval.CanSet() { + return utils.NewError(hydrateProperty, "unable to set array value", v, nil) + } else { + voldval.Set(reflect.Append(voldval, vnewval)) + } + } else if vlit { + // for literals, set the dereferenced value + if !voldval.CanSet() { + return utils.NewError(hydrateProperty, "unable to set literal value", v, nil) + } else { + voldval.Set(vnewval) + } + } else if !v.CanSet() { + return utils.NewError(hydrateProperty, "unable to set pointer value", v, nil) + } else { + // everything else should be a pointer, set it directly + v.Set(vnew) + } + + return nil + +} + +func hydrateNestedComponent(v reflect.Value, component *token) error { + + // create a new object to hold the property value + var vnew, varr = newValue(v) + if err := hydrateComponent(vnew, component); err != nil { + return utils.NewError(hydrateNestedComponent, "unable to decode component", component, err) + } + + if varr { + // for arrays, append the new value into the array structure + voldval := dereferencePointerValue(v) + if !voldval.CanSet() { + return utils.NewError(hydrateNestedComponent, "unable to set array value", v, nil) + } else { + voldval.Set(reflect.Append(voldval, vnew)) + } + } else if !v.CanSet() { + return utils.NewError(hydrateNestedComponent, "unable to set pointer value", v, nil) + } else { + // everything else should be a pointer, set it directly + v.Set(vnew) + } + + return nil + +} + +func hydrateProperties(v reflect.Value, component *token) error { + + vdref := dereferencePointerValue(v) + vtype := vdref.Type() + vkind := vdref.Kind() + + if vkind != reflect.Struct { + return utils.NewError(hydrateProperties, "unable to hydrate properties of non-struct", v, nil) + } + + n := vtype.NumField() + for i := 0; i < n; i++ { + + prop := properties.PropertyFromStructField(vtype.Field(i)) + if prop == nil { + continue // skip if field is ignored + } + + vfield := vdref.Field(i) + + // first try to hydrate property values + if properties, ok := component.properties[prop.Name]; ok { + for _, prop := range properties { + if err := hydrateProperty(vfield, prop); err != nil { + msg := fmt.Sprintf("unable to hydrate property %s", prop.Name) + return utils.NewError(hydrateProperties, msg, v, err) + } + } + } + + // then try to hydrate components + vtemp, _ := newValue(vfield) + if tag, err := extractTagFromValue(vtemp); err != nil { + msg := fmt.Sprintf("unable to extract tag from property %s", prop.Name) + return utils.NewError(hydrateProperties, msg, v, err) + } else if components, ok := component.components[tag]; ok { + for _, comp := range components { + if err := hydrateNestedComponent(vfield, comp); err != nil { + msg := fmt.Sprintf("unable to hydrate component %s", prop.Name) + return utils.NewError(hydrateProperties, msg, v, err) + } + } + } + } + + return nil + +} + +func hydrateComponent(v reflect.Value, component *token) error { + if tag, err := extractTagFromValue(v); err != nil { + return utils.NewError(hydrateComponent, "error extracting tag from value", component, err) + } else if tag != component.name { + msg := fmt.Sprintf("expected %s and found %s", tag, component.name) + return utils.NewError(hydrateComponent, msg, component, nil) + } else if err := hydrateProperties(v, component); err != nil { + return utils.NewError(hydrateComponent, "unable to hydrate properties", component, err) + } + return nil +} + +func hydrateComponents(v reflect.Value, components []*token) error { + vdref := dereferencePointerValue(v) + for i, component := range components { + velem := reflect.New(vdref.Type().Elem()) + if err := hydrateComponent(velem, component); err != nil { + msg := fmt.Sprintf("unable to hydrate component %d", i) + return utils.NewError(hydrateComponent, msg, component, err) + } else { + v.Set(reflect.Append(vdref, velem)) + } + } + return nil +} + +func hydrateValue(v reflect.Value, component *token) error { + + if !v.IsValid() || v.Kind() != reflect.Ptr { + return utils.NewError(hydrateValue, "unmarshal target must be a valid pointer", v, nil) + } + + // handle any encodable properties + if encoder, isprop := v.Interface().(properties.CanEncodeName); isprop { + if name, err := encoder.EncodeICalName(); err != nil { + return utils.NewError(hydrateValue, "unable to lookup property name", v, err) + } else if properties, found := component.properties[name]; !found || len(properties) == 0 { + return utils.NewError(hydrateValue, "no matching propery values found for "+string(name), v, nil) + } else if len(properties) > 1 { + return utils.NewError(hydrateValue, "more than one property value matches single property interface", v, nil) + } else { + return hydrateProperty(v, properties[0]) + } + } + + // handle components + vkind := dereferencePointerValue(v).Kind() + if tag, err := extractTagFromValue(v); err != nil { + return utils.NewError(hydrateValue, "unable to extract component tag", v, err) + } else if components, found := component.components[tag]; !found || len(components) == 0 { + msg := fmt.Sprintf("unable to find matching component for %s", tag) + return utils.NewError(hydrateValue, msg, v, nil) + } else if vkind == reflect.Array || vkind == reflect.Slice { + return hydrateComponents(v, components) + } else if len(components) > 1 { + return utils.NewError(hydrateValue, "non-array interface provided but more than one component found!", v, nil) + } else { + return hydrateComponent(v, components[0]) + } + +} + +// decodes encoded icalendar data into a native interface +func Unmarshal(encoded string, into interface{}) error { + if component, err := tokenize(encoded); err != nil { + return utils.NewError(Unmarshal, "unable to tokenize encoded data", encoded, err) + } else { + return hydrateValue(reflect.ValueOf(into), component) + } +} diff --git a/vendor/github.com/dolanor/caldav-go/icalendar/values/cal_scale.go b/vendor/github.com/dolanor/caldav-go/icalendar/values/cal_scale.go new file mode 100644 index 0000000..449d256 --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/icalendar/values/cal_scale.go @@ -0,0 +1,7 @@ +package values + +type CalScale string + +const ( + GregorianCalScale CalScale = "GREGORIAN" +) diff --git a/vendor/github.com/dolanor/caldav-go/icalendar/values/comment.go b/vendor/github.com/dolanor/caldav-go/icalendar/values/comment.go new file mode 100644 index 0000000..cfff0f2 --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/icalendar/values/comment.go @@ -0,0 +1,33 @@ +package values + +import ( + "github.com/dolanor/caldav-go/icalendar/properties" +) + +// specifies non-processing information intended to provide a comment to the calendar user. +type Comment string + +// encodes the comment value for the iCalendar specification +func (c Comment) EncodeICalValue() (string, error) { + return string(c), nil +} + +// decodes the comment value from the iCalendar specification +func (c Comment) DecodeICalValue(value string) error { + c = Comment(value) + return nil +} + +// encodes the comment value for the iCalendar specification +func (c Comment) EncodeICalName() (properties.PropertyName, error) { + return properties.CommentPropertyName, nil +} + +// creates a list of comments from strings +func NewComments(comments ...string) []Comment { + var _comments []Comment + for _, comment := range comments { + _comments = append(_comments, Comment(comment)) + } + return _comments +} diff --git a/vendor/github.com/dolanor/caldav-go/icalendar/values/contact.go b/vendor/github.com/dolanor/caldav-go/icalendar/values/contact.go new file mode 100644 index 0000000..6f5d088 --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/icalendar/values/contact.go @@ -0,0 +1,139 @@ +package values + +import ( + "fmt" + "github.com/dolanor/caldav-go/icalendar/properties" + "github.com/dolanor/caldav-go/utils" + "log" + "net/mail" + "strings" +) + +var _ = log.Print + +// Specifies the organizer of a group scheduled calendar entity. The property is specified within the "VFREEBUSY" +// calendar component to specify the calendar user requesting the free or busy time. When publishing a "VFREEBUSY" +// calendar component, the property is used to specify the calendar that the published busy time came from. +// +// The property has the property parameters CN, for specifying the common or display name associated with the +// "Organizer", DIR, for specifying a pointer to the directory information associated with the "Organizer", +// SENT-BY, for specifying another calendar user that is acting on behalf of the "Organizer". The non-standard +// parameters may also be specified on this property. If the LANGUAGE property parameter is specified, the identified +// language applies to the CN parameter value. +type Contact struct { + Entry mail.Address +} + +type AttendeeContact Contact +type OrganizerContact Contact + +// creates a new icalendar attendee representation +func NewAttendeeContact(name, email string) *AttendeeContact { + return &AttendeeContact{Entry: mail.Address{Name: name, Address: email}} +} + +// creates a new icalendar organizer representation +func NewOrganizerContact(name, email string) *OrganizerContact { + return &OrganizerContact{Entry: mail.Address{Name: name, Address: email}} +} + +// validates the contact value for the iCalendar specification +func (c *Contact) ValidateICalValue() error { + email := c.Entry.String() + if _, err := mail.ParseAddress(email); err != nil { + msg := fmt.Sprintf("unable to validate address %s", email) + return utils.NewError(c.ValidateICalValue, msg, c, err) + } else { + return nil + } +} + +// encodes the contact value for the iCalendar specification +func (c *Contact) EncodeICalValue() (string, error) { + return fmt.Sprintf("MAILTO:%s", c.Entry.Address), nil +} + +// encodes the contact params for the iCalendar specification +func (c *Contact) EncodeICalParams() (params properties.Params, err error) { + if c.Entry.Name != "" { + params = properties.Params{properties.CanonicalNameParameterName: c.Entry.Name} + } + return +} + +// decodes the contact value from the iCalendar specification +func (c *Contact) DecodeICalValue(value string) error { + parts := strings.SplitN(value, ":", 2) + if len(parts) > 1 { + c.Entry.Address = parts[1] + } + return nil +} + +// decodes the contact params from the iCalendar specification +func (c *Contact) DecodeICalParams(params properties.Params) error { + if name, found := params[properties.CanonicalNameParameterName]; found { + c.Entry.Name = name + } + return nil +} + +// validates the contact value for the iCalendar specification +func (c *OrganizerContact) ValidateICalValue() error { + return (*Contact)(c).ValidateICalValue() +} + +// encodes the contact value for the iCalendar specification +func (c *OrganizerContact) EncodeICalValue() (string, error) { + return (*Contact)(c).EncodeICalValue() +} + +// encodes the contact params for the iCalendar specification +func (c *OrganizerContact) EncodeICalParams() (params properties.Params, err error) { + return (*Contact)(c).EncodeICalParams() +} + +// decodes the contact value from the iCalendar specification +func (c *OrganizerContact) DecodeICalValue(value string) error { + return (*Contact)(c).DecodeICalValue(value) +} + +// decodes the contact params from the iCalendar specification +func (c *OrganizerContact) DecodeICalParams(params properties.Params) error { + return (*Contact)(c).DecodeICalParams(params) +} + +// encodes the contact property name for the iCalendar specification +func (o *OrganizerContact) EncodeICalName() (properties.PropertyName, error) { + return properties.OrganizerPropertyName, nil +} + +// validates the contact value for the iCalendar specification +func (c *AttendeeContact) ValidateICalValue() error { + return (*Contact)(c).ValidateICalValue() +} + +// encodes the contact value for the iCalendar specification +func (c *AttendeeContact) EncodeICalValue() (string, error) { + return (*Contact)(c).EncodeICalValue() +} + +// encodes the contact params for the iCalendar specification +func (c *AttendeeContact) EncodeICalParams() (params properties.Params, err error) { + return (*Contact)(c).EncodeICalParams() +} + +// decodes the contact value from the iCalendar specification +func (c *AttendeeContact) DecodeICalValue(value string) error { + return (*Contact)(c).DecodeICalValue(value) +} + +// decodes the contact params from the iCalendar specification +func (c *AttendeeContact) DecodeICalParams(params properties.Params) error { + return (*Contact)(c).DecodeICalParams(params) +} + +// encodes the contact property name for the iCalendar specification +func (o *AttendeeContact) EncodeICalName() (properties.PropertyName, error) { + return properties.AttendeePropertyName, nil +} diff --git a/vendor/github.com/dolanor/caldav-go/icalendar/values/csv.go b/vendor/github.com/dolanor/caldav-go/icalendar/values/csv.go new file mode 100644 index 0000000..1eb69a1 --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/icalendar/values/csv.go @@ -0,0 +1,24 @@ +package values + +import ( + "log" + "strings" +) + +var _ = log.Print + +type CSV []string + +func (csv *CSV) EncodeICalValue() (string, error) { + return strings.Join(*csv, ","), nil +} + +func (csv *CSV) DecodeICalValue(value string) error { + value = strings.TrimSpace(value) + *csv = CSV(strings.Split(value, ",")) + return nil +} + +func NewCSV(items ...string) *CSV { + return (*CSV)(&items) +} diff --git a/vendor/github.com/dolanor/caldav-go/icalendar/values/datetime.go b/vendor/github.com/dolanor/caldav-go/icalendar/values/datetime.go new file mode 100644 index 0000000..201053d --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/icalendar/values/datetime.go @@ -0,0 +1,265 @@ +package values + +import ( + "fmt" + "github.com/dolanor/caldav-go/icalendar/properties" + "github.com/dolanor/caldav-go/utils" + "log" + "strings" + "time" +) + +var _ = log.Print + +const DateFormatString = "20060102" +const DateTimeFormatString = "20060102T150405" +const UTCDateTimeFormatString = "20060102T150405Z" + +// a representation of a date and time for iCalendar +type DateTime struct { + t time.Time +} + +type DateTimes []*DateTime + +// The exception dates, if specified, are used in computing the recurrence set. The recurrence set is the complete set +// of recurrence instances for a calendar component. The recurrence set is generated by considering the initial +// "DTSTART" property along with the "RRULE", "RDATE", "EXDATE" and "EXRULE" properties contained within the iCalendar +// object. The "DTSTART" property defines the first instance in the recurrence set. Multiple instances of the "RRULE" +// and "EXRULE" properties can also be specified to define more sophisticated recurrence sets. The final recurrence set +// is generated by gathering all of the start date-times generated by any of the specified "RRULE" and "RDATE" +// properties, and then excluding any start date and times which fall within the union of start date and times +// generated by any specified "EXRULE" and "EXDATE" properties. This implies that start date and times within exclusion +// related properties (i.e., "EXDATE" and "EXRULE") take precedence over those specified by inclusion properties +// (i.e., "RDATE" and "RRULE"). Where duplicate instances are generated by the "RRULE" and "RDATE" properties, only +// one recurrence is considered. Duplicate instances are ignored. +// +// The "EXDATE" property can be used to exclude the value specified in "DTSTART". However, in such cases the original +// "DTSTART" date MUST still be maintained by the calendaring and scheduling system because the original "DTSTART" +// value has inherent usage dependencies by other properties such as the "RECURRENCE-ID". +type ExceptionDateTimes DateTimes + +// The recurrence dates, if specified, are used in computing the recurrence set. The recurrence set is the complete set +// of recurrence instances for a calendar component. The recurrence set is generated by considering the initial +// "DTSTART" property along with the "RRULE", "RDATE", "EXDATE" and "EXRULE" properties contained within the iCalendar +// object. The "DTSTART" property defines the first instance in the recurrence set. Multiple instances of the "RRULE" +// and "EXRULE" properties can also be specified to define more sophisticated recurrence sets. The final recurrence set +// is generated by gathering all of the start date-times generated by any of the specified "RRULE" and "RDATE" +// properties, and then excluding any start date and times which fall within the union of start date and times +// generated by any specified "EXRULE" and "EXDATE" properties. This implies that start date and times within exclusion +// related properties (i.e., "EXDATE" and "EXRULE") take precedence over those specified by inclusion properties +// (i.e., "RDATE" and "RRULE"). Where duplicate instances are generated by the "RRULE" and "RDATE" properties, only +// one recurrence is considered. Duplicate instances are ignored. +type RecurrenceDateTimes DateTimes + +// creates a new icalendar datetime representation +func NewDateTime(t time.Time) *DateTime { + return &DateTime{t: t.Truncate(time.Second)} +} + +// creates a new icalendar datetime array representation +func NewDateTimes(dates ...*DateTime) DateTimes { + return DateTimes(dates) +} + +// creates a new icalendar datetime array representation +func NewExceptionDateTimes(dates ...*DateTime) *ExceptionDateTimes { + datetimes := NewDateTimes(dates...) + return (*ExceptionDateTimes)(&datetimes) +} + +// creates a new icalendar datetime array representation +func NewRecurrenceDateTimes(dates ...*DateTime) *RecurrenceDateTimes { + datetimes := NewDateTimes(dates...) + return (*RecurrenceDateTimes)(&datetimes) +} + +// checks to see if two datetimes are equal +func (d *DateTime) Equals(test *DateTime) bool { + return d.t.Equal(test.t) +} + +// returns the native time for the datetime object +func (d *DateTime) NativeTime() time.Time { + return d.t +} + +// encodes the datetime value for the iCalendar specification +func (d *DateTime) EncodeICalValue() (string, error) { + val := d.t.Format(DateTimeFormatString) + loc := d.t.Location() + if loc == time.UTC { + val = fmt.Sprintf("%sZ", val) + } + return val, nil +} + +// decodes the datetime value from the iCalendar specification +func (d *DateTime) DecodeICalValue(value string) error { + layout := DateTimeFormatString + if strings.HasSuffix(value, "Z") { + layout = UTCDateTimeFormatString + } else if len(value) == 8 { + layout = DateFormatString + } + var err error + d.t, err = time.ParseInLocation(layout, value, time.UTC) + if err != nil { + return utils.NewError(d.DecodeICalValue, "unable to parse datetime value", d, err) + } else { + return nil + } +} + +// encodes the datetime params for the iCalendar specification +func (d *DateTime) EncodeICalParams() (params properties.Params, err error) { + loc := d.t.Location() + if loc != time.UTC { + params = properties.Params{properties.TimeZoneIdPropertyName: loc.String()} + } + return +} + +// decodes the datetime params from the iCalendar specification +func (d *DateTime) DecodeICalParams(params properties.Params) error { + layout := DateTimeFormatString + value := d.t.Format(layout) + if name, found := params[properties.TimeZoneIdPropertyName]; !found { + return nil + } else if loc, err := time.LoadLocation(name); err != nil { + return utils.NewError(d.DecodeICalValue, "unable to parse timezone", d, err) + } else if t, err := time.ParseInLocation(layout, value, loc); err != nil { + return utils.NewError(d.DecodeICalValue, "unable to parse datetime value", d, err) + } else { + d.t = t + return nil + } +} + +// validates the datetime value against the iCalendar specification +func (d *DateTime) ValidateICalValue() error { + + loc := d.t.Location() + + if loc == time.Local { + msg := "DateTime location may not Local, please use UTC or explicit Location" + return utils.NewError(d.ValidateICalValue, msg, d, nil) + } + + if loc.String() == "" { + msg := "DateTime location must have a valid name" + return utils.NewError(d.ValidateICalValue, msg, d, nil) + } + + return nil +} + +// encodes the datetime value for the iCalendar specification +func (d *DateTime) String() string { + if s, err := d.EncodeICalValue(); err != nil { + panic(err) + } else { + return s + } +} + +// encodes a list of datetime values for the iCalendar specification +func (ds *DateTimes) EncodeICalValue() (string, error) { + var csv CSV + for i, d := range *ds { + if s, err := d.EncodeICalValue(); err != nil { + msg := fmt.Sprintf("unable to encode datetime at index %d", i) + return "", utils.NewError(ds.EncodeICalValue, msg, ds, err) + } else { + csv = append(csv, s) + } + } + return csv.EncodeICalValue() +} + +// encodes a list of datetime params for the iCalendar specification +func (ds *DateTimes) EncodeICalParams() (params properties.Params, err error) { + if len(*ds) > 0 { + params, err = (*ds)[0].EncodeICalParams() + } + return +} + +// decodes a list of datetime params from the iCalendar specification +func (ds *DateTimes) DecodeICalParams(params properties.Params) error { + for i, d := range *ds { + if err := d.DecodeICalParams(params); err != nil { + msg := fmt.Sprintf("unable to decode datetime params for index %d", i) + return utils.NewError(ds.DecodeICalValue, msg, ds, err) + } + } + return nil +} + +// encodes a list of datetime values for the iCalendar specification +func (ds *DateTimes) DecodeICalValue(value string) error { + csv := new(CSV) + if err := csv.DecodeICalValue(value); err != nil { + return utils.NewError(ds.DecodeICalValue, "unable to decode datetime list as CSV", ds, err) + } + for i, value := range *csv { + d := new(DateTime) + if err := d.DecodeICalValue(value); err != nil { + msg := fmt.Sprintf("unable to decode datetime at index %d", i) + return utils.NewError(ds.DecodeICalValue, msg, ds, err) + } else { + *ds = append(*ds, d) + } + } + return nil +} + +// encodes exception date times property name for icalendar +func (e *ExceptionDateTimes) EncodeICalName() (properties.PropertyName, error) { + return properties.ExceptionDateTimesPropertyName, nil +} + +// encodes recurrence date times property name for icalendar +func (r *RecurrenceDateTimes) EncodeICalName() (properties.PropertyName, error) { + return properties.RecurrenceDateTimesPropertyName, nil +} + +// encodes exception date times property value for icalendar +func (e *ExceptionDateTimes) EncodeICalValue() (string, error) { + return (*DateTimes)(e).EncodeICalValue() +} + +// encodes recurrence date times property value for icalendar +func (r *RecurrenceDateTimes) EncodeICalValue() (string, error) { + return (*DateTimes)(r).EncodeICalValue() +} + +// decodes exception date times property value for icalendar +func (e *ExceptionDateTimes) DecodeICalValue(value string) error { + return (*DateTimes)(e).DecodeICalValue(value) +} + +// decodes recurrence date times property value for icalendar +func (r *RecurrenceDateTimes) DecodeICalValue(value string) error { + return (*DateTimes)(r).DecodeICalValue(value) +} + +// encodes exception date times property params for icalendar +func (e *ExceptionDateTimes) EncodeICalParams() (params properties.Params, err error) { + return (*DateTimes)(e).EncodeICalParams() +} + +// encodes recurrence date times property params for icalendar +func (r *RecurrenceDateTimes) EncodeICalParams() (params properties.Params, err error) { + return (*DateTimes)(r).EncodeICalParams() +} + +// encodes exception date times property params for icalendar +func (e *ExceptionDateTimes) DecodeICalParams(params properties.Params) error { + return (*DateTimes)(e).DecodeICalParams(params) +} + +// encodes recurrence date times property params for icalendar +func (r *RecurrenceDateTimes) DecodeICalParams(params properties.Params) error { + return (*DateTimes)(r).DecodeICalParams(params) +} diff --git a/vendor/github.com/dolanor/caldav-go/icalendar/values/duration.go b/vendor/github.com/dolanor/caldav-go/icalendar/values/duration.go new file mode 100644 index 0000000..9900490 --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/icalendar/values/duration.go @@ -0,0 +1,132 @@ +package values + +import ( + "fmt" + "github.com/dolanor/caldav-go/utils" + "log" + "math" + "regexp" + "strconv" + "strings" + "time" +) + +var _ = log.Print + +// a representation of duration for iCalendar +type Duration struct { + d time.Duration +} + +// breaks apart the duration into its component time parts +func (d *Duration) Decompose() (weeks, days, hours, minutes, seconds int64) { + + // chip away at this + rem := time.Duration(math.Abs(float64(d.d))) + + div := time.Hour * 24 * 7 + weeks = int64(rem / div) + rem = rem % div + div = div / 7 + days = int64(rem / div) + rem = rem % div + div = div / 24 + hours = int64(rem / div) + rem = rem % div + div = div / 60 + minutes = int64(rem / div) + rem = rem % div + div = div / 60 + seconds = int64(rem / div) + + return + +} + +// returns the native golang duration +func (d *Duration) NativeDuration() time.Duration { + return d.d +} + +// returns true if the duration is negative +func (d *Duration) IsPast() bool { + return d.d < 0 +} + +// encodes the duration of time into iCalendar format +func (d *Duration) EncodeICalValue() (string, error) { + var parts []string + weeks, days, hours, minutes, seconds := d.Decompose() + if d.IsPast() { + parts = append(parts, "-") + } + parts = append(parts, "P") + if weeks > 0 { + parts = append(parts, fmt.Sprintf("%dW", weeks)) + } + if days > 0 { + parts = append(parts, fmt.Sprintf("%dD", days)) + } + if hours > 0 || minutes > 0 || seconds > 0 { + parts = append(parts, "T") + if hours > 0 { + parts = append(parts, fmt.Sprintf("%dH", hours)) + } + if minutes > 0 { + parts = append(parts, fmt.Sprintf("%dM", minutes)) + } + if seconds > 0 { + parts = append(parts, fmt.Sprintf("%dS", seconds)) + } + } + return strings.Join(parts, ""), nil +} + +var durationRegEx = regexp.MustCompile("(\\d+)(\\w)") + +// decodes the duration of time from iCalendar format +func (d *Duration) DecodeICalValue(value string) error { + var seconds int64 + var isPast = strings.HasPrefix(value, "-P") + var matches = durationRegEx.FindAllStringSubmatch(value, -1) + for _, match := range matches { + var multiplier int64 + ivalue, err := strconv.ParseInt(match[1], 10, 64) + if err != nil { + return utils.NewError(d.DecodeICalValue, "unable to decode duration value "+match[1], d, nil) + } + switch match[2] { + case "S": + multiplier = 1 + case "M": + multiplier = 60 + case "H": + multiplier = 60 * 60 + case "D": + multiplier = 60 * 60 * 24 + case "W": + multiplier = 60 * 60 * 24 * 7 + default: + return utils.NewError(d.DecodeICalValue, "unable to decode duration segment "+match[2], d, nil) + } + seconds = seconds + multiplier*ivalue + } + d.d = time.Duration(seconds) * time.Second + if isPast { + d.d = -d.d + } + return nil +} + +func (d *Duration) String() string { + if s, err := d.EncodeICalValue(); err != nil { + panic(err) + } else { + return s + } +} + +// creates a new iCalendar duration representation +func NewDuration(d time.Duration) *Duration { + return &Duration{d: d} +} diff --git a/vendor/github.com/dolanor/caldav-go/icalendar/values/event_access_classification.go b/vendor/github.com/dolanor/caldav-go/icalendar/values/event_access_classification.go new file mode 100644 index 0000000..5701320 --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/icalendar/values/event_access_classification.go @@ -0,0 +1,17 @@ +package values + +// An access classification is only one component of the general security system within a calendar application. +// It provides a method of capturing the scope of the access the calendar owner intends for information within an +// individual calendar entry. The access classification of an individual iCalendar component is useful when measured +// along with the other security components of a calendar system (e.g., calendar user authentication, authorization, +// access rights, access role, etc.). Hence, the semantics of the individual access classifications cannot be completely +// defined by this memo alone. Additionally, due to the "blind" nature of most exchange processes using this memo, these +// access classifications cannot serve as an enforcement statement for a system receiving an iCalendar object. Rather, +// they provide a method for capturing the intention of the calendar owner for the access to the calendar component. +type EventAccessClassification string + +const ( + PublicEventAccessClassification EventAccessClassification = "PUBLIC" + PrivateEventAccessClassification = "PRIVATE" + ConfidentialEventAccessClassification = "CONFIDENTIAL" +) diff --git a/vendor/github.com/dolanor/caldav-go/icalendar/values/event_status.go b/vendor/github.com/dolanor/caldav-go/icalendar/values/event_status.go new file mode 100644 index 0000000..0419563 --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/icalendar/values/event_status.go @@ -0,0 +1,13 @@ +package values + +// In a group scheduled calendar component, the property is used by the "Organizer" to provide a confirmation of the +// event to the "Attendees". +// For example in an Event calendar component, the "Organizer" can indicate that a meeting is tentative, confirmed or +// cancelled. +type EventStatus string + +const ( + TentativeEventStatus EventStatus = "TENTATIVE" // Indicates event is tentative. + ConfirmedEventStatus = "CONFIRMED" // Indicates event is definite. + CancelledEventStatus = "CANCELLED" // Indicates event is cancelled. +) diff --git a/vendor/github.com/dolanor/caldav-go/icalendar/values/geo.go b/vendor/github.com/dolanor/caldav-go/icalendar/values/geo.go new file mode 100644 index 0000000..af44880 --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/icalendar/values/geo.go @@ -0,0 +1,69 @@ +package values + +import ( + "fmt" + "github.com/dolanor/caldav-go/utils" + "log" + "strconv" + "strings" +) + +var _ = log.Print + +// a representation of a geographical point for iCalendar +type Geo struct { + coords []float64 +} + +// creates a new icalendar geo representation +func NewGeo(lat, lng float64) *Geo { + return &Geo{coords: []float64{lat, lng}} +} + +// returns the latitude encoded into the geo point +func (g *Geo) Lat() float64 { + return g.coords[0] +} + +// returns the longitude encoded into the geo point +func (g *Geo) Lng() float64 { + return g.coords[1] +} + +// validates the geo value against the iCalendar specification +func (g *Geo) ValidateICalValue() error { + + if len(g.coords) != 2 { + return utils.NewError(g.ValidateICalValue, "geo value must have length of 2", g, nil) + } + + if g.Lat() < -90 || g.Lat() > 90 { + return utils.NewError(g.ValidateICalValue, "geo latitude must be between -90 and 90 degrees", g, nil) + } + + if g.Lng() < -180 || g.Lng() > 180 { + return utils.NewError(g.ValidateICalValue, "geo longitude must be between -180 and 180 degrees", g, nil) + } + + return nil + +} + +// encodes the geo value for the iCalendar specification +func (g *Geo) EncodeICalValue() (string, error) { + return fmt.Sprintf("%f %f", g.Lat(), g.Lng()), nil +} + +// decodes the geo value from the iCalendar specification +func (g *Geo) DecodeICalValue(value string) error { + if latlng := strings.Split(value, " "); len(latlng) < 2 { + return utils.NewError(g.DecodeICalValue, "geo value must have both a latitude and longitude component", g, nil) + } else if lat, err := strconv.ParseFloat(latlng[0], 64); err != nil { + return utils.NewError(g.DecodeICalValue, "unable to decode latitude component", g, err) + } else if lng, err := strconv.ParseFloat(latlng[1], 64); err != nil { + return utils.NewError(g.DecodeICalValue, "unable to decode latitude component", g, err) + } else { + *g = Geo{coords: []float64{lat, lng}} + return nil + } +} diff --git a/vendor/github.com/dolanor/caldav-go/icalendar/values/location.go b/vendor/github.com/dolanor/caldav-go/icalendar/values/location.go new file mode 100644 index 0000000..e2d06e9 --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/icalendar/values/location.go @@ -0,0 +1,78 @@ +package values + +import ( + "github.com/dolanor/caldav-go/icalendar/properties" + "github.com/dolanor/caldav-go/utils" + "log" + "net/url" +) + +var _ = log.Print + +// Specific venues such as conference or meeting rooms may be explicitly specified using this property. An alternate +// representation may be specified that is a URI that points to directory information with more structured specification +// of the location. For example, the alternate representation may specify either an LDAP URI pointing to an LDAP server +// entry or a CID URI pointing to a MIME body part containing a vCard [RFC 2426] for the location. +type Location struct { + value string + altrep *url.URL +} + +// creates a new icalendar location representation +func NewLocation(value string, altrep ...*url.URL) *Location { + loc := &Location{value: value} + if len(altrep) > 0 { + loc.altrep = altrep[0] + } + return loc +} + +// returns an alternate representation for the location +// if one exists +func (l *Location) AltRep() *url.URL { + return l.altrep +} + +// encodes the location for the iCalendar specification +func (l *Location) EncodeICalValue() (string, error) { + return l.value, nil +} + +// decodes the location from the iCalendar specification +func (l *Location) DecodeICalValue(value string) error { + l.value = value + return nil +} + +// encodes the location params for the iCalendar specification +func (l *Location) EncodeICalParams() (params properties.Params, err error) { + if l.altrep != nil { + params = properties.Params{properties.AlternateRepresentationName: l.altrep.String()} + } + return +} + +// decodes the location params from the iCalendar specification +func (l *Location) DecodeICalParams(params properties.Params) error { + if rep, found := params[properties.AlternateRepresentationName]; !found { + return nil + } else if altrep, err := url.Parse(rep); err != nil { + return utils.NewError(l.DecodeICalValue, "unable to parse alternate representation", l, err) + } else { + l.altrep = altrep + return nil + } +} + +// validates the location against the iCalendar specification +func (l *Location) ValidateICalValue() error { + + if l.altrep != nil { + if _, err := url.Parse(l.altrep.String()); err != nil { + msg := "location alternate representation must be a valid url" + return utils.NewError(l.ValidateICalValue, msg, l, err) + } + } + + return nil +} diff --git a/vendor/github.com/dolanor/caldav-go/icalendar/values/method.go b/vendor/github.com/dolanor/caldav-go/icalendar/values/method.go new file mode 100644 index 0000000..862da7e --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/icalendar/values/method.go @@ -0,0 +1,7 @@ +package values + +type Method string + +const ( + PublishMethod Method = "PUBLISH" +) diff --git a/vendor/github.com/dolanor/caldav-go/icalendar/values/recurrence_rule.go b/vendor/github.com/dolanor/caldav-go/icalendar/values/recurrence_rule.go new file mode 100644 index 0000000..3e96e3a --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/icalendar/values/recurrence_rule.go @@ -0,0 +1,434 @@ +package values + +import ( + "fmt" + "github.com/dolanor/caldav-go/icalendar/properties" + "github.com/dolanor/caldav-go/utils" + "log" + "regexp" + "strconv" + "strings" +) + +// The recurrence rule, if specified, is used in computing the recurrence set. The recurrence set is the complete set +// of recurrence instances for a calendar component. The recurrence set is generated by considering the initial +// "DTSTART" property along with the "RRULE", "RDATE", "EXDATE" and "EXRULE" properties contained within the iCalendar +// object. The "DTSTART" property defines the first instance in the recurrence set. Multiple instances of the "RRULE" +// and "EXRULE" properties can also be specified to define more sophisticated recurrence sets. The final recurrence +// set is generated by gathering all of the start date/times generated by any of the specified "RRULE" and "RDATE" +// properties, and excluding any start date/times which fall within the union of start date/times generated by any +// specified "EXRULE" and "EXDATE" properties. This implies that start date/times within exclusion related properties +// (i.e., "EXDATE" and "EXRULE") take precedence over those specified by inclusion properties +// (i.e., "RDATE" and "RRULE"). Where duplicate instances are generated by the "RRULE" and "RDATE" properties, only +// one recurrence is considered. Duplicate instances are ignored. + +// The "DTSTART" and "DTEND" property pair or "DTSTART" and "DURATION" property pair, specified within the iCalendar +// object defines the first instance of the recurrence. When used with a recurrence rule, the "DTSTART" and "DTEND" +// properties MUST be specified in local time and the appropriate set of "VTIMEZONE" calendar components MUST be +// included. For detail on the usage of the "VTIMEZONE" calendar component, see the "VTIMEZONE" calendar component +// definition. + +// Any duration associated with the iCalendar object applies to all members of the generated recurrence set. Any +// modified duration for specific recurrences MUST be explicitly specified using the "RDATE" property. +type RecurrenceRule struct { + Frequency RecurrenceFrequency + Until *DateTime + Count int + Interval int + BySecond []int + ByMinute []int + ByHour []int + ByDay []RecurrenceWeekday + ByMonthDay []int + ByYearDay []int + ByWeekNumber []int + ByMonth []int + BySetPosition []int + WeekStart RecurrenceWeekday +} + +var _ = log.Print + +// the frequency an event recurs +type RecurrenceFrequency string + +const ( + SecondRecurrenceFrequency RecurrenceFrequency = "SECONDLY" + MinuteRecurrenceFrequency = "MINUTELY" + HourRecurrenceFrequency = "HOURLY" + DayRecurrenceFrequency = "DAILY" + WeekRecurrenceFrequency = "WEEKLY" + MonthRecurrenceFrequency = "MONTHLY" + YearRecurrenceFrequency = "YEARLY" +) + +// the frequency an event recurs +type RecurrenceWeekday string + +const ( + MondayRecurrenceWeekday RecurrenceWeekday = "MO" + TuesdayRecurrenceWeekday = "TU" + WednesdayRecurrenceWeekday = "WE" + ThursdayRecurrenceWeekday = "TH" + FridayRecurrenceWeekday = "FR" + SaturdayRecurrenceWeekday = "SA" + SundayRecurrenceWeekday = "SU" +) + +// creates a new recurrence rule object for iCalendar +func NewRecurrenceRule(frequency RecurrenceFrequency) *RecurrenceRule { + return &RecurrenceRule{Frequency: frequency} +} + +var weekdayRegExp = regexp.MustCompile("MO|TU|WE|TH|FR|SA|SU") + +// returns true if weekday is a valid constant +func (r RecurrenceWeekday) IsValidWeekDay() bool { + return weekdayRegExp.MatchString(strings.ToUpper(string(r))) +} + +var frequencyRegExp = regexp.MustCompile("SECONDLY|MINUTELY|HOURLY|DAILY|WEEKLY|MONTHLY|YEARLY") + +// returns true if weekday is a valid constant +func (r RecurrenceFrequency) IsValidFrequency() bool { + return frequencyRegExp.MatchString(strings.ToUpper(string(r))) +} + +// returns the recurrence rule name for the iCalendar specification +func (r *RecurrenceRule) EncodeICalName() (properties.PropertyName, error) { + return properties.RecurrenceRulePropertyName, nil +} + +// encodes the recurrence rule value for the iCalendar specification +func (r *RecurrenceRule) EncodeICalValue() (string, error) { + + out := []string{fmt.Sprintf("FREQ=%s", strings.ToUpper(string(r.Frequency)))} + + if r.Until != nil { + if encoded, err := r.Until.EncodeICalValue(); err != nil { + return "", utils.NewError(r.EncodeICalValue, "unable to encode until date", r, err) + } else { + out = append(out, fmt.Sprintf("UNTIL=%s", encoded)) + } + } + + if r.Count > 0 { + out = append(out, fmt.Sprintf("COUNT=%d", r.Count)) + } + + if r.Interval > 0 { + out = append(out, fmt.Sprintf("INTERVAL=%d", r.Interval)) + } + + if len(r.BySecond) > 0 { + if encoded, err := intsToCSV(r.BySecond); err != nil { + return "", utils.NewError(r.EncodeICalValue, "unable to encode by second value", r, err) + } else { + out = append(out, fmt.Sprintf("BYSECOND=%s", encoded)) + } + } + + if len(r.ByMinute) > 0 { + if encoded, err := intsToCSV(r.ByMinute); err != nil { + return "", utils.NewError(r.EncodeICalValue, "unable to encode by minute value", r, err) + } else { + out = append(out, fmt.Sprintf("BYMINUTE=%s", encoded)) + } + } + + if len(r.ByHour) > 0 { + if encoded, err := intsToCSV(r.ByHour); err != nil { + return "", utils.NewError(r.EncodeICalValue, "unable to encode by hour value", r, err) + } else { + out = append(out, fmt.Sprintf("BYHOUR=%s", encoded)) + } + } + + if len(r.ByDay) > 0 { + if encoded, err := daysToCSV(r.ByDay); err != nil { + return "", utils.NewError(r.EncodeICalValue, "unable to encode by day value", r, err) + } else { + out = append(out, fmt.Sprintf("BYDAY=%s", encoded)) + } + } + + if len(r.ByMonthDay) > 0 { + if encoded, err := intsToCSV(r.ByMonthDay); err != nil { + return "", utils.NewError(r.EncodeICalValue, "unable to encode by month day value", r, err) + } else { + out = append(out, fmt.Sprintf("BYMONTHDAY=%s", encoded)) + } + } + + if len(r.ByYearDay) > 0 { + if encoded, err := intsToCSV(r.ByYearDay); err != nil { + return "", utils.NewError(r.EncodeICalValue, "unable to encode by year day value", r, err) + } else { + out = append(out, fmt.Sprintf("BYYEARDAY=%s", encoded)) + } + } + + if len(r.ByWeekNumber) > 0 { + if encoded, err := intsToCSV(r.ByWeekNumber); err != nil { + return "", utils.NewError(r.EncodeICalValue, "unable to encode by week number value", r, err) + } else { + out = append(out, fmt.Sprintf("BYWEEKNO=%s", encoded)) + } + } + + if len(r.ByMonth) > 0 { + if encoded, err := intsToCSV(r.ByMonth); err != nil { + return "", utils.NewError(r.EncodeICalValue, "unable to encode by month value", r, err) + } else { + out = append(out, fmt.Sprintf("BYMONTH=%s", encoded)) + } + } + + if len(r.BySetPosition) > 0 { + if encoded, err := intsToCSV(r.BySetPosition); err != nil { + return "", utils.NewError(r.EncodeICalValue, "unable to encode by set position value", r, err) + } else { + out = append(out, fmt.Sprintf("BYSETPOS=%s", encoded)) + } + } + + if r.WeekStart != "" { + out = append(out, fmt.Sprintf("WKST=%s", r.WeekStart)) + } + + return strings.Join(out, ";"), nil +} + +var rruleParamRegExp = regexp.MustCompile("(\\w+)\\s*=\\s*([^;]+)") + +// decodes the recurrence rule value from the iCalendar specification +func (r *RecurrenceRule) DecodeICalValue(value string) error { + + matches := rruleParamRegExp.FindAllStringSubmatch(value, -1) + if len(matches) <= 0 { + return utils.NewError(r.DecodeICalValue, "no recurrence rules found", r, nil) + } + + for _, match := range matches { + if err := r.decodeICalValue(match[1], match[2]); err != nil { + msg := fmt.Sprintf("unable to decode %s value", match[1]) + return utils.NewError(r.DecodeICalValue, msg, r, err) + } + } + + return nil + +} + +func (r *RecurrenceRule) decodeICalValue(name string, value string) error { + + switch name { + case "FREQ": + r.Frequency = RecurrenceFrequency(value) + case "UNTIL": + until := new(DateTime) + if err := until.DecodeICalValue(value); err != nil { + return utils.NewError(r.decodeICalValue, "invalid until value "+value, r, err) + } else { + r.Until = until + } + case "COUNT": + if count, err := strconv.ParseInt(value, 10, 64); err != nil { + return utils.NewError(r.decodeICalValue, "invalid count value "+value, r, err) + } else { + r.Count = int(count) + } + case "INTERVAL": + if interval, err := strconv.ParseInt(value, 10, 64); err != nil { + return utils.NewError(r.decodeICalValue, "invalid interval value "+value, r, err) + } else { + r.Interval = int(interval) + } + case "BYSECOND": + if ints, err := csvToInts(value); err != nil { + return utils.NewError(r.decodeICalValue, "invalid by second value "+value, r, err) + } else { + r.BySecond = ints + } + case "BYMINUTE": + if ints, err := csvToInts(value); err != nil { + return utils.NewError(r.decodeICalValue, "invalid by minute value "+value, r, err) + } else { + r.ByMinute = ints + } + case "BYHOUR": + if ints, err := csvToInts(value); err != nil { + return utils.NewError(r.decodeICalValue, "invalid by hour value "+value, r, err) + } else { + r.ByHour = ints + } + case "BYDAY": + if days, err := csvToDays(value); err != nil { + return utils.NewError(r.decodeICalValue, "invalid by day value "+value, r, err) + } else { + r.ByDay = days + } + case "BYMONTHDAY": + if ints, err := csvToInts(value); err != nil { + return utils.NewError(r.decodeICalValue, "invalid by month day value "+value, r, err) + } else { + r.ByMonthDay = ints + } + case "BYYEARDAY": + if ints, err := csvToInts(value); err != nil { + return utils.NewError(r.decodeICalValue, "invalid by year day value "+value, r, err) + } else { + r.ByYearDay = ints + } + case "BYWEEKNO": + if ints, err := csvToInts(value); err != nil { + return utils.NewError(r.decodeICalValue, "invalid by week number value "+value, r, err) + } else { + r.ByWeekNumber = ints + } + case "BYMONTH": + if ints, err := csvToInts(value); err != nil { + return utils.NewError(r.decodeICalValue, "unable to encode by month value "+value, r, err) + } else { + r.ByMonth = ints + } + case "BYSETPOS": + if ints, err := csvToInts(value); err != nil { + return utils.NewError(r.decodeICalValue, "unable to encode by set position value "+value, r, err) + } else { + r.BySetPosition = ints + } + case "WKST": + r.WeekStart = RecurrenceWeekday(value) + } + + return nil + +} + +// validates the recurrence rule value against the iCalendar specification +func (r *RecurrenceRule) ValidateICalValue() error { + if !r.Frequency.IsValidFrequency() { + return utils.NewError(r.ValidateICalValue, "a frequency is required in all recurrence rules", r, nil) + } else if r.Until != nil && r.Count > 0 { + return utils.NewError(r.ValidateICalValue, "until and count values are mutually exclusive", r, nil) + } else if found, fine := intsInRange(r.BySecond, 59); !fine { + msg := fmt.Sprintf("by second value of %d is out of bounds", found) + return utils.NewError(r.ValidateICalValue, msg, r, nil) + } else if found, fine := intsInRange(r.ByMinute, 59); !fine { + msg := fmt.Sprintf("by minute value of %d is out of bounds", found) + return utils.NewError(r.ValidateICalValue, msg, r, nil) + } else if found, fine := intsInRange(r.ByHour, 23); !fine { + msg := fmt.Sprintf("by hour value of %d is out of bounds", found) + return utils.NewError(r.ValidateICalValue, msg, r, nil) + } else if err := daysInRange(r.ByDay); err != nil { + return utils.NewError(r.ValidateICalValue, "by day value not in range", r, err) + } else if found, fine := intsInRange(r.ByMonthDay, 31); !fine { + msg := fmt.Sprintf("by month day value of %d is out of bounds", found) + return utils.NewError(r.ValidateICalValue, msg, r, nil) + } else if found, fine := intsInRange(r.ByYearDay, 366); !fine { + msg := fmt.Sprintf("by year day value of %d is out of bounds", found) + return utils.NewError(r.ValidateICalValue, msg, r, nil) + } else if found, fine := intsInRange(r.ByMonth, 12); !fine { + msg := fmt.Sprintf("by month value of %d is out of bounds", found) + return utils.NewError(r.ValidateICalValue, msg, r, nil) + } else if found, fine := intsInRange(r.BySetPosition, 366); !fine { + msg := fmt.Sprintf("by month value of %d is out of bounds", found) + return utils.NewError(r.ValidateICalValue, msg, r, nil) + } else if err := dayInRange(r.WeekStart); r.WeekStart != "" && err != nil { + return utils.NewError(r.ValidateICalValue, "week start value not in range", r, err) + } else { + return nil + } +} + +func intsToCSV(ints []int) (string, error) { + csv := new(CSV) + for _, i := range ints { + *csv = append(*csv, fmt.Sprintf("%d", i)) + } + return csv.EncodeICalValue() +} + +func csvToInts(value string) (ints []int, err error) { + csv := new(CSV) + if ierr := csv.DecodeICalValue(value); err != nil { + err = utils.NewError(csvToInts, "unable to decode CSV value", value, ierr) + return + } + for _, v := range *csv { + if i, ierr := strconv.ParseInt(v, 10, 64); err != nil { + err = utils.NewError(csvToInts, "unable to parse int value "+v, value, ierr) + return + } else { + ints = append(ints, int(i)) + } + } + return +} + +func intsInRange(ints []int, max int) (int, bool) { + for _, i := range ints { + if i < -max || i > max { + return i, false + } + } + return 0, true +} + +func daysInRange(days []RecurrenceWeekday) error { + for _, day := range days { + if err := dayInRange(day); err != nil { + msg := fmt.Sprintf("day value %s is not in range", day) + return utils.NewError(dayInRange, msg, days, err) + } + } + return nil +} + +var dayRegExp = regexp.MustCompile("(\\d{1,2})?(\\w{2})") + +func dayInRange(day RecurrenceWeekday) error { + var ordinal, weekday string + if matches := dayRegExp.FindAllStringSubmatch(string(day), -1); len(matches) <= 0 { + msg := fmt.Sprintf("weekday value %s is not in valid format", day) + return utils.NewError(dayInRange, msg, day, nil) + } else if len(matches[0]) > 2 { + ordinal = matches[0][1] + weekday = matches[0][2] + } else { + weekday = matches[0][1] + } + if !RecurrenceWeekday(weekday).IsValidWeekDay() { + msg := fmt.Sprintf("weekday value %s is not valid", weekday) + return utils.NewError(dayInRange, msg, day, nil) + } else if i, err := strconv.ParseInt(ordinal, 10, 64); ordinal != "" && err != nil { + msg := fmt.Sprintf("weekday ordinal value %d is not valid", i) + return utils.NewError(dayInRange, msg, day, err) + } else if i < -53 || i > 53 { + msg := fmt.Sprintf("weekday ordinal value %d is not in range", i) + return utils.NewError(dayInRange, msg, day, nil) + } else { + return nil + } +} + +func daysToCSV(days []RecurrenceWeekday) (string, error) { + csv := new(CSV) + for _, day := range days { + *csv = append(*csv, strings.ToUpper(string(day))) + } + return csv.EncodeICalValue() +} + +func csvToDays(value string) (days []RecurrenceWeekday, err error) { + csv := new(CSV) + if ierr := csv.DecodeICalValue(value); err != nil { + err = utils.NewError(csvToInts, "unable to decode CSV value", value, ierr) + return + } + for _, v := range *csv { + days = append(days, RecurrenceWeekday(v)) + } + return +} diff --git a/vendor/github.com/dolanor/caldav-go/icalendar/values/time_transparency.go b/vendor/github.com/dolanor/caldav-go/icalendar/values/time_transparency.go new file mode 100644 index 0000000..7d347a7 --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/icalendar/values/time_transparency.go @@ -0,0 +1,12 @@ +package values + +// Time Transparency is the characteristic of an event that determines whether it appears to consume time on a calendar. +// Events that consume actual time for the individual or resource associated with the calendar SHOULD be recorded as +// OPAQUE, allowing them to be detected by free-busy time searches. Other events, which do not take up the individual's +// (or resource's) time SHOULD be recorded as TRANSPARENT, making them invisible to free-busy time searches. +type TimeTransparency string + +const ( + OpaqueTimeTransparency TimeTransparency = "OPAQUE" // Blocks or opaque on busy time searches. DEFAULT + TransparentTimeTransparency = "TRANSPARENT" // Transparent on busy time searches. +) diff --git a/vendor/github.com/dolanor/caldav-go/icalendar/values/url.go b/vendor/github.com/dolanor/caldav-go/icalendar/values/url.go new file mode 100644 index 0000000..1bc3080 --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/icalendar/values/url.go @@ -0,0 +1,49 @@ +package values + +import ( + "github.com/dolanor/caldav-go/icalendar/properties" + "github.com/dolanor/caldav-go/utils" + "net/url" +) + +// a representation of duration for iCalendar +type Url struct { + u url.URL +} + +// encodes the URL into iCalendar format +func (u *Url) EncodeICalValue() (string, error) { + return u.u.String(), nil +} + +// encodes the url params for the iCalendar specification +func (u *Url) EncodeICalParams() (params properties.Params, err error) { + params = properties.Params{ + properties.ValuePropertyName: "URI", + } + return +} + +// decodes the URL from iCalendar format +func (u *Url) DecodeICalValue(value string) error { + if parsed, err := url.Parse(value); err != nil { + return utils.NewError(u.ValidateICalValue, "unable to parse url", u, err) + } else { + u.u = *parsed + return nil + } +} + +// validates the URL for iCalendar format +func (u *Url) ValidateICalValue() error { + if _, err := url.Parse(u.u.String()); err != nil { + return utils.NewError(u.ValidateICalValue, "invalid URL object", u, err) + } else { + return nil + } +} + +// creates a new iCalendar duration representation +func NewUrl(u url.URL) *Url { + return &Url{u: u} +} diff --git a/vendor/github.com/dolanor/caldav-go/utils/error.go b/vendor/github.com/dolanor/caldav-go/utils/error.go new file mode 100644 index 0000000..0c4c145 --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/utils/error.go @@ -0,0 +1,37 @@ +package utils + +import ( + "fmt" + "reflect" + "runtime" +) + +type Error struct { + method interface{} + message string + context interface{} + cause error +} + +func NewError(method interface{}, message string, context interface{}, cause error) *Error { + e := new(Error) + e.method = method + e.message = message + e.context = context + e.cause = cause + return e +} + +func (e *Error) Error() string { + pc := reflect.ValueOf(e.method).Pointer() + fn := runtime.FuncForPC(pc).Name() + msg := fmt.Sprintf("error: %s\nfunc: %s", e.message, fn) + if e.context != nil { + tname := reflect.ValueOf(e.context).Type() + msg = fmt.Sprintf("%s\ncontext: %s", msg, tname.String()) + } + if e.cause != nil { + msg = fmt.Sprintf("%s\ncause: %s", msg, e.cause.Error()) + } + return msg +} diff --git a/vendor/github.com/dolanor/caldav-go/webdav/client.go b/vendor/github.com/dolanor/caldav-go/webdav/client.go new file mode 100644 index 0000000..72dfe6a --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/webdav/client.go @@ -0,0 +1,119 @@ +package webdav + +import ( + "fmt" + "github.com/dolanor/caldav-go/http" + "github.com/dolanor/caldav-go/utils" + "github.com/dolanor/caldav-go/webdav/entities" + nhttp "net/http" +) + +const ( + StatusMulti = 207 +) + +// a client for making WebDAV requests +type Client http.Client + +// downcasts the client to the local HTTP interface +func (c *Client) Http() *http.Client { + return (*http.Client)(c) +} + +// returns the embedded WebDav server reference +func (c *Client) Server() *Server { + return (*Server)(c.Http().Server()) +} + +// executes a WebDAV request +func (c *Client) Do(req *Request) (*Response, error) { + if resp, err := c.Http().Do((*http.Request)(req)); err != nil { + return nil, utils.NewError(c.Do, "unable to execute WebDAV request", c, err) + } else { + return NewResponse(resp), nil + } +} + +// checks if a resource exists given a particular path +func (c *Client) Exists(path string) (bool, error) { + if req, err := c.Server().NewRequest("HEAD", path); err != nil { + return false, utils.NewError(c.Exists, "unable to create request", c, err) + } else if resp, err := c.Do(req); err != nil { + return false, utils.NewError(c.Exists, "unable to execute request", c, err) + } else { + return resp.StatusCode != nhttp.StatusNotFound, nil + } +} + +// deletes a resource if it exists on a particular path +func (c *Client) Delete(path string) error { + if req, err := c.Server().NewRequest("DELETE", path); err != nil { + return utils.NewError(c.Delete, "unable to create request", c, err) + } else if resp, err := c.Do(req); err != nil { + return utils.NewError(c.Delete, "unable to execute request", c, err) + } else if resp.StatusCode != nhttp.StatusNoContent && resp.StatusCode != nhttp.StatusNotFound { + err := new(entities.Error) + resp.Decode(err) + msg := fmt.Sprintf("unexpected server response %s", resp.Status) + return utils.NewError(c.Delete, msg, c, err) + } else { + return nil + } +} + +// fetches a list of WebDAV features supported by the server +// returns an error if the server does not support DAV +func (c *Client) Features(path string) ([]string, error) { + if req, err := c.Server().NewRequest("OPTIONS", path); err != nil { + return []string{}, utils.NewError(c.Features, "unable to create request", c, err) + } else if resp, err := c.Do(req); err != nil { + return []string{}, utils.NewError(c.Features, "unable to execute request", c, err) + } else { + return resp.Features(), nil + } +} + +// returns an error if the server does not support WebDAV +func (c *Client) ValidateServer(path string) error { + if features, err := c.Features(path); err != nil { + return utils.NewError(c.ValidateServer, "feature detection failed", c, err) + } else if len(features) <= 0 { + return utils.NewError(c.ValidateServer, "no DAV headers found", c, err) + } else { + return nil + } +} + +// executes a PROPFIND request against the WebDAV server +// returns a multistatus XML entity +func (c *Client) Propfind(path string, depth Depth, pf *entities.Propfind) (*entities.Multistatus, error) { + + ms := new(entities.Multistatus) + + if req, err := c.Server().NewRequest("PROPFIND", path, pf); err != nil { + return nil, utils.NewError(c.Propfind, "unable to create request", c, err) + } else if req.Http().Native().Header.Set("Depth", string(depth)); depth == "" { + return nil, utils.NewError(c.Propfind, "search depth must be defined", c, nil) + } else if resp, err := c.Do(req); err != nil { + return nil, utils.NewError(c.Propfind, "unable to execute request", c, err) + } else if resp.StatusCode != StatusMulti { + msg := fmt.Sprintf("unexpected status: %s", resp.Status) + return nil, utils.NewError(c.Propfind, msg, c, nil) + } else if err := resp.Decode(ms); err != nil { + return nil, utils.NewError(c.Propfind, "unable to decode response", c, err) + } + + return ms, nil + +} + +// creates a new client for communicating with an WebDAV server +func NewClient(server *Server, native *nhttp.Client) *Client { + return (*Client)(http.NewClient((*http.Server)(server), native)) +} + +// creates a new client for communicating with a WebDAV server +// uses the default HTTP client from net/http +func NewDefaultClient(server *Server) *Client { + return NewClient(server, nhttp.DefaultClient) +} diff --git a/vendor/github.com/dolanor/caldav-go/webdav/depth.go b/vendor/github.com/dolanor/caldav-go/webdav/depth.go new file mode 100644 index 0000000..20637bb --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/webdav/depth.go @@ -0,0 +1,9 @@ +package webdav + +type Depth string + +const ( + Depth0 Depth = "0" + Depth1 = "1" + DepthInfinity = "infinity" +) diff --git a/vendor/github.com/dolanor/caldav-go/webdav/entities/error.go b/vendor/github.com/dolanor/caldav-go/webdav/entities/error.go new file mode 100644 index 0000000..f1be786 --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/webdav/entities/error.go @@ -0,0 +1,18 @@ +package entities + +import "encoding/xml" + +// a WebDAV error +type Error struct { + XMLName xml.Name `xml:"DAV: error"` + Description string `xml:"error-description,omitempty"` + Message string `xml:"message,omitempty"` +} + +func (e *Error) Error() string { + if e.Description != "" { + return e.Description + } else { + return e.Message + } +} diff --git a/vendor/github.com/dolanor/caldav-go/webdav/entities/multistatus.go b/vendor/github.com/dolanor/caldav-go/webdav/entities/multistatus.go new file mode 100644 index 0000000..512877e --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/webdav/entities/multistatus.go @@ -0,0 +1,23 @@ +package entities + +import "encoding/xml" + +// metadata about a property +type PropStat struct { + XMLName xml.Name `xml:"propstat"` + Status string `xml:"status"` + Prop *Prop `xml:",omitempty"` +} + +// a multistatus response entity +type Response struct { + XMLName xml.Name `xml:"response"` + Href string `xml:"href"` + PropStats []*PropStat `xml:"propstat,omitempty"` +} + +// a request to find properties on an an entity or collection +type Multistatus struct { + XMLName xml.Name `xml:"DAV: multistatus"` + Responses []*Response `xml:"response,omitempty"` +} diff --git a/vendor/github.com/dolanor/caldav-go/webdav/entities/prop.go b/vendor/github.com/dolanor/caldav-go/webdav/entities/prop.go new file mode 100644 index 0000000..0527fae --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/webdav/entities/prop.go @@ -0,0 +1,32 @@ +package entities + +import ( + "encoding/xml" +) + +// a property of a resource +type Prop struct { + XMLName xml.Name `xml:"DAV: prop"` + GetContentType string `xml:"getcontenttype,omitempty"` + DisplayName string `xml:"displayname,omitempty"` + ResourceType *ResourceType `xml:",omitempty"` + CTag string `xml:"http://calendarserver.org/ns/ getctag,omitempty"` + ETag string `xml:"http://calendarserver.org/ns/ getetag,omitempty"` +} + +// the type of a resource +type ResourceType struct { + XMLName xml.Name `xml:"resourcetype"` + Collection *ResourceTypeCollection `xml:",omitempty"` + Calendar *ResourceTypeCalendar `xml:",omitempty"` +} + +// A calendar resource type +type ResourceTypeCalendar struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav calendar"` +} + +// A collection resource type +type ResourceTypeCollection struct { + XMLName xml.Name `xml:"collection"` +} diff --git a/vendor/github.com/dolanor/caldav-go/webdav/entities/propfind.go b/vendor/github.com/dolanor/caldav-go/webdav/entities/propfind.go new file mode 100644 index 0000000..f21933f --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/webdav/entities/propfind.go @@ -0,0 +1,20 @@ +package entities + +import "encoding/xml" + +// a request to find properties on an an entity or collection +type Propfind struct { + XMLName xml.Name `xml:"DAV: propfind"` + AllProp *AllProp `xml:",omitempty"` + Props []*Prop `xml:"prop,omitempty"` +} + +// a propfind property representing all properties +type AllProp struct { + XMLName xml.Name `xml:"allprop"` +} + +// a convenience method for searching all properties +func NewAllPropsFind() *Propfind { + return &Propfind{AllProp: new(AllProp)} +} diff --git a/vendor/github.com/dolanor/caldav-go/webdav/request.go b/vendor/github.com/dolanor/caldav-go/webdav/request.go new file mode 100644 index 0000000..158d4c1 --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/webdav/request.go @@ -0,0 +1,56 @@ +package webdav + +import ( + "bytes" + "encoding/xml" + "github.com/dolanor/caldav-go/http" + "github.com/dolanor/caldav-go/utils" + "io" + "io/ioutil" + "log" + "strings" +) + +var _ = log.Print + +// an WebDAV request object +type Request http.Request + +// downcasts the request to the local HTTP interface +func (r *Request) Http() *http.Request { + return (*http.Request)(r) +} + +// creates a new WebDAV request object +func NewRequest(method string, urlstr string, xmldata ...interface{}) (*Request, error) { + if buffer, length, err := xmlToReadCloser(xmldata); err != nil { + return nil, utils.NewError(NewRequest, "unable to encode xml data", xmldata, err) + } else if r, err := http.NewRequest(method, urlstr, buffer); err != nil { + return nil, utils.NewError(NewRequest, "unable to create request", urlstr, err) + } else { + if buffer != nil { + // set the content type to XML if we have a body + r.Native().Header.Set("Content-Type", "text/xml; charset=UTF-8") + r.ContentLength = int64(length) + } + return (*Request)(r), nil + } +} + +func xmlToReadCloser(xmldata ...interface{}) (io.ReadCloser, int, error) { + var buffer []string + for _, xmldatum := range xmldata { + if encoded, err := xml.Marshal(xmldatum); err != nil { + return nil, 0, utils.NewError(xmlToReadCloser, "unable to encode as xml", xmldatum, err) + } else { + buffer = append(buffer, string(encoded)) + } + } + if len(buffer) > 0 { + var encoded = strings.Join(buffer, "\n") + // log.Printf("[WebDAV Request]\n%+v\n", encoded) + return ioutil.NopCloser(bytes.NewBuffer([]byte(encoded))), len(encoded), nil + } else { + return nil, 0, nil + } +} diff --git a/vendor/github.com/dolanor/caldav-go/webdav/response.go b/vendor/github.com/dolanor/caldav-go/webdav/response.go new file mode 100644 index 0000000..12fd8e5 --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/webdav/response.go @@ -0,0 +1,54 @@ +package webdav + +import ( + "encoding/xml" + "github.com/dolanor/caldav-go/http" + "github.com/dolanor/caldav-go/utils" + "io/ioutil" + "log" + "strings" +) + +var _ = log.Print +var _ = ioutil.ReadAll + +// a WebDAV response object +type Response http.Response + +// downcasts the response to the local HTTP interface +func (r *Response) Http() *http.Response { + return (*http.Response)(r) +} + +// returns a list of WebDAV features found in the response +func (r *Response) Features() (features []string) { + if dav := r.Header.Get("DAV"); dav != "" { + features = strings.Split(dav, ", ") + } + return +} + +// decodes a WebDAV XML response into the provided interface +func (r *Response) Decode(into interface{}) error { + // data, _ := ioutil.ReadAll(r.Body) + // log.Printf("[WebDAV Response]\n%+v\n", string(data)) + // if err := xml.Unmarshal(data, into); err != nil { + // return utils.NewError(r.Decode, "unable to decode response body", r, err) + // } else { + // return nil + // } + if body := r.Body; body == nil { + return nil + } else if decoder := xml.NewDecoder(body); decoder == nil { + return nil + } else if err := decoder.Decode(into); err != nil { + return utils.NewError(r.Decode, "unable to decode response body", r, err) + } else { + return nil + } +} + +// creates a new WebDAV response object +func NewResponse(response *http.Response) *Response { + return (*Response)(response) +} diff --git a/vendor/github.com/dolanor/caldav-go/webdav/server.go b/vendor/github.com/dolanor/caldav-go/webdav/server.go new file mode 100644 index 0000000..0b95c63 --- /dev/null +++ b/vendor/github.com/dolanor/caldav-go/webdav/server.go @@ -0,0 +1,28 @@ +package webdav + +import ( + "github.com/dolanor/caldav-go/http" + "github.com/dolanor/caldav-go/utils" +) + +// a server that accepts WebDAV requests +type Server http.Server + +// creates a reference to an WebDAV server +func NewServer(baseUrlStr string) (*Server, error) { + if s, err := http.NewServer(baseUrlStr); err != nil { + return nil, utils.NewError(NewServer, "unable to create WebDAV server", baseUrlStr, err) + } else { + return (*Server)(s), nil + } +} + +// downcasts the server to the local HTTP interface +func (s *Server) Http() *http.Server { + return (*http.Server)(s) +} + +// creates a new WebDAV request object +func (s *Server) NewRequest(method string, path string, xmldata ...interface{}) (*Request, error) { + return NewRequest(method, s.Http().AbsUrlStr(path), xmldata...) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 17e71c0..844b600 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -4,6 +4,19 @@ github.com/beorn7/perks/quantile # github.com/cespare/xxhash/v2 v2.1.2 ## explicit; go 1.11 github.com/cespare/xxhash/v2 +# github.com/dolanor/caldav-go v0.2.1 +## explicit; go 1.13 +github.com/dolanor/caldav-go/caldav +github.com/dolanor/caldav-go/caldav/entities +github.com/dolanor/caldav-go/caldav/values +github.com/dolanor/caldav-go/http +github.com/dolanor/caldav-go/icalendar +github.com/dolanor/caldav-go/icalendar/components +github.com/dolanor/caldav-go/icalendar/properties +github.com/dolanor/caldav-go/icalendar/values +github.com/dolanor/caldav-go/utils +github.com/dolanor/caldav-go/webdav +github.com/dolanor/caldav-go/webdav/entities # github.com/golang/protobuf v1.5.2 ## explicit; go 1.9 github.com/golang/protobuf/proto