commit 1d83b87c7c96431236d6c51ede2908dd0e30030b
parent 93f3f7ee6b4259c3b42b6cf698ecf5b15da97f17
Author: Andreas Gruhler <andreas.gruhler@adfinis.com>
Date: Wed, 27 Sep 2023 01:42:38 +0200
feat: send ics invite
Diffstat:
11 files changed, 337 insertions(+), 108 deletions(-)
diff --git a/Makefile b/Makefile
@@ -16,7 +16,7 @@ clean:
.PHONY: fmt
fmt:
- $(GOFMT) -w ***/*.go
+ $(GOFMT) -w -s .
.PHONY: test
test:
diff --git a/README.md b/README.md
@@ -50,6 +50,8 @@ The application is configurable through environment variables or the
#HIVEDAV_LISTEN_ADDRESS=127.0.0.1
#HIVEDAV_LISTEN_PORT=3737
#HIVEDAV_CALENDAR=0
+#HIVEDAV_BOOKING_SUBJ="HiveDAV Booking"
+#HIVEDAV_BOOKING_REMINDER=15
# Required
HIVEDAV_HOST=hivedav.example.com
@@ -70,6 +72,10 @@ There is an example config file provided in `./app.env.example`.
collection in the ["calendar home
set"](https://www.rfc-editor.org/rfc/rfc4791.html#section-6.2.1) which should
be used to read availability data
+* `HIVEDAV_BOOKING_SUBJ` is the subject of the calendar invite for new booking
+ requests
+* `HIVEDAV_BOOKING_REMINDER` is the reminder time in minutes before the start
+ of the calendar appointment
* `HIVEDAV_HOST` is the fqdn of the HiveDAV service. It is used to print the
help commands in the curlable interface.
* `HIVEDAV_CALDAV_*` are settings to connect to the CalDAV server for fetching
diff --git a/app.env.example b/app.env.example
@@ -6,6 +6,9 @@
#HIVEDAV_CALDAV_USER=
#HIVEDAV_CALDAV_PASSWORD=
#HIVEDAV_REFRESH_INTERVAL=30
-#HIVEDAV_SMTP_SERVER=
+#HIVEDAV_SMTP_HOST=
+#HIVEDAV_SMTP_PORT=587
#HIVEDAV_SMTP_USER=
#HIVEDAV_SMTP_PASSWORD=
+#HIVEDAV_BOOKING_SUBJ="HiveDAV Booking"
+#HIVEDAV_BOOKING_REMINDER=15
diff --git a/config/config.go b/config/config.go
@@ -14,9 +14,13 @@ type Config struct {
CaldavUser string `mapstructure:"HIVEDAV_CALDAV_USER"`
CaldavPassword string `mapstructure:"HIVEDAV_CALDAV_PASSWORD"`
RefreshInterval int `mapstructure:"HIVEDAV_REFRESH_INTERVAL"`
- SmtpServer string `mapstructure:"HIVEDAV_SMTP_SERVER"`
+ SmtpHost string `mapstructure:"HIVEDAV_SMTP_HOST"`
+ SmtpPort int `mapstructure:"HIVEDAV_SMTP_PORT"`
SmtpUser string `mapstructure:"HIVEDAV_SMTP_USER"`
SmtpPassword string `mapstructure:"HIVEDAV_SMTP_PASSWORD"`
+ BookingSummary string `mapstructure:"HIVEDAV_BOOKING_SUMMARY"`
+ BookingLocation string `mapstructure:"HIVEDAV_BOOKING_LOCATION"`
+ BookingReminder int `mapstructure:"HIVEDAV_BOOKING_REMINDER"`
}
func (c *Config) LoadConfig(path string) (*Config, error) {
@@ -32,6 +36,9 @@ func (c *Config) LoadConfig(path string) (*Config, error) {
viper.SetDefault("HIVEDAV_LISTEN_ADDRESS", "[::]")
viper.SetDefault("HIVEDAV_CALENDAR", 0)
viper.SetDefault("HIVEDAV_REFRESH_INTERVAL", 30)
+ viper.SetDefault("HIVEDAV_BOOKING_SUMMARY", "HiveDAV Booking")
+ viper.SetDefault("HIVEDAV_BOOKING_LOCATION", "https://meet.jit.si")
+ viper.SetDefault("HIVEDAV_BOOKING_REMINDER", 15)
err := viper.ReadInConfig()
if err != nil {
@@ -74,6 +81,27 @@ func (c *Config) LoadConfig(path string) (*Config, error) {
if viper.IsSet("REFRESH_INTERVAL") {
c.RefreshInterval = viper.GetInt("REFRESH_INTERVAL")
}
+ if viper.IsSet("HIVEDAV_SMTP_HOST") {
+ c.SmtpHost = viper.GetString("HIVEDAV_SMTP_HOST")
+ }
+ if viper.IsSet("HIVEDAV_SMTP_PORT") {
+ c.SmtpPort = viper.GetInt("HIVEDAV_SMTP_PORT")
+ }
+ if viper.IsSet("HIVEDAV_SMTP_USER") {
+ c.SmtpUser = viper.GetString("HIVEDAV_SMTP_USER")
+ }
+ if viper.IsSet("HIVEDAV_SMTP_PASSWORD") {
+ c.SmtpPassword = viper.GetString("HIVEDAV_SMTP_PASSWORD")
+ }
+ if viper.IsSet("HIVEDAV_BOOKING_SUMMARY") {
+ c.BookingSummary = viper.GetString("HIVEDAV_BOOKING_SUMMARY")
+ }
+ if viper.IsSet("HIVEDAV_BOOKING_LOCATION") {
+ c.BookingLocation = viper.GetString("HIVEDAV_BOOKING_LOCATION")
+ }
+ if viper.IsSet("HIVEDAV_BOOKING_REMINDER") {
+ c.BookingReminder = viper.GetInt("HIVEDAV_BOOKING_REMINDER")
+ }
return c, nil
}
diff --git a/go.mod b/go.mod
@@ -4,16 +4,18 @@ go 1.21.0
require (
github.com/emersion/go-ical v0.0.0-20220601085725-0864dccc089f
- github.com/in0rdr/go-webdav v0.0.0-20230903141941-cde95745290e
+ github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
+ github.com/julienschmidt/httprouter v1.3.0
github.com/mattn/go-sqlite3 v1.14.17
github.com/pquerna/termchalk v0.0.0-20140809212720-8cc5932700ba
github.com/spf13/viper v1.16.0
+ github.com/teambition/rrule-go v1.8.2
+ gopkg.in/robfig/cron.v2 v2.0.0-20150107220207-be2e0b0deed5
)
require (
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
- github.com/julienschmidt/httprouter v1.3.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
@@ -24,11 +26,9 @@ require (
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
- github.com/teambition/rrule-go v1.8.2 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
- gopkg.in/robfig/cron.v2 v2.0.0-20150107220207-be2e0b0deed5 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/go.sum b/go.sum
@@ -127,8 +127,8 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/in0rdr/go-webdav v0.0.0-20230903141941-cde95745290e h1:iXhjrrNT+r6E6HrXsUfFcYGWkG3rnOdqPHDxcvbt/9g=
-github.com/in0rdr/go-webdav v0.0.0-20230903141941-cde95745290e/go.mod h1:u8ZQQE8aW8NvpP101ttUsXcbtRKiedaHAPtsKAFpPnY=
+github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
+github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
@@ -185,7 +185,6 @@ github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gt
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
-github.com/teambition/rrule-go v1.7.2 h1:goEajFWYydfCgavn2m/3w5U+1b3PGqPUHx/fFSVfTy0=
github.com/teambition/rrule-go v1.7.2/go.mod h1:mBJ1Ht5uboJ6jexKdNUJg2NcwP8uUMNvStWXlJD3MvU=
github.com/teambition/rrule-go v1.8.2 h1:lIjpjvWTj9fFUZCmuoVDrKVOtdiyzbzc93qTmRVe/J8=
github.com/teambition/rrule-go v1.8.2/go.mod h1:Ieq5AbrKGciP1V//Wq8ktsTXwSwJHDD5mD/wLBGl3p4=
diff --git a/main.go b/main.go
@@ -27,10 +27,11 @@ func main() {
log.Fatal("Error creating server with config: ", err)
}
- log.Printf("Starting hivedav %s, %s 🍯\n", hivedavVersion, conf.HiveDavHost)
- log.Printf("Cron availability refresh interval is %d minutes ⏰\n", conf.RefreshInterval)
- log.Println("Reading availability from:", conf.CaldavUri)
- log.Println("Reading as user:", conf.CaldavUser)
+ log.Printf("HiveDAV %s, %s 🍯\n", hivedavVersion, conf.HiveDavHost)
+ log.Println("----------------------------------------------------")
+ log.Printf("Cron interval:\t%d minutes\n", conf.RefreshInterval)
+ log.Printf("CalDAV server:\t%s\n", conf.CaldavUri)
+ log.Printf("CalDAV user:\t%s\n", conf.CaldavUser)
c := cron.New()
c.AddFunc(fmt.Sprintf("@every %dm", conf.RefreshInterval), func() { updateDatabase(server, c) })
@@ -45,34 +46,33 @@ func main() {
router.POST("/book/:dtstart", server.BookCubicle)
router.ServeFiles("/css/*filepath", http.Dir("./css"))
router.GET("/healthz", server.Healthz)
- log.Printf("Listening on %s:%d\n", conf.ListenAddress, conf.ListenPort)
+ log.Printf("Listening:\t\t%s:%d\n", conf.ListenAddress, conf.ListenPort)
log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%d", conf.ListenAddress, conf.ListenPort), router))
}
func updateDatabase(s *Server, c *cron.Cron) {
- log.Println("Starting availability update 🔄")
-
// Find current user principal
userPrincipal, err := caldav.UserPrincipal(s.config.CaldavUri, s.config.CaldavUser, s.config.CaldavPassword)
if err != nil {
log.Fatal("Error reading current user principal: ", err)
}
- log.Println("User principal:", userPrincipal)
+ log.Printf("User principal:\t%s\n", userPrincipal)
// Find home set of current user principal
homeSetUri, err := caldav.CalendarHome(s.config.CaldavUri, s.config.CaldavUser, s.config.CaldavPassword, userPrincipal, s.config.Calendar)
if err != nil {
log.Fatal("Error reading home set URI: ", err)
}
- log.Println("Calendar homeset:", homeSetUri)
+ log.Printf("Calendar homeset:\t%s", homeSetUri)
// Find calendar URI for the calendar specified in the config
calendarUri, err := caldav.CalendarFromHomeSet(s.config.CaldavUri, s.config.CaldavUser, s.config.CaldavPassword, homeSetUri, s.config.Calendar)
if err != nil {
log.Fatal("Error reading calendar URI: ", err)
}
- log.Printf("Using calendar %d in homeset with URI '%s'\n", s.config.Calendar, calendarUri)
- log.Println("Fetching availability from CalDAV server, please wait.. ⏳")
+ log.Printf("Calendar %d:\t\t%s\n", s.config.Calendar, calendarUri)
+ log.Println("----------------------------------------------------")
+ log.Println("Fetching availability from CalDAV server, please wait..")
// Get availability data for the calendar specified in the config
calData, err := caldav.GetAvailability(s.config.CaldavUri, s.config.CaldavUser, s.config.CaldavPassword, calendarUri)
@@ -85,6 +85,7 @@ func updateDatabase(s *Server, c *cron.Cron) {
log.Fatalf("Error fetching availability: %v", err)
}
- log.Println("Ready. Database was initialized with latest CalDAV availability data.")
- log.Printf("Next automatic refresh of the db (cron): %v\n", c.Entries()[0].Schedule.Next(time.Now()).Format("2006-01-02 15:04"))
+ log.Println("Database initialized")
+ log.Printf("Next db refresh (cron): %v\n", c.Entries()[0].Schedule.Next(time.Now()).Format("2006-01-02 15:04"))
+ log.Println("----------------------------------------------------")
}
diff --git a/server.go b/server.go
@@ -1,9 +1,13 @@
package main
import (
+ "crypto/rand"
+ "crypto/tls"
"database/sql"
+ "encoding/hex"
"fmt"
"github.com/emersion/go-ical"
+ "github.com/jordan-wright/email"
"github.com/julienschmidt/httprouter"
_ "github.com/mattn/go-sqlite3"
"github.com/pquerna/termchalk/prettytable"
@@ -14,12 +18,13 @@ import (
"io"
"log"
"net/http"
+ netmail "net/mail"
+ "net/smtp"
+ "os"
"regexp"
"strconv"
"strings"
"time"
- "net/smtp"
- "bytes"
)
type Server struct {
@@ -28,11 +33,31 @@ type Server struct {
}
type TableData struct {
- TableHead []string
- Rows [][]string
- Week int
- Year int
- Version string
+ TableHead []string
+ Rows [][]string
+ Week int
+ Year int
+ Version string
+ HiveDavHost string
+}
+
+type IcsData struct {
+ Timestamp string
+ Prodid string
+ Uid string
+ Dtstart string
+ Dtend string
+ Timezone string
+ StandardTzOffsetFrom string
+ StandardTzOffsetTo string
+ DstTzOffsetFrom string
+ DstTzOffsetTo string
+ Summary string
+ Reminder int
+ Description string
+ Location string
+ Organizer string
+ Attendee string
}
var drop = `DROP TABLE IF EXISTS availability;
@@ -141,12 +166,6 @@ func (s *Server) available(start string, end string) (bool, error) {
}
func (s *Server) Week(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
- // TODO: make display timezone configurable, display in local time
- localLocation, err := time.LoadLocation("Local")
- if err != nil {
- return
- }
-
userAgent := req.Header.Get("user-agent")
timeNow := time.Now()
@@ -154,11 +173,11 @@ func (s *Server) Week(w http.ResponseWriter, req *http.Request, ps httprouter.Pa
year, week := timeNow.ISOWeek()
// overwrite with requested week
- week, err = strconv.Atoi(ps.ByName("week"))
+ week, err := strconv.Atoi(ps.ByName("week"))
if err != nil {
- log.Printf("Cannot serve requested week '%d'\n", week)
w.WriteHeader(http.StatusBadRequest)
- // TODO: Print human readable error to w
+ log.Printf("Cannot serve requested week '%d'\n", week)
+ fmt.Fprintf(w, "Cannot serve requested week '%d'\n", week)
return
}
@@ -186,16 +205,18 @@ func (s *Server) Week(w http.ResponseWriter, req *http.Request, ps httprouter.Pa
}
tableData := TableData{
- TableHead: tableHead,
- Rows: rows,
- Week: week,
- Year: year,
- Version: hivedavVersion,
+ TableHead: tableHead,
+ Rows: rows,
+ Week: week,
+ Year: year,
+ Version: hivedavVersion,
+ HiveDavHost: s.config.HiveDavHost,
}
// TODO: use timeIt to go through the loops below, remove the static arrays
- timeIt := time.Date(monday.Year(), monday.Month(), monday.Day(), 0, 0, 0, 0, localLocation)
- //dayEnd := time.Date(monday.Year(), monday.Month(), monday.Day(), 17, 0, 0, 0, localLocation)
+ // TODO: make display timezone configurable, display in local time
+ timeIt := time.Date(monday.Year(), monday.Month(), monday.Day(), 0, 0, 0, 0, time.Local)
+ //dayEnd := time.Date(monday.Year(), monday.Month(), monday.Day(), 17, 0, 0, 0, time.Local)
//weekEnd := dayEnd.Add(time.Hour * 24*5)
// Working hours - Eight to Five
@@ -258,14 +279,15 @@ func (s *Server) Week(w http.ResponseWriter, req *http.Request, ps httprouter.Pa
if err != nil {
log.Printf("Error parsing html template: %v\n", err)
w.WriteHeader(http.StatusInternalServerError)
- // TODO: Print human readable error to w
+ log.Printf("Error parsing html template: %v\n", err)
+ fmt.Fprintf(w, "Error parsing html template: %v\n", err)
return
}
err = tmpl.Execute(w, tableData)
if err != nil {
- log.Printf("Error executing html template: %v\n", err)
w.WriteHeader(http.StatusInternalServerError)
- // TODO: Print human readable error to w
+ log.Printf("Error executing html template: %v\n", err)
+ fmt.Fprintf(w, "Error executing html template: %v\n", err)
return
}
}
@@ -283,36 +305,31 @@ func (s *Server) Index(w http.ResponseWriter, req *http.Request, _ httprouter.Pa
func (s *Server) UpdateAvailability(calData []caldav.CalData) error {
// TODO: Make max for recurring events without end time configurable
rrLimit := time.Date(2024, 12, 31, 0, 0, 0, 0, time.Local)
- //localNow := time.Now()
+ localNow := time.Now()
+ isDSTNow := localNow.IsDST()
// Walk through events and store start/end time in the database
for _, event := range calData {
dec := ical.NewDecoder(strings.NewReader(event.Data))
cal, err := dec.Decode()
- var (
- //tzProp *ical.Prop
- // offset which is in use prior to this time zone observance
- //tzOffsetTo string
- //tzOffsetFrom string
- // offset which is in use in this time zone observance
- tzOffsetTo string
- // TODO:TZOFFSETFROM is the local time offset from GMT
- // when daylight saving time is in operation,
- // TZOFFSETTO is the local time offset from GMT when
- // standard time is in operation
- // https://stackoverflow.com/questions/3872178/what-does-tzoffsetfrom-and-tzoffsetto-mean
- )
+ var tzOffsetTo string
if err != nil {
return err
}
- // check for TIMEZONE component
+ // check for TIMEZONE component and read TZOFFSETTO from the
+ // correct sub-child
for _, c := range cal.Children {
if c.Name == ical.CompTimezone {
for _, child := range c.Children {
- //tzOffsetFrom = child.Props.Get(ical.PropTimezoneOffsetFrom).Value;
- tzOffsetTo = child.Props.Get(ical.PropTimezoneOffsetTo).Value
+ if isDSTNow && child.Name == ical.CompTimezoneDaylight {
+ // summer time
+ tzOffsetTo = child.Props.Get(ical.PropTimezoneOffsetTo).Value
+ } else if !isDSTNow && child.Name == ical.CompTimezoneStandard {
+ // winter time
+ tzOffsetTo = child.Props.Get(ical.PropTimezoneOffsetTo).Value
+ }
}
}
}
@@ -452,12 +469,6 @@ func (s *Server) ListCubicles(w http.ResponseWriter, req *http.Request, _ httpro
}
func (s *Server) ListCubiclesInWeek(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
- // TODO: make display timezone configurable, display in local time
- localLocation, err := time.LoadLocation("Local")
- if err != nil {
- return
- }
-
userAgent := req.Header.Get("user-agent")
timeNow := time.Now()
@@ -465,11 +476,11 @@ func (s *Server) ListCubiclesInWeek(w http.ResponseWriter, req *http.Request, ps
year, week := timeNow.ISOWeek()
// overwrite with requested week
- week, err = strconv.Atoi(ps.ByName("week"))
+ week, err := strconv.Atoi(ps.ByName("week"))
if err != nil {
- log.Printf("Cannot serve requested week '%d'\n", week)
w.WriteHeader(http.StatusBadRequest)
- // TODO: Print human readable error to w
+ log.Printf("Cannot serve requested week '%d'\n", week)
+ fmt.Fprintf(w, "Cannot serve requested week '%d'\n", week)
return
}
@@ -486,21 +497,21 @@ func (s *Server) ListCubiclesInWeek(w http.ResponseWriter, req *http.Request, ps
// TODO: Make heading date format configurable, start of week, etc..
tableHead := []string{
"Date/Time ",
- // TODO: %!v(PANIC=String method: strings: negative Repeat count)
- // with too many spaces
- "Book Cubicle ",
+ // TODO: %!v(PANIC=String method: strings: negative Repeat count)
+ "Book Cubicle ",
}
tableData := TableData{
- TableHead: tableHead,
- Rows: rows,
- Week: week,
- Year: year,
- Version: hivedavVersion,
+ TableHead: tableHead,
+ Rows: rows,
+ Week: week,
+ Year: year,
+ Version: hivedavVersion,
+ HiveDavHost: s.config.HiveDavHost,
}
// TODO: use timeIt to go through the loops below, remove the static arrays
- timeIt := time.Date(monday.Year(), monday.Month(), monday.Day(), 0, 0, 0, 0, localLocation)
+ timeIt := time.Date(monday.Year(), monday.Month(), monday.Day(), 0, 0, 0, 0, time.Local)
// 5 days * 9 1hour time slots = 45 possible booking commands
for d := 0; d < 5; d++ {
@@ -551,70 +562,180 @@ func (s *Server) ListCubiclesInWeek(w http.ResponseWriter, req *http.Request, ps
tmpl, err := template.ParseFiles("./templates/listcubicles.html")
if err != nil {
- log.Printf("Error parsing html template: %v\n", err)
w.WriteHeader(http.StatusInternalServerError)
- // TODO: Print human readable error to w
+ log.Printf("Error parsing html template: %v\n", err)
+ fmt.Fprintf(w, "Error parsing html template: %v\n", err)
return
}
err = tmpl.Execute(w, tableData)
if err != nil {
- log.Printf("Error executing html template: %v\n", err)
w.WriteHeader(http.StatusInternalServerError)
- // TODO: Print human readable error to w
+ log.Printf("Error executing html template: %v\n", err)
+ fmt.Fprintf(w, "Error executing html template: %v\n", err)
return
}
}
}
func (s *Server) BookCubicle(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
- //TODO: create and attach ical file
dtstart := ps.ByName("dtstart")
- //TODO: replace by config setting
- dtstartTime, err := time.ParseInLocation("2006-01-02-15", dtstart, time.Local)
+ //TODO: replace Local TZ by config setting
+ //TODO: remove Year from the input format
+ dtStartTimeLocal, err := time.ParseInLocation("2006-01-02-15", dtstart, time.Local)
if err != nil {
- log.Println("Error parsing booking dtstart")
+ w.WriteHeader(http.StatusInternalServerError)
+ log.Printf("Error parsing booking dtstart: %v\n", err)
+ fmt.Fprintf(w, "Error parsing booking dtstart: %v\n", err)
return
}
- log.Println("dtstart booking request: ", dtstartTime)
+ log.Printf("dtstart booking request (local time): %v\n", dtStartTimeLocal)
// total of maxMemory bytes of its file parts are stored in memory
// with the remainder stored on disk in temporary files
// https://pkg.go.dev/net/http#Request.ParseMultipartForm
err = req.ParseMultipartForm(256)
if err != nil {
- fmt.Fprintf(w, "ParseMultipartForm() err: %v", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ log.Printf("ParseMultipartForm err: %v\n", err)
+ fmt.Fprintf(w, "ParseMultipartForm err: %v\n", err)
return
}
mail := req.FormValue("mail")
msg := req.FormValue("msg")
- fmt.Fprintf(w, "Mail: %s\n", mail)
- fmt.Fprintf(w, "Msg: %s\n", msg)
+ //fmt.Fprintf(w, "Mail: %s\n", mail)
+ //fmt.Fprintf(w, "Msg: %s\n", msg)
log.Printf("Form: %+v\n", req.Form)
- if (mail == "" || msg == "") {
- fmt.Fprintf(w, "mail and msg required, try again: 'curl %s/book/%s -F 'mail=' -F 'msg='", s.config.HiveDavHost, dtstart)
+ if mail == "" || msg == "" {
+ w.WriteHeader(http.StatusBadRequest)
+ fmt.Fprintf(w, "Mail and msg required, try again: 'curl %s/book/%s -F 'mail=' -F 'msg='\n", s.config.HiveDavHost, dtstart)
return
}
- s.sendMail(mail, msg, dtstartTime)
- // TODO: never returns
+ err = s.sendMail(mail, msg, dtStartTimeLocal)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ log.Printf("Error occured while sending mail: %v\n", err)
+ fmt.Fprintf(w, "Error occured while sending mail: %v\n", err)
+ return
+ }
+ // TODO: print success message to user
return
}
// book a time slot in the calendar
-func (s *Server) sendMail(bee string, msg string, dtstart time.Time) {
+func (s *Server) sendMail(recepient string, msg string, dtstart time.Time) error {
+ _, err := netmail.ParseAddress(recepient)
+ if err != nil {
+ return err
+ }
- auth := smtp.PlainAuth("", s.config.SmtpUser, s.config.SmtpPassword, s.config.SmtpServer)
- bees := []string{
+ e := email.NewEmail()
+
+ e.From = s.config.SmtpUser
+ e.To = []string{
s.config.CaldavUser,
- bee,
+ recepient,
}
+ e.Subject = s.config.BookingSummary
+ e.Text = []byte(msg)
- var body bytes.Buffer
- body.Write([]byte(fmt.Sprintln("Subject: HiveDAV test")))
- err := smtp.SendMail(s.config.SmtpServer+":"+"587", auth, s.config.SmtpUser, bees, body.Bytes())
+ // parse ics booking template
+ icsTmpl, err := template.ParseFiles("templates/booking.ics")
if err != nil {
- log.Println("Error sending mail")
+ return err
+ }
+
+ // create ics booking from template
+ tmpFileName := fmt.Sprintf("/tmp/%s", dtstart.Format("2006-01-02-15"))
+ icsFile, err := os.Create(tmpFileName)
+ if err != nil {
+ return err
+ }
+ defer icsFile.Close()
+ defer os.Remove(tmpFileName)
+
+ //uuid := uuid.New()
+ uuid, _ := genUid(12)
+
+ // get timezone and offset
+ // TODO: This will probably have to change for winter (standard) time
+ // TODO: Check the 1/2h timezones
+ zone, offsetSec := dtstart.Zone()
+ offsetHours := offsetSec / 60 / 60
+
+ var (
+ dstTzOffsetTo string
+ dstTzOffsetFrom string
+ standardTzOffsetTo string
+ standardTzOffsetFrom string
+ )
+
+ if dtstart.IsDST() {
+ dstTzOffsetTo = fmt.Sprintf("%+03d00", (offsetHours))
+ standardTzOffsetFrom = fmt.Sprintf("%+03d00", (offsetHours))
+ dstTzOffsetFrom = fmt.Sprintf("%+03d00", (offsetHours - 1))
+ standardTzOffsetTo = fmt.Sprintf("%+03d00", (offsetHours - 1))
+ } else {
+ // Winter/Standard time
+ standardTzOffsetTo = fmt.Sprintf("%+03d00", (offsetHours))
+ dstTzOffsetFrom = fmt.Sprintf("%+03d00", (offsetHours))
+ standardTzOffsetFrom = fmt.Sprintf("%+03d00", (offsetHours + 1))
+ dstTzOffsetTo = fmt.Sprintf("%+03d00", (offsetHours + 1))
+ }
+
+ icsData := IcsData{
+ Prodid: fmt.Sprintf("-//HiveDAV//%s//EN", hivedavVersion),
+ Uid: strings.ToUpper(uuid),
+ Timestamp: time.Now().Format("20060102T150405Z"),
+ Dtstart: dtstart.Format("20060102T150405"),
+ Dtend: dtstart.Add(time.Hour).Format("20060102T150405"),
+ Timezone: zone,
+ StandardTzOffsetFrom: standardTzOffsetFrom,
+ StandardTzOffsetTo: standardTzOffsetTo,
+ DstTzOffsetFrom: dstTzOffsetFrom,
+ DstTzOffsetTo: dstTzOffsetTo,
+ Summary: s.config.BookingSummary,
+ Reminder: s.config.BookingReminder,
+ Description: msg,
+ Location: fmt.Sprintf("%s/%s", s.config.BookingLocation, strings.ToUpper(uuid)),
+ Organizer: s.config.CaldavUser,
+ Attendee: recepient,
+ }
+
+ log.Printf("IcsData: +%v\n", icsData)
+
+ err = icsTmpl.Execute(icsFile, icsData)
+ if err != nil {
+ return err
+ }
+
+ e.AttachFile(tmpFileName)
+ // https://datatracker.ietf.org/doc/html/rfc2447#section-2.4
+ e.Attachments[0].ContentType = "text/calendar; charset=utf-8; method=REQUEST"
+ log.Printf("%s\n", e.Attachments[0].Content)
+ smtpServer := fmt.Sprintf("%s:%d", s.config.SmtpHost, s.config.SmtpPort)
+ auth := smtp.PlainAuth("", s.config.SmtpUser, s.config.SmtpPassword, s.config.SmtpHost)
+
+ tlsConfig := tls.Config{
+ InsecureSkipVerify: true,
+ }
+ // TODO: Add config option to send without TLS
+ //err = e.SendWithStartTLS(smtpServer, auth, nil)
+ err = e.SendWithStartTLS(smtpServer, auth, &tlsConfig)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// https://datatracker.ietf.org/doc/html/rfc7986#section-5.3
+func genUid(n int) (string, error) {
+ bytes := make([]byte, n)
+ if _, err := rand.Read(bytes); err != nil {
+ return "", err
}
+ return hex.EncodeToString(bytes), nil
}
diff --git a/templates/booking.ics b/templates/booking.ics
@@ -0,0 +1,40 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:{{ .Prodid }}
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+BEGIN:VTIMEZONE
+TZID:{{ .Timezone }}
+BEGIN:STANDARD
+DTSTART:19700101T030000
+TZOFFSETFROM:{{ html .StandardTzOffsetFrom }}
+TZOFFSETTO:{{ html .StandardTzOffsetTo }}
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10;WKST=SU
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19700101T020000
+TZOFFSETFROM:{{ html .DstTzOffsetFrom }}
+TZOFFSETTO:{{ html .DstTzOffsetTo }}
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3;WKST=SU
+END:DAYLIGHT
+END:VTIMEZONE
+BEGIN:VEVENT
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:{{ .Organizer }}
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:{{ .Attendee }}
+ORGANIZER:mailto:{{ .Organizer }}
+CREATED:{{ .Timestamp }}
+DTSTAMP:{{ .Timestamp }}
+LAST-MODIFIED:{{ .Timestamp }}
+DTSTART;TZID={{ .Timezone }}:{{ .Dtstart }}
+DTEND;TZID={{ .Timezone }}:{{ .Dtend}}
+SUMMARY:{{ .Summary }}
+DESCRIPTION:{{ .Description }}
+UID:{{ .Uid }}
+LOCATION:{{ .Location }}
+BEGIN:VALARM
+TRIGGER:-PT{{ .Reminder }}M
+ACTION:DISPLAY
+DESCRIPTION:Reminder
+END:VALARM
+END:VEVENT
+END:VCALENDAR
diff --git a/templates/index.html b/templates/index.html
@@ -8,6 +8,9 @@
<p>
Serving week <i>{{ .Week }}</i> of year <i>{{ .Year }}</i>
</p>
+ <p>
+ This site is curlable, try <code>`curl {{ .HiveDavHost }}`</code>
+ </p>
<table>
<tr>
{{ range $i, $h := .TableHead }}
diff --git a/templates/listcubicles.html b/templates/listcubicles.html
@@ -0,0 +1,28 @@
+<html>
+ <head>
+ <title>HiveDAV - ListCubicles {{ .Week }}, Year {{ .Year }}</title>
+ <link rel="stylesheet" href="/css/index.css">
+ <link rel="stylesheet" href="/css/style.css">
+ </head>
+ <body>
+ <p>
+ Serving week <i>{{ .Week }}</i> of year <i>{{ .Year }}</i>
+ </p>
+ <table>
+ <tr>
+ {{ range $i, $h := .TableHead }}
+ <th>{{ $h }}</th>
+ {{ end }}
+ </tr>
+ {{ range $i, $r := .Rows }}
+ <tr>
+ <td>{{ index $r 0 }}</td>
+ <td>{{ index $r 1 }}</td>
+ </tr>
+ {{ end }}
+ </table>
+ <footer>
+ HiveDAV <a href="https://code.in0rdr.ch/hivedav/refs.html">{{ .Version }}</a> 🍯
+ </footer>
+ </body>
+</html>