GC: Backup: Contacts: Ability to filter on folder [FEATURE] (#891)

## Description
PR enables Filtering for `exchange.Contact` items wherein if an `exchange.ContactFolder` is requested specifically, only the requested folders are backed up. 
<!-- Insert PR description-->

## Type of change

<!--- Please check the type of change your PR introduces: --->
- [x] 🌻 Feature


## Issue(s)

<!-- Can reference multiple issues. Use one of the following "magic words" - "closes, fixes" to auto-close the Github issue. -->
*closes  #811<issue>

## Test Plan

<!-- How will this be tested prior to merging.-->
- [ ] 💪 Manual
- [x]  Unit test
- [ ] 💚 E2E
This commit is contained in:
Danny 2022-09-19 13:42:12 -04:00 committed by GitHub
parent 423b6e19f7
commit f250665dd6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 172 additions and 112 deletions

View File

@ -26,6 +26,12 @@ const (
// MailReceiveDateTimeOverrideProperty allows receive date time to be updated. // MailReceiveDateTimeOverrideProperty allows receive date time to be updated.
// Section: 2.789 PidTagMessageDeliveryTime // Section: 2.789 PidTagMessageDeliveryTime
MailReceiveDateTimeOverriveProperty = "SystemTime 0x0E06" 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 // descendable represents objects that implement msgraph-sdk-go/models.entityable

View File

@ -143,7 +143,7 @@ func (suite *ExchangeIteratorSuite) TestIterativeFunctions() {
scope: eventScope, scope: eventScope,
transformer: models.CreateCalendarCollectionResponseFromDiscriminatorValue, transformer: models.CreateCalendarCollectionResponseFromDiscriminatorValue,
}, { }, {
name: "Folder Iterative Check", name: "Folder Iterative Check Mail",
queryFunction: GetAllFolderNamesForUser, queryFunction: GetAllFolderNamesForUser,
iterativeFunction: IterateFilterFolderDirectoriesForCollections, iterativeFunction: IterateFilterFolderDirectoriesForCollections,
scope: mailScope, scope: mailScope,
@ -153,6 +153,12 @@ func (suite *ExchangeIteratorSuite) TestIterativeFunctions() {
"Sent Items": {}, "Sent Items": {},
"Deleted Items": {}, "Deleted Items": {},
}, },
}, {
name: "Folder Iterative Check Contacts",
queryFunction: GetAllContactFolderNamesForUser,
iterativeFunction: IterateFilterFolderDirectoriesForCollections,
scope: contactScope,
transformer: models.CreateContactFolderCollectionResponseFromDiscriminatorValue,
}, },
} }
for _, test := range tests { for _, test := range tests {

View File

@ -16,6 +16,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/alcionai/corso/src/internal/path" "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 // exchange.Query Option Section
// These functions can be used to filter a response on M365 // These functions can be used to filter a response on M365

View File

@ -335,14 +335,7 @@ func SetupExchangeCollectionVars(scope selectors.ExchangeScope) (
return models.CreateMessageCollectionResponseFromDiscriminatorValue, return models.CreateMessageCollectionResponseFromDiscriminatorValue,
GetAllMessagesForUser, GetAllMessagesForUser,
IterateAndFilterMessagesForCollections, IterateAndFilterDescendablesForCollections,
nil
}
if scope.IncludesCategory(selectors.ExchangeEvent) {
return models.CreateCalendarCollectionResponseFromDiscriminatorValue,
GetAllCalendarNamesForUser,
IterateSelectAllEventsFromCalendars,
nil nil
} }
@ -353,6 +346,13 @@ func SetupExchangeCollectionVars(scope selectors.ExchangeScope) (
nil nil
} }
if scope.IncludesCategory(selectors.ExchangeEvent) {
return models.CreateCalendarCollectionResponseFromDiscriminatorValue,
GetAllCalendarNamesForUser,
IterateSelectAllEventsFromCalendars,
nil
}
return nil, nil, nil, errors.New("exchange scope option not supported") return nil, nil, nil, errors.New("exchange scope option not supported")
} }

View File

@ -206,10 +206,10 @@ func IterateSelectAllEventsFromCalendars(
} }
} }
// IterateAndFilterMessagesForCollections is a filtering GraphIterateFunc // IterateAndFilterDescendablesForCollections is a filtering GraphIterateFunc
// that places exchange mail message ids belonging to specific directories // that places exchange objectsids belonging to specific directories
// into a Collection. Messages outside of those directories are omitted. // into a Collection. Messages outside of those directories are omitted.
func IterateAndFilterMessagesForCollections( func IterateAndFilterDescendablesForCollections(
ctx context.Context, ctx context.Context,
qp graph.QueryParams, qp graph.QueryParams,
errUpdater func(string, error), errUpdater func(string, error),
@ -218,9 +218,9 @@ func IterateAndFilterMessagesForCollections(
) func(any) bool { ) func(any) bool {
var isFilterSet bool var isFilterSet bool
return func(messageItem any) bool { return func(descendItem any) bool {
if !isFilterSet { if !isFilterSet {
err := CollectMailFolders( err := CollectFolders(
ctx, ctx,
qp, qp,
collections, collections,
@ -234,7 +234,7 @@ func IterateAndFilterMessagesForCollections(
isFilterSet = true isFilterSet = true
} }
message, ok := messageItem.(descendable) message, ok := descendItem.(descendable)
if !ok { if !ok {
errUpdater(qp.User, errors.New("casting messageItem to descendable")) errUpdater(qp.User, errors.New("casting messageItem to descendable"))
return true return true
@ -259,38 +259,66 @@ func IterateFilterFolderDirectoriesForCollections(
statusUpdater support.StatusUpdater, statusUpdater support.StatusUpdater,
) func(any) bool { ) func(any) bool {
var ( var (
service graph.Service resolver graph.ContainerResolver
isSet bool
collectPath string
err error 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 { return func(folderItem any) bool {
folder, ok := folderItem.(displayable) folder, ok := folderItem.(displayable)
if !ok { if !ok {
errUpdater(qp.User, errors.New("casting folderItem to displayable")) errUpdater(qp.User, errors.New("casting folderItem to displayable"))
return true 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 // Continue to iterate if folder name is empty
if folder.GetDisplayName() == nil { if folder.GetDisplayName() == nil {
return true return true
} }
if !qp.Scope.Matches(selectors.ExchangeMailFolder, *folder.GetDisplayName()) { if validate(*folder.GetDisplayName()) {
return true return true
} }
directory := *folder.GetId() if option == contacts {
collectPath = *folder.GetDisplayName()
} else {
collectPath = *folder.GetId()
}
dirPath, err := getCollectionPath( dirPath, err := getCollectionPath(
ctx, ctx,
qp, qp,
resolver, resolver,
directory, collectPath,
path.EmailCategory, category,
) )
if err != nil { if err != nil {
errUpdater( errUpdater(
@ -301,7 +329,7 @@ func IterateFilterFolderDirectoriesForCollections(
return true return true
} }
service, err = createService(qp.Credentials, qp.FailFast) service, err := createService(qp.Credentials, qp.FailFast)
if err != nil { if err != nil {
errUpdater( errUpdater(
*folder.GetDisplayName(), *folder.GetDisplayName(),
@ -313,11 +341,11 @@ func IterateFilterFolderDirectoriesForCollections(
temp := NewCollection( temp := NewCollection(
qp.User, qp.User,
dirPath, dirPath,
messages, option,
service, service,
statusUpdater, statusUpdater,
) )
collections[directory] = &temp collections[*folder.GetId()] = &temp
return true return true
} }
@ -330,7 +358,10 @@ func IterateSelectAllContactsForCollections(
collections map[string]*Collection, collections map[string]*Collection,
statusUpdater support.StatusUpdater, statusUpdater support.StatusUpdater,
) func(any) bool { ) func(any) bool {
var isPrimarySet bool var (
isPrimarySet bool
service graph.Service
)
return func(folderItem any) bool { return func(folderItem any) bool {
folder, ok := folderItem.(models.ContactFolderable) folder, ok := folderItem.(models.ContactFolderable)
@ -342,7 +373,18 @@ func IterateSelectAllContactsForCollections(
} }
if !isPrimarySet && folder.GetParentFolderId() != nil { 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 { if err != nil {
errUpdater( errUpdater(
qp.User, qp.User,
@ -352,17 +394,9 @@ func IterateSelectAllContactsForCollections(
return true return true
} }
contactIDS, err := ReturnContactIDsFromDirectory(service, qp.User, *folder.GetParentFolderId()) // Create and Populate Default Contacts folder Collection if true
if err != nil { if qp.Scope.Matches(selectors.ExchangeContactFolder, DefaultContactFolder) {
errUpdater( dirPath, err := path.Builder{}.Append(DefaultContactFolder).ToDataLayerExchangePathForCategory(
qp.User,
err,
)
return true
}
dirPath, err := path.Builder{}.Append(*folder.GetParentFolderId()).ToDataLayerExchangePathForCategory(
qp.Credentials.TenantID, qp.Credentials.TenantID,
qp.User, qp.User,
path.ContactsCategory, path.ContactsCategory,
@ -374,7 +408,7 @@ func IterateSelectAllContactsForCollections(
err, err,
) )
return true return false
} }
edc := NewCollection( edc := NewCollection(
@ -384,12 +418,34 @@ func IterateSelectAllContactsForCollections(
service, service,
statusUpdater, statusUpdater,
) )
edc.jobs = append(edc.jobs, contactIDS...)
collections["Contacts"] = &edc 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 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 { if err != nil {
errUpdater( errUpdater(
qp.User, qp.User,
@ -399,49 +455,7 @@ func IterateSelectAllContactsForCollections(
return true return true
} }
folderID := *folder.GetId() collection.jobs = append(collection.jobs, listOfIDs...)
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
return true return true
} }

View File

@ -122,18 +122,38 @@ func RetrieveMessageDataForUser(gs graph.Service, user, m365ID string) (absser.P
return gs.Client().UsersById(user).MessagesById(m365ID).Get() return gs.Client().UsersById(user).MessagesById(m365ID).Get()
} }
func CollectMailFolders( func CollectFolders(
ctx context.Context, ctx context.Context,
qp graph.QueryParams, qp graph.QueryParams,
collections map[string]*Collection, collections map[string]*Collection,
statusUpdater support.StatusUpdater, statusUpdater support.StatusUpdater,
) error { ) error {
queryService, err := createService(qp.Credentials, qp.FailFast) var (
query GraphQuery
transformer absser.ParsableFactory
queryService, err = createService(qp.Credentials, qp.FailFast)
)
if err != nil { 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 { if err != nil {
return fmt.Errorf( return fmt.Errorf(
"unable to query mail folder for %s: details: %s", "unable to query mail folder for %s: details: %s",
@ -145,9 +165,9 @@ func CollectMailFolders(
// Iterator required to ensure all potential folders are inspected // Iterator required to ensure all potential folders are inspected
// when the breadth of the folder space is large // when the breadth of the folder space is large
pageIterator, err := msgraphgocore.NewPageIterator( pageIterator, err := msgraphgocore.NewPageIterator(
query, response,
&queryService.adapter, &queryService.adapter,
models.CreateMailFolderCollectionResponseFromDiscriminatorValue) transformer)
if err != nil { if err != nil {
return errors.Wrap(err, "unable to create iterator during mail folder query service") return errors.Wrap(err, "unable to create iterator during mail folder query service")
} }