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:
parent
423b6e19f7
commit
f250665dd6
@ -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
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user