hivedav

A curlable free/busy scheduler with CalDAV integration
git clone https://git.in0rdr.ch/hivedav.git
Log | Files | Refs | Pull requests |Archive | README | LICENSE

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 }