diff --git a/common.go b/common.go index 1303165..b8d06ca 100644 --- a/common.go +++ b/common.go @@ -13,11 +13,18 @@ const ( ResponseClassError ResponseClass = "Error" ) +type ResponseItems struct { + Message []ItemId `xml:"Items>Message>ItemId"` + CalendarItem []ItemId `xml:"Items>CalendarItem>ItemId"` +} + type Response struct { ResponseClass ResponseClass `xml:"ResponseClass,attr"` MessageText string `xml:"MessageText"` ResponseCode string `xml:"ResponseCode"` MessageXml MessageXml `xml:"MessageXml"` + + ResponseItems } type EmailAddress struct { diff --git a/create_item.go b/create_item.go index d905255..f2bee09 100644 --- a/create_item.go +++ b/create_item.go @@ -32,18 +32,18 @@ type Message struct { } type CalendarItem struct { - Subject string `xml:"t:Subject"` - Body Body `xml:"t:Body"` - ReminderIsSet bool `xml:"t:ReminderIsSet"` - ReminderMinutesBeforeStart int `xml:"t:ReminderMinutesBeforeStart"` - Start time.Time `xml:"t:Start"` - End time.Time `xml:"t:End"` - IsAllDayEvent bool `xml:"t:IsAllDayEvent"` - LegacyFreeBusyStatus string `xml:"t:LegacyFreeBusyStatus"` - Location string `xml:"t:Location"` - RequiredAttendees []Attendees `xml:"t:RequiredAttendees"` - OptionalAttendees []Attendees `xml:"t:OptionalAttendees"` - Resources []Attendees `xml:"t:Resources"` + Subject string `xml:"t:Subject,omitempty"` + Body Body `xml:"t:Body,omitempty"` + ReminderIsSet bool `xml:"t:ReminderIsSet,omitempty"` + ReminderMinutesBeforeStart int `xml:"t:ReminderMinutesBeforeStart,omitempty"` + Start time.Time `xml:"t:Start,omitempty"` + End time.Time `xml:"t:End,omitempty"` + IsAllDayEvent bool `xml:"t:IsAllDayEvent,omitempty"` + LegacyFreeBusyStatus string `xml:"t:LegacyFreeBusyStatus,omitempty"` + Location string `xml:"t:Location,omitempty"` + RequiredAttendees []Attendees `xml:"t:RequiredAttendees,omitempty"` + OptionalAttendees []Attendees `xml:"t:OptionalAttendees,omitempty"` + Resources []Attendees `xml:"t:Resources,omitempty"` } type Body struct { @@ -75,21 +75,24 @@ type createItemResponseBodyEnvelop struct { XMLName struct{} `xml:"Envelope"` Body createItemResponseBody `xml:"Body"` } + type createItemResponseBody struct { - CreateItemResponse CreateItemResponse `xml:"CreateItemResponse"` + CreateItemResponse ItemOperationResponse `xml:"CreateItemResponse"` } -type CreateItemResponse struct { +type ItemOperationResponse struct { ResponseMessages ResponseMessages `xml:"ResponseMessages"` } type ResponseMessages struct { CreateItemResponseMessage Response `xml:"CreateItemResponseMessage"` + UpdateItemResponseMessage Response `xml:"UpdateItemResponseMessage"` + DeleteItemResponseMessage Response `xml:"DeleteItemResponseMessage"` } // CreateMessageItem // https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createitem-operation-email-message -func CreateMessageItem(c Client, m ...Message) error { +func CreateMessageItem(c Client, m ...Message) ([]ItemId, error) { item := &CreateItem{ MessageDisposition: "SendAndSaveCopy", @@ -99,24 +102,25 @@ func CreateMessageItem(c Client, m ...Message) error { xmlBytes, err := xml.MarshalIndent(item, "", " ") if err != nil { - return err + return nil, err } bb, err := c.SendAndReceive(xmlBytes) if err != nil { - return err + return nil, err } - if err := checkCreateItemResponseForErrors(bb); err != nil { - return err + items, err := checkCreateItemResponseForErrors(bb) + if err != nil { + return nil, err } - return nil + return items.Message, nil } // CreateCalendarItem // https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createitem-operation-calendar-item -func CreateCalendarItem(c Client, ci ...CalendarItem) error { +func CreateCalendarItem(c Client, ci ...CalendarItem) ([]ItemId, error) { item := &CreateItem{ SendMeetingInvitations: "SendToAllAndSaveCopy", @@ -126,30 +130,32 @@ func CreateCalendarItem(c Client, ci ...CalendarItem) error { xmlBytes, err := xml.MarshalIndent(item, "", " ") if err != nil { - return err + return nil, err } bb, err := c.SendAndReceive(xmlBytes) if err != nil { - return err + return nil, err } - if err := checkCreateItemResponseForErrors(bb); err != nil { - return err + items, err := checkCreateItemResponseForErrors(bb) + if err != nil { + return nil, err } - return nil + return items.CalendarItem, nil } -func checkCreateItemResponseForErrors(bb []byte) error { +func checkCreateItemResponseForErrors(bb []byte) (items ResponseItems, err error) { var soapResp createItemResponseBodyEnvelop - if err := xml.Unmarshal(bb, &soapResp); err != nil { - return err + if err = xml.Unmarshal(bb, &soapResp); err != nil { + return } resp := soapResp.Body.CreateItemResponse.ResponseMessages.CreateItemResponseMessage if resp.ResponseClass == ResponseClassError { - return errors.New(resp.MessageText) + err = errors.New(resp.MessageText) + return } - return nil + return resp.ResponseItems, nil } diff --git a/delete_item.go b/delete_item.go new file mode 100644 index 0000000..2acc965 --- /dev/null +++ b/delete_item.go @@ -0,0 +1,74 @@ +package ews + +import ( + "encoding/xml" + "errors" +) + +type DeleteStrategy struct { + DeleteType string `xml:"DeleteType,attr,omitempty"` + SendMeetingCancellations string `xml:"SendMeetingCancellations,attr,omitempty"` +} + +type DeleteItem struct { + XMLName struct{} `xml:"m:DeleteItem"` + ItemIds ItemIds `xml:"m:ItemIds"` + + DeleteStrategy +} + +type ItemIds struct { + XMLName struct{} `xml:"m:ItemIds"` + + ItemId []ItemId `xml:"t:ItemId"` +} + +type deleteItemResponseBodyEnvelop struct { + XMLName struct{} `xml:"Envelope"` + Body deleteItemResponseBody `xml:"Body"` +} + +type deleteItemResponseBody struct { + DeleteItemResponse ItemOperationResponse `xml:"DeleteItemResponse"` +} + +// DeleteItems +// https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/how-to-delete-appointments-and-cancel-meetings-by-using-ews-in-exchange +func DeleteItems(c Client, id []ItemId, strategy ...DeleteStrategy) error { + + strategy = append(strategy, DeleteStrategy{ + DeleteType: "MoveToDeletedItems", + SendMeetingCancellations: "SendToAllAndSaveCopy", + }) + + item := DeleteItem{ + ItemIds: ItemIds{ + ItemId: id, + }, + DeleteStrategy: strategy[0], + } + + xmlBytes, err := xml.MarshalIndent(item, "", " ") + if err != nil { + return err + } + + bb, err := c.SendAndReceive(xmlBytes) + if err != nil { + return err + } + return checkDeleteItemResponseForErrors(bb) +} + +func checkDeleteItemResponseForErrors(bb []byte) (err error) { + var soapResp deleteItemResponseBodyEnvelop + if err = xml.Unmarshal(bb, &soapResp); err != nil { + return + } + + resp := soapResp.Body.DeleteItemResponse.ResponseMessages.DeleteItemResponseMessage + if resp.ResponseClass == ResponseClassError { + err = errors.New(resp.MessageText) + } + return +} diff --git a/delete_item_test.go b/delete_item_test.go new file mode 100644 index 0000000..26f74aa --- /dev/null +++ b/delete_item_test.go @@ -0,0 +1,31 @@ +package ews + +import ( + "encoding/xml" + "log" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_marshal_DeleteItems(t *testing.T) { + + ditem := &DeleteItem{ + ItemIds: ItemIds{ + ItemId: []ItemId{ + {"ID", "Key"}, + }, + }, + } + + xmlBytes, err := xml.MarshalIndent(ditem, "", " ") + if err != nil { + log.Fatal(err) + } + + assert.Equal(t, ` + + + +`, string(xmlBytes)) +} diff --git a/ews.go b/ews.go index f7c17b8..f229079 100644 --- a/ews.go +++ b/ews.go @@ -4,10 +4,11 @@ import ( "bytes" "crypto/tls" "fmt" - "github.com/Azure/go-ntlmssp" "io/ioutil" "net/http" "net/http/httputil" + + "github.com/Azure/go-ntlmssp" ) const ( @@ -27,8 +28,18 @@ const ( type Config struct { Dump bool + OAuth bool NTLM bool SkipTLS bool + + Transport func(*http.Request) (*http.Response, error) +} + +func (c *Config) RoundTrip(req *http.Request) (*http.Response, error) { + if c.Transport == nil { + c.Transport = http.DefaultClient.Transport.RoundTrip + } + return c.Transport(req) } type Client interface { @@ -78,6 +89,7 @@ func (c *client) SendAndReceive(body []byte) ([]byte, error) { req.Header.Set("Content-Type", "text/xml") client := &http.Client{ + Transport: c.config, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, @@ -105,7 +117,17 @@ func (c *client) SendAndReceive(body []byte) ([]byte, error) { func applyConfig(config *Config, client *http.Client) { if config.NTLM { - client.Transport = ntlmssp.Negotiator{} + client.Transport = ntlmssp.Negotiator{ + RoundTripper: &http.Transport{ + TLSNextProto: map[string]func(authority string, c *tls.Conn) http.RoundTripper{}, + TLSClientConfig: &tls.Config{InsecureSkipVerify: config.SkipTLS}, + }, + } + } + if config.OAuth { + //To get AccessToken from MSAL: https://learn.microsoft.com/en-us/exchange/client-developer/exchange-web-services/how-to-authenticate-an-ews-application-by-using-oauth and pass it as PASSWORD in any circumstances. + //BE CAREFUL: 'Main' BRANCH OF GO SUPPORT REPOSITORY github.com/AzureAD/microsoft-authentication-library-for-go MAY NOT UP TO DATE WITH THE PASSAGE REQUIREMENT, USE 'Dev' BRANCH AND PROCEED WITH CAUTION IF YOU ARE IN PRODUCTION. + client.Transport = bearerNegotiator{config} } if config.SkipTLS { http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} diff --git a/ewsutil/create_event.go b/ewsutil/create_event.go index 527f005..b14eac3 100644 --- a/ewsutil/create_event.go +++ b/ewsutil/create_event.go @@ -1,26 +1,27 @@ package ewsutil import ( - "github.com/mhewedy/ews" "time" + + "github.com/mhewedy/ews" ) func CreateHTMLEvent( c ews.Client, to, optional []string, subject, body, location string, from time.Time, duration time.Duration, -) error { +) ([]ews.ItemId, error) { return createEvent(c, to, optional, subject, body, location, "HTML", from, duration) } // CreateEvent helper method to send Message func CreateEvent( c ews.Client, to, optional []string, subject, body, location string, from time.Time, duration time.Duration, -) error { +) ([]ews.ItemId, error) { return createEvent(c, to, optional, subject, body, location, "Text", from, duration) } func createEvent( c ews.Client, to, optional []string, subject, body, location, bodyType string, from time.Time, duration time.Duration, -) error { +) ([]ews.ItemId, error) { requiredAttendees := make([]ews.Attendee, len(to)) for i, tt := range to { @@ -41,16 +42,16 @@ func createEvent( BodyType: bodyType, Body: []byte(body), }, - ReminderIsSet: true, - ReminderMinutesBeforeStart: 15, - Start: from, - End: from.Add(duration), - IsAllDayEvent: false, - LegacyFreeBusyStatus: ews.BusyTypeBusy, - Location: location, - RequiredAttendees: []ews.Attendees{{Attendee: requiredAttendees}}, - OptionalAttendees: []ews.Attendees{{Attendee: optionalAttendees}}, - Resources: []ews.Attendees{{Attendee: room}}, + // ReminderIsSet: duration != 0, + // ReminderMinutesBeforeStart: 15, + Start: from, + End: from.Add(duration), + IsAllDayEvent: duration == 0, + LegacyFreeBusyStatus: ews.BusyTypeBusy, + Location: location, + RequiredAttendees: []ews.Attendees{{Attendee: requiredAttendees}}, + OptionalAttendees: []ews.Attendees{{Attendee: optionalAttendees}}, + Resources: []ews.Attendees{{Attendee: room}}, } return ews.CreateCalendarItem(c, m) diff --git a/ewsutil/delete_event.go b/ewsutil/delete_event.go new file mode 100644 index 0000000..321de17 --- /dev/null +++ b/ewsutil/delete_event.go @@ -0,0 +1,11 @@ +package ewsutil + +import ( + "github.com/mhewedy/ews" +) + +func DeleteEvent( + c ews.Client, id ...ews.ItemId, +) error { + return ews.DeleteItems(c, id) +} diff --git a/ewsutil/send_email.go b/ewsutil/send_email.go index e2b153d..eb061f0 100644 --- a/ewsutil/send_email.go +++ b/ewsutil/send_email.go @@ -3,7 +3,7 @@ package ewsutil import "github.com/mhewedy/ews" // SendEmail helper method to send Message -func SendEmail(c ews.Client, to []string, subject, body string) error { +func SendEmail(c ews.Client, to []string, subject, body string) (err error) { m := ews.Message{ ItemClass: "IPM.Note", @@ -24,5 +24,6 @@ func SendEmail(c ews.Client, to []string, subject, body string) error { } m.ToRecipients.Mailbox = append(m.ToRecipients.Mailbox, mb...) - return ews.CreateMessageItem(c, m) + _, err = ews.CreateMessageItem(c, m) + return } diff --git a/ewsutil/update_event.go b/ewsutil/update_event.go new file mode 100644 index 0000000..c2f0266 --- /dev/null +++ b/ewsutil/update_event.go @@ -0,0 +1,58 @@ +package ewsutil + +import ( + "time" + + "github.com/mhewedy/ews" +) + +func UpdateHTMLEvent( + c ews.Client, id ews.ItemId, to, optional []string, subject, body, location string, from time.Time, duration time.Duration, +) ([]ews.ItemId, error) { + return updateEvent(c, id, to, optional, subject, body, location, "HTML", from, duration) +} + +// UpdateEvent helper method to update Message +func UpdateEvent( + c ews.Client, id ews.ItemId, to, optional []string, subject, body, location string, from time.Time, duration time.Duration, +) ([]ews.ItemId, error) { + return updateEvent(c, id, to, optional, subject, body, location, "Text", from, duration) +} + +func updateEvent( + c ews.Client, id ews.ItemId, to, optional []string, subject, body, location, bodyType string, from time.Time, duration time.Duration, +) ([]ews.ItemId, error) { + + requiredAttendees := make([]ews.Attendee, len(to)) + for i, tt := range to { + requiredAttendees[i] = ews.Attendee{Mailbox: ews.Mailbox{EmailAddress: tt}} + } + + optionalAttendees := make([]ews.Attendee, len(optional)) + for i, tt := range optional { + optionalAttendees[i] = ews.Attendee{Mailbox: ews.Mailbox{EmailAddress: tt}} + } + + room := make([]ews.Attendee, 1) + room[0] = ews.Attendee{Mailbox: ews.Mailbox{EmailAddress: location}} + + m := ews.CalendarItem{ + Subject: subject, + Body: ews.Body{ + BodyType: bodyType, + Body: []byte(body), + }, + // ReminderIsSet: duration != 0, + // ReminderMinutesBeforeStart: 15, + Start: from, + End: from.Add(duration), + IsAllDayEvent: duration == 0, + LegacyFreeBusyStatus: ews.BusyTypeBusy, + Location: location, + RequiredAttendees: []ews.Attendees{{Attendee: requiredAttendees}}, + OptionalAttendees: []ews.Attendees{{Attendee: optionalAttendees}}, + Resources: []ews.Attendees{{Attendee: room}}, + } + + return ews.UpdateCalendarItem(c, id, m) +} diff --git a/example_test.go b/example_test.go index afdacfd..52bc29d 100644 --- a/example_test.go +++ b/example_test.go @@ -2,13 +2,14 @@ package ews_test import ( "fmt" - . "github.com/mhewedy/ews" - "github.com/mhewedy/ews/ewsutil" "io/ioutil" "math" "os" "testing" "time" + + . "github.com/mhewedy/ews" + "github.com/mhewedy/ews/ewsutil" ) func Test_Example(t *testing.T) { @@ -74,7 +75,7 @@ func testCreateCalendarItem(c Client) error { attendees := make([]Attendees, 0) attendees = append(attendees, Attendees{Attendee: attendee}) - return CreateCalendarItem(c, CalendarItem{ + var _, err = CreateCalendarItem(c, CalendarItem{ Subject: "Planning Meeting", Body: Body{ BodyType: "Text", @@ -89,6 +90,7 @@ func testCreateCalendarItem(c Client) error { Location: "Conference Room 721", RequiredAttendees: attendees, }) + return err } func testGetUserAvailability(c Client) error { @@ -185,7 +187,7 @@ func testListUsersEvents(c Client) error { func testCreateEvent(c Client) error { - return ewsutil.CreateEvent(c, + var _, err = ewsutil.CreateEvent(c, []string{"mhewedy@mhewedy.onmicrosoft.com", "example2@mhewedy.onmicrosoft.com"}, []string{}, "An Event subject", @@ -194,11 +196,12 @@ func testCreateEvent(c Client) error { time.Now().Add(24*time.Hour), 30*time.Minute, ) + return err } func testCreateHTMLEvent(c Client) error { - return ewsutil.CreateHTMLEvent(c, + var _, err = ewsutil.CreateHTMLEvent(c, []string{"mhewedy@mhewedy.onmicrosoft.com", "example@mhewedy.onmicrosoft.com"}, []string{}, "An Event subject", @@ -209,6 +212,7 @@ func testCreateHTMLEvent(c Client) error { time.Now().Add(24*time.Hour), 30*time.Minute, ) + return err } func testGetRoomLists(c Client) error { diff --git a/go.mod b/go.mod index 2702c37..fbe094a 100644 --- a/go.mod +++ b/go.mod @@ -6,5 +6,4 @@ require ( github.com/Azure/go-ntlmssp v0.0.0-20191115210519-2b2be6cc8ed4 github.com/stretchr/testify v1.4.0 golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 // indirect - golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f // indirect ) diff --git a/go.sum b/go.sum index 8da5354..34697c5 100644 --- a/go.sum +++ b/go.sum @@ -10,17 +10,10 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g= golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f h1:J5lckAjkw6qYlOZNj90mLYNTEKDvWeuc1yieZ8qUzUE= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f h1:kDxGY2VmgABOe55qheT/TFqUMtcTHnomIPS1iv3G4Ms= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= diff --git a/oauth.go b/oauth.go new file mode 100644 index 0000000..68c1429 --- /dev/null +++ b/oauth.go @@ -0,0 +1,53 @@ +package ews + +import ( + "bytes" + "encoding/base64" + "fmt" + "io" + "net/http" + "strings" +) + +type bearerNegotiator struct{ http.RoundTripper } + +// C# Code Ref: https://github.com/OfficeDev/ews-managed-api/blob/master/Credentials/OAuthCredentials.cs +func (b bearerNegotiator) RoundTrip(req *http.Request) (res *http.Response, err error) { + rt := b.RoundTripper + if rt == nil { + rt = http.DefaultTransport + } + u, p := b.authheader(req.Header.Get("Authorization")) + if len(u) == 0 { + //If it is not a simple auth, just run it as usual + return rt.RoundTrip(req) + } + //Set bearer header info + req.Header.Set("Authorization", "Bearer "+p) + //Set anchor mailbox info + req.Header.Set("X-AnchorMailbox", u) + //Read the body and add soap:Header element + if bin, _ := io.ReadAll(req.Body); len(bin) > 0 { + //IMPORTANT: REMOVE THIS PART YOU WILL MEET: + //ExchangeImpersonation SOAP header must be present for this type of OAuth token. + //Issue on stackoverflow: https://stackoverflow.com/questions/56148996/error-exchangeimpersonation-soap-header-must-be-present-for-this-type-of-oauth + //Manual by Microsoft: https://learn.microsoft.com/en-us/previous-versions/office/developer/exchange-server-2010/bb204088(v=exchg.140) + bin = bytes.Replace(bin, []byte(``), []byte(fmt.Sprintf(`%s`, u)), 1) + req.Body = io.NopCloser(bytes.NewReader(bin)) + //Update content-length information. + req.ContentLength = int64(len(bin)) + } + return rt.RoundTrip(req) +} + +func (bearerNegotiator) authheader(auth string) (u, p string) { + if !strings.HasPrefix(strings.ToLower(auth), "basic ") { + return + } + authStr, _ := base64.StdEncoding.DecodeString(auth[6:]) + var idx = bytes.LastIndex(authStr, []byte{':'}) + if idx != -1 { + u, p = string(authStr[:idx]), string(authStr[idx+1:]) + } + return +} diff --git a/update_item.go b/update_item.go new file mode 100644 index 0000000..06031a8 --- /dev/null +++ b/update_item.go @@ -0,0 +1,184 @@ +package ews + +import ( + "encoding/xml" + "errors" + "reflect" + "strings" +) + +type UpdateStrategy struct { + ConflictResolution string `xml:"ConflictResolution,attr,omitempty"` + MessageDisposition string `xml:"MessageDisposition,attr,omitempty"` + SendMeetingInvitationsOrCancellations string `xml:"SendMeetingInvitationsOrCancellations,attr,omitempty"` +} + +type UpdateItem struct { + XMLName struct{} `xml:"m:UpdateItem"` + + ItemChanges ItemChanges `xml:"m:ItemChanges"` + UpdateStrategy +} + +type ItemChanges struct { + XMLName xml.Name `xml:"m:ItemChanges"` + + ItemChanges []ItemChange +} + +type ItemChange struct { + XMLName xml.Name `xml:"t:ItemChange"` + + ItemId ItemId `xml:"t:ItemId"` + Updates Updates `xml:"t:Updates"` +} + +type Updates struct { + XMLName xml.Name `xml:"t:Updates"` + + Updates []SetItemField `xml:"t:Updates"` +} + +type SetItemField struct { + XMLName xml.Name `xml:"t:SetItemField"` + + FieldURI FieldURI `xml:"t:FieldURI"` + CalendarItem []Field `xml:"t:CalendarItem,omitempty"` + Message []Field `xml:"t:Message,omitempty"` +} + +type Field struct { + Name string + Attributes map[string]string + Value interface{} +} + +type updateItemResponseBodyEnvelop struct { + XMLName struct{} `xml:"Envelope"` + Body updateItemResponseBody `xml:"Body"` +} + +type updateItemResponseBody struct { + UpdateItemResponse ItemOperationResponse `xml:"UpdateItemResponse"` +} + +func (f Field) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + e.EncodeToken(start) + index, attr := 0, make([]xml.Attr, len(f.Attributes)) + for k, v := range f.Attributes { + attr[index].Name = xml.Name{Local: k} + attr[index].Value = v + } + var t = xml.StartElement{Name: xml.Name{Local: f.Name}, Attr: attr} + // e.EncodeToken(t) + e.EncodeElement(f.Value, t) + // e.EncodeToken(xml.EndElement{Name: t.Name}) + e.EncodeToken(xml.EndElement{Name: start.Name}) + return e.Flush() +} + +func getFields(obj interface{}) (fields []Field) { + val := reflect.Indirect(reflect.ValueOf(obj)) + t := val.Type() + length := t.NumField() + for index := 0; index < length; index++ { + value := val.Field(index) + if !value.IsZero() { + val, ok := t.Field(index).Tag.Lookup("xml") + if !ok { + val = t.Field(index).Name + } else { + val = strings.Split(val, ",")[0] + } + fields = append(fields, Field{ + Name: val, + Value: value.Interface(), + }) + } + } + return +} + +func (field Field) uri(replace string) string { + index := strings.Index(field.Name, ":") + if index != -1 { + return replace + field.Name[index:] + } + return field.Name +} + +var itemFields = map[string]bool{ + "t:Subject": true, + "t:Body": true, +} + +func getSetItemField(prefix string, fields ...Field) []SetItemField { + var setFields = make([]SetItemField, len(fields)) + for index, field := range fields { + setFields[index].CalendarItem = []Field{field} + replace := prefix + if itemFields[field.Name] { + replace = "item" + } + setFields[index].FieldURI = FieldURI{ + FieldURI: strings.Replace(field.Name, "t", replace, 1), + } + } + return setFields +} + +// UpdateCalendarItem +// https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/how-to-update-appointments-and-meetings-by-using-ews-in-exchange +func UpdateCalendarItem(c Client, id ItemId, ci CalendarItem, strategy ...UpdateStrategy) ([]ItemId, error) { + + strategy = append(strategy, UpdateStrategy{ + ConflictResolution: "AlwaysOverwrite", + MessageDisposition: "SaveOnly", + SendMeetingInvitationsOrCancellations: "SendToAllAndSaveCopy", + }) + + var setFields = getSetItemField("calendar", getFields(ci)...) + + item := UpdateItem{ + ItemChanges: ItemChanges{ItemChanges: []ItemChange{ + ItemChange{ + ItemId: id, + Updates: Updates{ + Updates: setFields, + }, + }, + }}, + UpdateStrategy: strategy[0], + } + + xmlBytes, err := xml.MarshalIndent(item, "", " ") + if err != nil { + return nil, err + } + + bb, err := c.SendAndReceive(xmlBytes) + if err != nil { + return nil, err + } + + items, err := checkUpdateItemResponseForErrors(bb) + if err != nil { + return nil, err + } + + return items.CalendarItem, nil +} + +func checkUpdateItemResponseForErrors(bb []byte) (items ResponseItems, err error) { + var soapResp updateItemResponseBodyEnvelop + if err = xml.Unmarshal(bb, &soapResp); err != nil { + return + } + + resp := soapResp.Body.UpdateItemResponse.ResponseMessages.UpdateItemResponseMessage + if resp.ResponseClass == ResponseClassError { + err = errors.New(resp.MessageText) + return + } + return resp.ResponseItems, nil +} diff --git a/update_item_test.go b/update_item_test.go new file mode 100644 index 0000000..b25ccc2 --- /dev/null +++ b/update_item_test.go @@ -0,0 +1,103 @@ +package ews + +import ( + "encoding/xml" + "log" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func Test_marshal_UpdateItems(t *testing.T) { + + attendee := make([]Attendee, 0) + attendee = append(attendee, + Attendee{Mailbox: Mailbox{EmailAddress: "User1@example.com"}}, + Attendee{Mailbox: Mailbox{EmailAddress: "User2@example.com"}}, + ) + attendees := make([]Attendees, 0) + attendees = append(attendees, Attendees{Attendee: attendee}) + + start, _ := time.Parse(time.RFC3339, "2006-11-02T14:00:00Z") + end, _ := time.Parse(time.RFC3339, "2006-11-02T15:00:00Z") + + citem := &CalendarItem{ + Subject: "Planning Meeting", + Body: Body{ + BodyType: "Text", + Body: []byte("Plan the agenda for next week's meeting."), + }, + Start: start, + End: end, + IsAllDayEvent: false, + LegacyFreeBusyStatus: "Busy", + Location: "Conference Room 721", + RequiredAttendees: attendees, + } + + uitem := &Updates{ + Updates: getSetItemField("calendar", getFields(citem)...), + } + + xmlBytes, err := xml.MarshalIndent(uitem, "", " ") + if err != nil { + log.Fatal(err) + } + + assert.Equal(t, ` + + + + Planning Meeting + + + + + + Plan the agenda for next week's meeting. + + + + + + 2006-11-02T14:00:00Z + + + + + + 2006-11-02T15:00:00Z + + + + + + Busy + + + + + + Conference Room 721 + + + + + + + + + User1@example.com + + + + + User2@example.com + + + + + +`, string(xmlBytes)) +}