diff --git a/src/internal/connector/graph_connector.go b/src/internal/connector/graph_connector.go index 4f655c3e8..0238e3db7 100644 --- a/src/internal/connector/graph_connector.go +++ b/src/internal/connector/graph_connector.go @@ -15,7 +15,6 @@ import ( msuser "github.com/microsoftgraph/msgraph-sdk-go/users" "github.com/pkg/errors" - "github.com/alcionai/corso/internal/connector/exchange" "github.com/alcionai/corso/internal/connector/support" "github.com/alcionai/corso/pkg/account" "github.com/alcionai/corso/pkg/logger" @@ -308,7 +307,6 @@ func (gc *GraphConnector) serializeMessages(ctx context.Context, user string) ([ return collections, errs } -// messageToDataCollection transfers message objects to objects within DataCollection func (gc *GraphConnector) messageToDataCollection( ctx context.Context, objectWriter *kw.JsonSerializationWriter, @@ -316,39 +314,49 @@ func (gc *GraphConnector) messageToDataCollection( message models.Messageable, user string, ) error { - if *message.GetHasAttachments() { + var err error + aMessage := message + adtl := message.GetAdditionalData() + if len(adtl) > 2 { + aMessage, err = support.ConvertFromMessageable(adtl, message) + if err != nil { + return err + } + } + if *aMessage.GetHasAttachments() { // getting all the attachments might take a couple attempts due to filesize var retriesErr error for count := 0; count < numberOfRetries; count++ { attached, err := gc.client. UsersById(user). - MessagesById(*message.GetId()). + MessagesById(*aMessage.GetId()). Attachments(). Get() retriesErr = err if err == nil && attached != nil { - message.SetAttachments(attached.GetValue()) + aMessage.SetAttachments(attached.GetValue()) break } } if retriesErr != nil { logger.Ctx(ctx).Debug("exceeded maximum retries") - return support.WrapAndAppend(*message.GetId(), errors.Wrap(retriesErr, "attachment failed"), nil) + return support.WrapAndAppend(*aMessage.GetId(), errors.Wrap(retriesErr, "attachment failed"), nil) } } - defer objectWriter.Close() - err := objectWriter.WriteObjectValue("", message) + err = objectWriter.WriteObjectValue("", aMessage) if err != nil { - return support.SetNonRecoverableError(errors.Wrapf(err, "%s", *message.GetId())) + return support.SetNonRecoverableError(errors.Wrapf(err, "%s", *aMessage.GetId())) } byteArray, err := objectWriter.GetSerializedContent() + objectWriter.Close() if err != nil { - return support.WrapAndAppend(*message.GetId(), errors.Wrap(err, "serializing mail content"), nil) + return support.WrapAndAppend(*aMessage.GetId(), errors.Wrap(err, "serializing mail content"), nil) } if byteArray != nil { - edc.PopulateCollection(&ExchangeData{id: *message.GetId(), message: byteArray, info: exchange.MessageInfo(message)}) + edc.PopulateCollection(&ExchangeData{id: *aMessage.GetId(), message: byteArray}) } + return nil } diff --git a/src/internal/connector/graph_connector_test.go b/src/internal/connector/graph_connector_test.go index 90f626c3a..9a30c99a8 100644 --- a/src/internal/connector/graph_connector_test.go +++ b/src/internal/connector/graph_connector_test.go @@ -58,17 +58,12 @@ func (suite *GraphConnectorIntegrationSuite) TestGraphConnector_setTenantUsers() } func (suite *GraphConnectorIntegrationSuite) TestGraphConnector_ExchangeDataCollection() { - t := suite.T() - sel := selectors.NewExchangeBackup() sel.Include(sel.Users("lidiah@8qzvrj.onmicrosoft.com")) collectionList, err := suite.connector.ExchangeDataCollection(context.Background(), sel.Selector) - - require.NotNil(t, collectionList, "collection list") - assert.Error(t, err) // TODO Remove after https://github.com/alcionai/corso/issues/140 - assert.NotNil(t, suite.connector.status, "connector status") - assert.NotContains(t, err.Error(), "attachment failed") // TODO Create Retry Exceeded Error - + assert.NotNil(suite.T(), collectionList, "collection list") + assert.Nil(suite.T(), err) + assert.NotNil(suite.T(), suite.connector.status, "connector status") exchangeData := collectionList[0] suite.Greater(len(exchangeData.FullPath()), 2) } diff --git a/src/internal/connector/support/m365Transform.go b/src/internal/connector/support/m365Transform.go index 55f84d20d..ee71bb9fb 100644 --- a/src/internal/connector/support/m365Transform.go +++ b/src/internal/connector/support/m365Transform.go @@ -1,13 +1,19 @@ package support import ( + "fmt" + "strconv" + + kw "github.com/microsoft/kiota-serialization-json-go" + "github.com/pkg/errors" + "github.com/microsoftgraph/msgraph-sdk-go/models" ) -// ToMessage transfers all data from old message to new -// message except for the messageId. -func ToMessage(orig models.Messageable) *models.Message { - message := models.NewMessage() +var eventResponsableFields = []string{"responseType"} +var eventRequestableFields = []string{"allowNewTimeProposals", "meetingRequestType", "responseRequested"} + +func CloneMessageableFields(orig, message models.Messageable) models.Messageable { message.SetSubject(orig.GetSubject()) message.SetBodyPreview(orig.GetBodyPreview()) message.SetBody(orig.GetBody()) @@ -38,5 +44,286 @@ func ToMessage(orig models.Messageable) *models.Message { message.SetUniqueBody(orig.GetUniqueBody()) message.SetWebLink(orig.GetWebLink()) return message - +} + +func ToMessage(orig models.Messageable) models.Messageable { + message := models.NewMessage() + temp := CloneMessageableFields(orig, message) + aMessage, ok := temp.(*models.Message) + if !ok { + return nil + } + return aMessage +} + +func SetEventMessageRequest(orig models.Messageable, adtl map[string]any) (models.EventMessageRequestable, error) { + aMessage := models.NewEventMessageRequest() + temp := CloneMessageableFields(orig, aMessage) + message, ok := temp.(models.EventMessageRequestable) + if !ok { + return nil, errors.New(*orig.GetId() + " failed to convert to eventMessageRequestable") + } + newMessage, err := SetAdditionalDataToEventMessage(adtl, message) + if err != nil { + return nil, errors.Wrap(err, *orig.GetId()+" eventMessageRequest could not set additional data") + } + additional, err := buildMapFromAdditional(eventRequestableFields, adtl) + if err != nil { + return nil, errors.Wrap(err, *orig.GetId()+" eventMessageRequest failed on method buildMapFromAdditional") + } + message, ok = newMessage.(models.EventMessageRequestable) + if !ok { + return nil, errors.New(*orig.GetId() + " failed to convert to eventMessageRequestable") + } + eventMessage, err := setEventRequestableFields(message, additional) + if err != nil { + return nil, err + } + return eventMessage, nil +} + +func SetEventMessageResponse(orig models.Messageable, adtl map[string]any) (models.EventMessageResponseable, error) { + aMessage := models.NewEventMessageResponse() + temp := CloneMessageableFields(orig, aMessage) + message, ok := temp.(models.EventMessageResponseable) + if !ok { + return nil, errors.New(*orig.GetId() + " failed to convert to eventMessageRequestable") + } + + newMessage, err := SetAdditionalDataToEventMessage(adtl, message) + if err != nil { + return nil, errors.Wrap(err, *orig.GetId()+" eventMessageResponse could not set additional data") + + } + message, ok = newMessage.(models.EventMessageResponseable) + if !ok { + return nil, errors.New("unable to create event message responseable from " + *orig.GetId()) + } + additional, err := buildMapFromAdditional(eventResponsableFields, adtl) + if err != nil { + return nil, errors.Wrap(err, *orig.GetId()+" eventMessageResponse failed on method buildMapFromAdditional") + } + for key, val := range additional { + switch key { + case "responseType": + temp, err := models.ParseResponseType(*val) + if err != nil { + return nil, errors.Wrap(err, *orig.GetId()+"failure to parse response type") + } + rType, ok := temp.(*models.ResponseType) + if !ok { + return nil, fmt.Errorf("%s : responseType not returned from models.ParseResponseType: %v\t%T", *orig.GetId(), temp, temp) + } + message.SetResponseType(rType) + default: + return nil, errors.New(key + " not supported for setEventMessageResponse") + } + + } + return message, nil +} + +// ConvertFromMessageable temporary function. Converts incorrect cast of messageable object to known +// type until upstream can make the appropriate changes +func ConvertFromMessageable(adtl map[string]any, orig models.Messageable) (models.EventMessageable, error) { + var aType string + aPointer, ok := adtl["@odata.type"] + if !ok { + return nil, errors.New("unknown data type: no @odata.type field") + } + ptr, ok := aPointer.(*string) + if !ok { + return nil, errors.New("unknown map type encountered") + } + aType = *ptr + if aType == "#microsoft.graph.eventMessageRequest" { + eventRequest, err := SetEventMessageRequest(orig, adtl) + if err != nil { + return nil, err + } + eventRequest.SetId(orig.GetId()) + return eventRequest, err + } + if aType == "#microsoft.graph.eventMessageResponse" { + eventMessage, err := SetEventMessageResponse(orig, adtl) + if err != nil { + return nil, err + } + eventMessage.SetId(orig.GetId()) + + return eventMessage, nil + } + + return nil, errors.New("unknown data type: " + aType) +} + +// buildMapFromAdditional returns a submap of map[string]*string from map[string]any +func buildMapFromAdditional(list []string, adtl map[string]any) (map[string]*string, error) { + returnMap := make(map[string]*string) + for _, entry := range list { + ptr, ok := adtl[entry] + if !ok { + continue + } + value, ok := ptr.(*string) + if !ok { + boolConvert, ok := ptr.(*bool) + if !ok { + return nil, errors.New("unsupported value type: key: " + entry + fmt.Sprintf(" with type: %T", ptr)) + } + aBool := *boolConvert + boolString := strconv.FormatBool(aBool) + returnMap[entry] = &boolString + continue + } + returnMap[entry] = value + } + return returnMap, nil +} + +func setEventRequestableFields(em models.EventMessageRequestable, adtl map[string]*string) (models.EventMessageRequestable, error) { + + for key, value := range adtl { + switch key { + case "meetingRequestType": + temp, err := models.ParseMeetingRequestType(*value) + if err != nil { + return nil, errors.Wrap(err, *em.GetId()+": failed on models.ParseMeetingRequestType") + } + rType, ok := temp.(*models.MeetingRequestType) + if !ok { + return nil, errors.New(*em.GetId() + ": failed to set meeting request type") + + } + em.SetMeetingRequestType(rType) + case "responseRequested": + boolValue, err := strconv.ParseBool(*value) + if err != nil { + return nil, errors.Wrap(err, *em.GetId()+": failed to set responseRequested") + } + em.SetResponseRequested(&boolValue) + case "allowNewTimeProposals": + boolValue, err := strconv.ParseBool(*value) + if err != nil { + return nil, errors.Wrap(err, *em.GetId()+": failed to set allowNewTimeProposals") + } + em.SetAllowNewTimeProposals(&boolValue) + } + } + return em, nil +} + +// SetAdditionalDataToEventMessage sets shared fields for 2 types of EventMessage: Response and Request +func SetAdditionalDataToEventMessage(adtl map[string]any, newMessage models.EventMessageable) (models.EventMessageable, error) { + for key, entry := range adtl { + if key == "endDateTime" { + dateTime := models.NewDateTimeTimeZone() + mapped, ok := entry.(map[string]*kw.JsonParseNode) + if ok { + for key, val := range mapped { + node := *val + value, err := node.GetStringValue() + if err != nil { + return nil, err + } + switch key { + case "dateTime": + dateTime.SetDateTime(value) + case "timeZone": + dateTime.SetTimeZone(value) + default: + return nil, errors.New("key not supported DateTime") + } + newMessage.SetEndDateTime(dateTime) + } + continue + } + } + if key == "startDateTime" { + dateTime := models.NewDateTimeTimeZone() + mapped, ok := entry.(map[string]*kw.JsonParseNode) + if ok { + for key, val := range mapped { + node := *val + value, err := node.GetStringValue() + if err != nil { + return nil, err + } + switch key { + case "dateTime": + dateTime.SetDateTime(value) + case "timeZone": + dateTime.SetTimeZone(value) + default: + return nil, errors.New("key not supported DateTime") + + } + newMessage.SetStartDateTime(dateTime) + } + continue + } + } + if key == "location" { + aLocation := models.NewLocation() + mapped, ok := entry.(map[string]*kw.JsonParseNode) + if ok { + for key, val := range mapped { + node := *val + value, err := node.GetStringValue() + if err != nil { + return nil, errors.New("map[string]*JsonParseNode conversion failure") + } + switch key { + case "displayName": + aLocation.SetDisplayName(value) + case "locationType": + temp, err := models.ParseLocationType(*value) + if err != nil { + return nil, errors.New("location type parse failure") + } + lType, ok := temp.(*models.LocationType) + if !ok { + return nil, errors.New("location type interface failure") + } + aLocation.SetLocationType(lType) + } + } + } + newMessage.SetLocation(aLocation) + } + value, ok := entry.(*string) + if ok { + switch key { + case "isAllDay": + boolValue, err := strconv.ParseBool(*value) + if err != nil { + return nil, err + } + newMessage.SetIsAllDay(&boolValue) + case "isDelegated": + boolValue, err := strconv.ParseBool(*value) + if err != nil { + return nil, err + } + newMessage.SetIsDelegated(&boolValue) + case "isOutOfDate": + boolValue, err := strconv.ParseBool(*value) + if err != nil { + return nil, err + } + newMessage.SetIsOutOfDate(&boolValue) + case "meetingMessageType": + temp, err := models.ParseMeetingMessageType(*value) + if err != nil { + return nil, err + } + mType, ok := temp.(*models.MeetingMessageType) + if !ok { + return nil, errors.New("failed to create meeting message type") + } + newMessage.SetMeetingMessageType(mType) + } + } + } + return newMessage, nil }