caldav.go (5987B)
1 package caldav 2 3 // Functions to read VEVENTs from the CalDav server: 4 // https://sabre.io/dav/building-a-caldav-client 5 6 import ( 7 "bytes" 8 "encoding/xml" 9 "fmt" 10 "net/http" 11 ) 12 13 type XMLUserPrincipalResponse struct { 14 XMLName xml.Name `xml:multistatus` 15 UserPrincipal string `xml:"response>propstat>prop>current-user-principal>href"` 16 } 17 type XMLHomeSetResponse struct { 18 XMLName xml.Name `xml:multistatus` 19 HomeSet string `xml:"response>propstat>prop>calendar-home-set>href"` 20 } 21 type XMLHomeSetCalendarResponse struct { 22 XMLName xml.Name `xml:multistatus` 23 Response []Response `xml:"response"` 24 } 25 type Response struct { 26 XMLName xml.Name `xml:response` 27 Href string `xml:"href"` 28 } 29 type XMLCalendarResponse struct { 30 XMLName xml.Name `xml:multistatus` 31 CalData []CalData `xml:"response"` 32 } 33 type CalData struct { 34 XMLName xml.Name `xml:response` 35 Href string `xml:"href"` 36 Data string `xml:"propstat>prop>calendar-data"` 37 } 38 39 func UserPrincipal(caldavHost string, user string, pass string) (string, error) { 40 payload := []byte(`<d:propfind xmlns:d="DAV:"> 41 <d:prop> 42 <d:current-user-principal /> 43 </d:prop> 44 </d:propfind>`) 45 reader := bytes.NewReader(payload) 46 req, err := http.NewRequest("PROPFIND", caldavHost, reader) 47 if err != nil { 48 return "", err 49 } 50 req.Header.Set("Depth", "0") 51 req.Header.Set("Content-Type", "text/xml") 52 req.SetBasicAuth(user, pass) 53 54 client := &http.Client{ 55 CheckRedirect: func(req *http.Request, via []*http.Request) error { 56 // return real redirect response code (>=300) 57 return http.ErrUseLastResponse 58 }, 59 } 60 res, err := client.Do(req) 61 if err != nil { 62 return "", err 63 } 64 defer res.Body.Close() 65 66 redirectUri, err := res.Location() 67 68 if res.StatusCode >= 300 && res.StatusCode <= 399 { 69 // follow first redirect to suggested uri, repeat request 70 // some providers are not discoverable starting at caldavHost 71 req, err = http.NewRequest("PROPFIND", caldavHost+redirectUri.Path, reader) 72 req.Header.Set("Depth", "0") 73 req.Header.Set("Content-Type", "text/xml") 74 req.SetBasicAuth(user, pass) 75 res, err = client.Do(req) 76 if err != nil { 77 return "", err 78 } 79 } 80 81 if res.StatusCode != 207 { 82 return "", err 83 } 84 85 decoder := xml.NewDecoder(res.Body) 86 var xmlResponse XMLUserPrincipalResponse 87 err = decoder.Decode(&xmlResponse) 88 if err != nil { 89 return "", err 90 } 91 92 return xmlResponse.UserPrincipal, nil 93 } 94 95 func CalendarHome(caldavHost string, user string, pass string, userPrincipal string, calendar int) (string, error) { 96 payload := []byte(`<d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav"> 97 <d:prop> 98 <c:calendar-home-set /> 99 </d:prop> 100 </d:propfind>`) 101 reader := bytes.NewReader(payload) 102 principalPath := fmt.Sprintf("%s%s", caldavHost, userPrincipal) 103 104 req, err := http.NewRequest("PROPFIND", principalPath, reader) 105 if err != nil { 106 return "", err 107 } 108 req.Header.Set("Depth", "0") 109 req.Header.Set("Content-Type", "text/xml") 110 req.SetBasicAuth(user, pass) 111 112 res, err := http.DefaultClient.Do(req) 113 defer res.Body.Close() 114 if err != nil { 115 return "", err 116 } 117 118 if res.StatusCode != 207 { 119 return "", err 120 } 121 122 decoder := xml.NewDecoder(res.Body) 123 var xmlResponse XMLHomeSetResponse 124 err = decoder.Decode(&xmlResponse) 125 if err != nil { 126 return "", err 127 } 128 129 return xmlResponse.HomeSet, nil 130 } 131 132 func CalendarFromHomeSet(caldavHost string, user string, pass string, homeSetPath string, calendar int) (calendarPath string, err error) { 133 payload := []byte(`<d:propfind xmlns:d="DAV:" xmlns:cs="http://calendarserver.org/ns/" xmlns:c="urn:ietf:params:xml:ns:caldav"> 134 <d:prop> 135 <d:resourcetype /> 136 <d:displayname /> 137 <cs:getctag /> 138 <c:supported-calendar-component-set /> 139 </d:prop> 140 </d:propfind>`) 141 reader := bytes.NewReader(payload) 142 homeSetUrl := fmt.Sprintf("%s%s", caldavHost, homeSetPath) 143 144 req, err := http.NewRequest("PROPFIND", homeSetUrl, reader) 145 if err != nil { 146 return "", err 147 } 148 req.Header.Set("Depth", "1") 149 req.Header.Set("Content-Type", "text/xml") 150 req.SetBasicAuth(user, pass) 151 152 res, err := http.DefaultClient.Do(req) 153 defer res.Body.Close() 154 if err != nil { 155 return "", err 156 } 157 158 if res.StatusCode != 207 { 159 return "", err 160 } 161 162 decoder := xml.NewDecoder(res.Body) 163 var xmlResponse XMLHomeSetCalendarResponse 164 err = decoder.Decode(&xmlResponse) 165 if err != nil { 166 return "", err 167 } 168 169 defer func() { 170 if r := recover(); r != nil { 171 err = fmt.Errorf("Calendar '%d' not found in homeset: %v", calendar, r) 172 } 173 }() 174 175 calendarPath = xmlResponse.Response[calendar].Href 176 return calendarPath, nil 177 } 178 179 func GetAvailability(caldavHost string, user string, pass string, calendarPath string) (data []CalData, err error) { 180 // concatenant the host and the calendar path 181 calendarUrl := fmt.Sprintf("%s%s", caldavHost, calendarPath) 182 return GetAvailabilityByUrl(calendarUrl, user, pass) 183 } 184 185 func GetAvailabilityByUrl(calendarUrl string, user string, pass string) (data []CalData, err error) { 186 payload := []byte(`<c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav"> 187 <d:prop> 188 <d:getetag /> 189 <c:calendar-data /> 190 </d:prop> 191 <c:filter> 192 <c:comp-filter name="VCALENDAR"> 193 <c:comp-filter name="VEVENT" /> 194 </c:comp-filter> 195 </c:filter> 196 </c:calendar-query>`) 197 reader := bytes.NewReader(payload) 198 199 req, err := http.NewRequest("REPORT", calendarUrl, reader) 200 if err != nil { 201 return data, err 202 } 203 req.Header.Set("Depth", "1") 204 req.Header.Set("Content-Type", "text/xml") 205 req.SetBasicAuth(user, pass) 206 207 res, err := http.DefaultClient.Do(req) 208 defer res.Body.Close() 209 if err != nil { 210 return data, err 211 } 212 213 if res.StatusCode != 207 { 214 return data, err 215 } 216 217 decoder := xml.NewDecoder(res.Body) 218 var xmlResponse XMLCalendarResponse 219 err = decoder.Decode(&xmlResponse) 220 if err != nil { 221 return data, err 222 } 223 224 data = xmlResponse.CalData 225 return data, nil 226 }