diff --git a/src/internal/connector/exchange/exchange_vars.go b/src/internal/connector/exchange/exchange_vars.go index d27d64708..3274a4ae4 100644 --- a/src/internal/connector/exchange/exchange_vars.go +++ b/src/internal/connector/exchange/exchange_vars.go @@ -26,6 +26,12 @@ const ( // MailReceiveDateTimeOverrideProperty allows receive date time to be updated. // Section: 2.789 PidTagMessageDeliveryTime MailReceiveDateTimeOverriveProperty = "SystemTime 0x0E06" + + //---------------------------------- + // Default Folder Names + //------------------------ + // Mail Definitions: https://docs.microsoft.com/en-us/graph/api/resources/mailfolder?view=graph-rest-1.0 + DefaultContactFolder = "Contacts" ) // descendable represents objects that implement msgraph-sdk-go/models.entityable diff --git a/src/internal/connector/exchange/iterators_test.go b/src/internal/connector/exchange/iterators_test.go index 272b3ca21..d9b66b3d1 100644 --- a/src/internal/connector/exchange/iterators_test.go +++ b/src/internal/connector/exchange/iterators_test.go @@ -143,7 +143,7 @@ func (suite *ExchangeIteratorSuite) TestIterativeFunctions() { scope: eventScope, transformer: models.CreateCalendarCollectionResponseFromDiscriminatorValue, }, { - name: "Folder Iterative Check", + name: "Folder Iterative Check Mail", queryFunction: GetAllFolderNamesForUser, iterativeFunction: IterateFilterFolderDirectoriesForCollections, scope: mailScope, @@ -153,6 +153,12 @@ func (suite *ExchangeIteratorSuite) TestIterativeFunctions() { "Sent Items": {}, "Deleted Items": {}, }, + }, { + name: "Folder Iterative Check Contacts", + queryFunction: GetAllContactFolderNamesForUser, + iterativeFunction: IterateFilterFolderDirectoriesForCollections, + scope: contactScope, + transformer: models.CreateContactFolderCollectionResponseFromDiscriminatorValue, }, } for _, test := range tests { diff --git a/src/internal/connector/exchange/query_options.go b/src/internal/connector/exchange/query_options.go index 2bc347cca..6308c9af3 100644 --- a/src/internal/connector/exchange/query_options.go +++ b/src/internal/connector/exchange/query_options.go @@ -16,6 +16,7 @@ import ( "github.com/pkg/errors" "github.com/alcionai/corso/src/internal/path" + "github.com/alcionai/corso/src/pkg/selectors" ) //----------------------------------------------------------------------- @@ -115,6 +116,19 @@ func categoryToOptionIdentifier(category path.CategoryType) optionIdentifier { } } +func scopeToOptionIdentifier(selector selectors.ExchangeScope) optionIdentifier { + switch selector.Category() { + case selectors.ExchangeMailFolder, selectors.ExchangeMail: + return messages + case selectors.ExchangeContactFolder, selectors.ExchangeContact: + return contacts + case selectors.ExchangeEventCalendar, selectors.ExchangeEvent: + return events + default: + return unknown + } +} + //--------------------------------------------------------------------------- // exchange.Query Option Section // These functions can be used to filter a response on M365 diff --git a/src/internal/connector/exchange/service_functions.go b/src/internal/connector/exchange/service_functions.go index 539590907..04a8aa3a1 100644 --- a/src/internal/connector/exchange/service_functions.go +++ b/src/internal/connector/exchange/service_functions.go @@ -335,14 +335,7 @@ func SetupExchangeCollectionVars(scope selectors.ExchangeScope) ( return models.CreateMessageCollectionResponseFromDiscriminatorValue, GetAllMessagesForUser, - IterateAndFilterMessagesForCollections, - nil - } - - if scope.IncludesCategory(selectors.ExchangeEvent) { - return models.CreateCalendarCollectionResponseFromDiscriminatorValue, - GetAllCalendarNamesForUser, - IterateSelectAllEventsFromCalendars, + IterateAndFilterDescendablesForCollections, nil } @@ -353,6 +346,13 @@ func SetupExchangeCollectionVars(scope selectors.ExchangeScope) ( nil } + if scope.IncludesCategory(selectors.ExchangeEvent) { + return models.CreateCalendarCollectionResponseFromDiscriminatorValue, + GetAllCalendarNamesForUser, + IterateSelectAllEventsFromCalendars, + nil + } + return nil, nil, nil, errors.New("exchange scope option not supported") } diff --git a/src/internal/connector/exchange/service_iterators.go b/src/internal/connector/exchange/service_iterators.go index 5dd8640a4..a2659fe7d 100644 --- a/src/internal/connector/exchange/service_iterators.go +++ b/src/internal/connector/exchange/service_iterators.go @@ -206,10 +206,10 @@ func IterateSelectAllEventsFromCalendars( } } -// IterateAndFilterMessagesForCollections is a filtering GraphIterateFunc -// that places exchange mail message ids belonging to specific directories +// IterateAndFilterDescendablesForCollections is a filtering GraphIterateFunc +// that places exchange objectsids belonging to specific directories // into a Collection. Messages outside of those directories are omitted. -func IterateAndFilterMessagesForCollections( +func IterateAndFilterDescendablesForCollections( ctx context.Context, qp graph.QueryParams, errUpdater func(string, error), @@ -218,9 +218,9 @@ func IterateAndFilterMessagesForCollections( ) func(any) bool { var isFilterSet bool - return func(messageItem any) bool { + return func(descendItem any) bool { if !isFilterSet { - err := CollectMailFolders( + err := CollectFolders( ctx, qp, collections, @@ -234,7 +234,7 @@ func IterateAndFilterMessagesForCollections( isFilterSet = true } - message, ok := messageItem.(descendable) + message, ok := descendItem.(descendable) if !ok { errUpdater(qp.User, errors.New("casting messageItem to descendable")) return true @@ -259,38 +259,66 @@ func IterateFilterFolderDirectoriesForCollections( statusUpdater support.StatusUpdater, ) func(any) bool { var ( - service graph.Service - err error + resolver graph.ContainerResolver + isSet bool + collectPath string + err error + option optionIdentifier + category path.CategoryType + validate func(string) bool ) - resolver, err := maybeGetAndPopulateFolderResolver(ctx, qp, path.EmailCategory) - if err != nil { - errUpdater("getting folder resolver for category email", err) - } - return func(folderItem any) bool { folder, ok := folderItem.(displayable) if !ok { errUpdater(qp.User, errors.New("casting folderItem to displayable")) return true } + + if !isSet { + option = scopeToOptionIdentifier(qp.Scope) + switch option { + case messages: + category = path.EmailCategory + validate = func(name string) bool { + return !qp.Scope.Matches(selectors.ExchangeMailFolder, name) + } + case contacts: + category = path.ContactsCategory + validate = func(name string) bool { + return !qp.Scope.Matches(selectors.ExchangeContactFolder, name) + } + } + + resolver, err = maybeGetAndPopulateFolderResolver(ctx, qp, category) + if err != nil { + errUpdater("getting folder resolver for category "+category.String(), err) + } + + isSet = true + } + // Continue to iterate if folder name is empty if folder.GetDisplayName() == nil { return true } - if !qp.Scope.Matches(selectors.ExchangeMailFolder, *folder.GetDisplayName()) { + if validate(*folder.GetDisplayName()) { return true } - directory := *folder.GetId() + if option == contacts { + collectPath = *folder.GetDisplayName() + } else { + collectPath = *folder.GetId() + } dirPath, err := getCollectionPath( ctx, qp, resolver, - directory, - path.EmailCategory, + collectPath, + category, ) if err != nil { errUpdater( @@ -301,7 +329,7 @@ func IterateFilterFolderDirectoriesForCollections( return true } - service, err = createService(qp.Credentials, qp.FailFast) + service, err := createService(qp.Credentials, qp.FailFast) if err != nil { errUpdater( *folder.GetDisplayName(), @@ -313,11 +341,11 @@ func IterateFilterFolderDirectoriesForCollections( temp := NewCollection( qp.User, dirPath, - messages, + option, service, statusUpdater, ) - collections[directory] = &temp + collections[*folder.GetId()] = &temp return true } @@ -330,7 +358,10 @@ func IterateSelectAllContactsForCollections( collections map[string]*Collection, statusUpdater support.StatusUpdater, ) func(any) bool { - var isPrimarySet bool + var ( + isPrimarySet bool + service graph.Service + ) return func(folderItem any) bool { folder, ok := folderItem.(models.ContactFolderable) @@ -342,7 +373,18 @@ func IterateSelectAllContactsForCollections( } if !isPrimarySet && folder.GetParentFolderId() != nil { - service, err := createService(qp.Credentials, qp.FailFast) + err := CollectFolders( + ctx, + qp, + collections, + statusUpdater, + ) + if err != nil { + errUpdater(qp.User, err) + return false + } + + service, err = createService(qp.Credentials, qp.FailFast) if err != nil { errUpdater( qp.User, @@ -352,44 +394,58 @@ func IterateSelectAllContactsForCollections( return true } - contactIDS, err := ReturnContactIDsFromDirectory(service, qp.User, *folder.GetParentFolderId()) - if err != nil { - errUpdater( + // Create and Populate Default Contacts folder Collection if true + if qp.Scope.Matches(selectors.ExchangeContactFolder, DefaultContactFolder) { + dirPath, err := path.Builder{}.Append(DefaultContactFolder).ToDataLayerExchangePathForCategory( + qp.Credentials.TenantID, qp.User, - err, + path.ContactsCategory, + false, + ) + if err != nil { + errUpdater( + qp.User, + err, + ) + + return false + } + + edc := NewCollection( + qp.User, + dirPath, + contacts, + service, + statusUpdater, ) - return true + listOfIDs, err := ReturnContactIDsFromDirectory(service, qp.User, *folder.GetParentFolderId()) + if err != nil { + errUpdater( + qp.User, + err, + ) + + return false + } + + edc.jobs = append(edc.jobs, listOfIDs...) + collections[DefaultContactFolder] = &edc + isPrimarySet = true } - - dirPath, err := path.Builder{}.Append(*folder.GetParentFolderId()).ToDataLayerExchangePathForCategory( - qp.Credentials.TenantID, - qp.User, - path.ContactsCategory, - false, - ) - if err != nil { - errUpdater( - qp.User, - err, - ) - - return true - } - - edc := NewCollection( - qp.User, - dirPath, - contacts, - service, - statusUpdater, - ) - edc.jobs = append(edc.jobs, contactIDS...) - collections["Contacts"] = &edc - isPrimarySet = true } - service, err := createService(qp.Credentials, qp.FailFast) + if folder.GetDisplayName() == nil { + // This should never happen. Skipping to avoid kernel panic + return true + } + + collection, ok := collections[*folder.GetDisplayName()] + if !ok { + return true // Not included + } + + listOfIDs, err := ReturnContactIDsFromDirectory(service, qp.User, *folder.GetId()) if err != nil { errUpdater( qp.User, @@ -399,49 +455,7 @@ func IterateSelectAllContactsForCollections( return true } - folderID := *folder.GetId() - - listOfIDs, err := ReturnContactIDsFromDirectory(service, qp.User, folderID) - if err != nil { - errUpdater( - qp.User, - err, - ) - - return true - } - - if folder.GetDisplayName() == nil || - listOfIDs == nil { - return true // Invalid state TODO: How should this be named - } - - directory := *folder.GetDisplayName() - - dirPath, err := path.Builder{}.Append(directory).ToDataLayerExchangePathForCategory( - qp.Credentials.TenantID, - qp.User, - path.ContactsCategory, - false, - ) - if err != nil { - errUpdater( - qp.User, - err, - ) - - return true - } - - edc := NewCollection( - qp.User, - dirPath, - contacts, - service, - statusUpdater, - ) - edc.jobs = append(edc.jobs, listOfIDs...) - collections[directory] = &edc + collection.jobs = append(collection.jobs, listOfIDs...) return true } diff --git a/src/internal/connector/exchange/service_query.go b/src/internal/connector/exchange/service_query.go index 2f717cab4..a2dd1d369 100644 --- a/src/internal/connector/exchange/service_query.go +++ b/src/internal/connector/exchange/service_query.go @@ -122,18 +122,38 @@ func RetrieveMessageDataForUser(gs graph.Service, user, m365ID string) (absser.P return gs.Client().UsersById(user).MessagesById(m365ID).Get() } -func CollectMailFolders( +func CollectFolders( ctx context.Context, qp graph.QueryParams, collections map[string]*Collection, statusUpdater support.StatusUpdater, ) error { - queryService, err := createService(qp.Credentials, qp.FailFast) + var ( + query GraphQuery + transformer absser.ParsableFactory + queryService, err = createService(qp.Credentials, qp.FailFast) + ) + if err != nil { - return errors.New("unable to create a mail folder query service for " + qp.User) + return errors.Wrapf( + err, + "unable to create graph.Service within CollectFolders service for "+qp.User, + ) } - query, err := GetAllFolderNamesForUser(queryService, qp.User) + option := scopeToOptionIdentifier(qp.Scope) + switch option { + case messages: + query = GetAllFolderNamesForUser + transformer = models.CreateMailFolderCollectionResponseFromDiscriminatorValue + case contacts: + query = GetAllContactFolderNamesForUser + transformer = models.CreateContactFolderCollectionResponseFromDiscriminatorValue + default: + return fmt.Errorf("unsupported option %s used in CollectFolders", option) + } + + response, err := query(queryService, qp.User) if err != nil { return fmt.Errorf( "unable to query mail folder for %s: details: %s", @@ -145,9 +165,9 @@ func CollectMailFolders( // Iterator required to ensure all potential folders are inspected // when the breadth of the folder space is large pageIterator, err := msgraphgocore.NewPageIterator( - query, + response, &queryService.adapter, - models.CreateMailFolderCollectionResponseFromDiscriminatorValue) + transformer) if err != nil { return errors.Wrap(err, "unable to create iterator during mail folder query service") }