package main import ( "bytes" "crypto/md5" "encoding/hex" "encoding/xml" "fmt" "github.com/vadimi/go-http-ntlm" "io" "net/http" "net/http/cookiejar" "strings" "sync" "text/template" "time" ) type FolderId struct { Id string `xml:",attr"` ChangeKey string `xml:",attr"` } type CalendarItem struct { Subject string UID string Start time.Time End time.Time RecurrenceId string Sensitivity string CalendarItemType string AppointmentState AppointmentState IsAllDayEvent bool } type AppointmentState int const ( AppointmentStateMeeting AppointmentState = 1 << iota // This appointment is a meeting. AppointmentStateReceived // This appointment has been received. AppointmentStateCancelled // This appointment has been canceled. ) // Build a hash for the given calendar item by combining the UID and // the recurrenceId therefore guaranteeing a unique identifier for the // event, even if it has been a calculated recurrence (which would // otherwise share the same UID). func (ci CalendarItem) Hash() string { h := md5.New() h.Write([]byte(ci.UID)) h.Write([]byte(ci.RecurrenceId)) return strings.ToUpper(hex.EncodeToString(h.Sum(nil))) } func (ci CalendarItem) IsCancelled() bool { return ci.AppointmentState&AppointmentStateCancelled != 0 } type EWSCalendar struct { httpClient *http.Client url string } type authType int const ( authTypeUnknown authType = iota authTypeBasic authTypeNTLM ) type EWSRoundTripper struct { initMutex sync.Mutex authType authType username string password string delegate http.RoundTripper } func (er EWSRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) { er.initMutex.Lock() if er.authType == authTypeUnknown { // Find authentication scheme. resp, err := http.DefaultClient.Get(r.URL.String()) if err == nil && resp.StatusCode == http.StatusUnauthorized { // This is a good time to find out what the server prefers. authHeaders := resp.Header["Www-Authenticate"] if authHeaders != nil { for _, h := range authHeaders { if strings.HasPrefix(h, "Basic") { er.authType = authTypeBasic } else if strings.HasPrefix(h, "NTLM") { er.authType = authTypeNTLM break // NTLM is the best we could do } } } if er.authType == authTypeNTLM { // We need to replace the delegator. er.delegate = &httpntlm.NtlmTransport{ Domain: "", User: er.username, Password: er.password, } } } } er.initMutex.Unlock() if er.authType == authTypeBasic { r.SetBasicAuth(er.username, er.password) } return er.delegate.RoundTrip(r) } func NewEWSCalendar(url, username, password string) *EWSCalendar { // Prepare a cookie jar, since EWS uses some. jar, err := cookiejar.New(nil) if err != nil { panic(err) } return &EWSCalendar{ httpClient: &http.Client{ Jar: jar, Transport: EWSRoundTripper{ username: username, password: password, delegate: http.DefaultTransport, }, }, url: url, } } // Internal helper to prepare a request to the EWS server. This is a shortcut // since all requests share some similarities: they transmit XML and receive // XML. Also it needs a basic auth header. func (e *EWSCalendar) prepareRequest(body io.Reader) (req *http.Request, err error) { req, err = http.NewRequest(http.MethodPost, e.url, body) req.Header.Set("Content-Type", "text/xml") req.Header.Set("Accept", "text/xml") return } // Request the ID of the default calendar folder. This is necessary to do // the actual event query. func (e *EWSCalendar) getCalendarFolderID() (id *FolderId, err error) { req, err := e.prepareRequest(strings.NewReader(folderIdRequest)) if err != nil { return } resp, err := e.httpClient.Do(req) if err != nil { return } if resp.StatusCode != http.StatusOK { err = fmt.Errorf("unexpected status when querying folderID: %d", resp.StatusCode) return } d := xml.NewDecoder(resp.Body) defer resp.Body.Close() for { var t xml.Token t, err = d.Token() if err == io.EOF { err = nil return } else if err != nil { return } switch t := t.(type) { case xml.StartElement: if t.Name.Local == "FolderId" { id = &FolderId{} err = d.DecodeElement(id, &t) // The first one is enough return } } } } // Query events from the given folder in the given timeframe. func (e *EWSCalendar) getCalendarItems(folder *FolderId, start, end time.Time) (items []CalendarItem, err error) { b := &bytes.Buffer{} model := struct { FolderId *FolderId StartDate string EndDate string }{folder, start.Format(time.RFC3339), end.Format(time.RFC3339)} err = calendarQuery.Execute(b, &model) if err != nil { return } req, err := e.prepareRequest(b) if err != nil { return } resp, err := e.httpClient.Do(req) if err != nil { return } if resp.StatusCode != http.StatusOK { err = fmt.Errorf("unexpected status when querying calendar items: %d", resp.StatusCode) return } d := xml.NewDecoder(resp.Body) defer resp.Body.Close() for { var t xml.Token t, err = d.Token() if err == io.EOF { err = nil return } else if err != nil { return } switch t := t.(type) { case xml.StartElement: if t.Name.Local == "CalendarItem" { item := CalendarItem{} err = d.DecodeElement(&item, &t) items = append(items, item) } } } return } const folderIdRequest = ` AllProperties ` var calendarQuery = template.Must(template.New("calendarQuery").Parse(` IdOnly `))