Contact Restore: Create / Delete Folders

Contact folder creation and deletion enabled by the PR. Test suites expanded
This commit is contained in:
Danny 2022-08-29 17:23:34 -04:00 committed by GitHub
parent 3f6f6604a2
commit b2d3e62536
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 261 additions and 67 deletions

View File

@ -238,6 +238,14 @@ func (suite *ExchangeServiceSuite) TestGraphQueryFunctions() {
name: "GraphQuery: Get All Users",
function: GetAllUsersForTenant,
},
{
name: "GraphQuery: Get All ContactFolders",
function: GetAllContactFolderNamesForUser,
},
{
name: "GraphQuery: Get All Events for User",
function: GetAllEventsForUser,
},
}
for _, test := range tests {
suite.T().Run(test.name, func(t *testing.T) {
@ -291,6 +299,54 @@ func (suite *ExchangeServiceSuite) TestParseCalendarIDFromEvent() {
}
}
// TestGetMailFolderID verifies the ability to retrieve folder ID of folders
// at the top level of the file tree
func (suite *ExchangeServiceSuite) TestGetFolderID() {
userID := tester.M365UserID(suite.T())
tests := []struct {
name string
folderName string
// category references the current optionId :: TODO --> use selector fields
category optionIdentifier
checkError assert.ErrorAssertionFunc
}{
{
name: "Mail Valid",
folderName: "Inbox",
category: messages,
checkError: assert.NoError,
},
{
name: "Mail Invalid",
folderName: "FolderThatIsNotHere",
category: messages,
checkError: assert.Error,
},
{
name: "Contact Invalid",
folderName: "FolderThatIsNotHereContacts",
category: contacts,
checkError: assert.Error,
},
{
name: "Contact Valid",
folderName: "TrialFolder",
category: contacts,
checkError: assert.NoError,
},
}
for _, test := range tests {
suite.T().Run(test.name, func(t *testing.T) {
_, err := GetFolderID(
suite.es,
test.folderName,
userID,
test.category)
test.checkError(t, err, "Unable to find folder: "+test.folderName)
})
}
}
// TestIterativeFunctions verifies that GraphQuery to Iterate
// functions are valid for current versioning of msgraph-go-sdk
func (suite *ExchangeServiceSuite) TestIterativeFunctions() {

View File

@ -13,11 +13,12 @@ func _() {
_ = x[events-2]
_ = x[messages-3]
_ = x[users-4]
_ = x[contacts-5]
}
const _optionIdentifier_name = "unknownfolderseventsmessagesusers"
const _optionIdentifier_name = "unknownfolderseventsmessagesuserscontacts"
var _optionIdentifier_index = [...]uint8{0, 7, 14, 20, 28, 33}
var _optionIdentifier_index = [...]uint8{0, 7, 14, 20, 28, 33, 41}
func (i optionIdentifier) String() string {
if i < 0 || i >= optionIdentifier(len(_optionIdentifier_index)-1) {

View File

@ -2,6 +2,7 @@ package exchange
import (
msuser "github.com/microsoftgraph/msgraph-sdk-go/users"
mscontactfolder "github.com/microsoftgraph/msgraph-sdk-go/users/item/contactfolders"
mscontacts "github.com/microsoftgraph/msgraph-sdk-go/users/item/contacts"
msevents "github.com/microsoftgraph/msgraph-sdk-go/users/item/events"
msfolder "github.com/microsoftgraph/msgraph-sdk-go/users/item/mailfolders"
@ -84,6 +85,25 @@ const (
contacts
)
const (
mailCategory = "mail"
contactsCategory = "contacts"
eventsCategory = "events"
)
func categoryToOptionIdentifier(category string) optionIdentifier {
switch category {
case mailCategory:
return messages
case contactsCategory:
return contacts
case eventsCategory:
return events
default:
return unknown
}
}
//---------------------------------------------------
// exchange.Query Option Section
//------------------------------------------------
@ -122,6 +142,23 @@ func OptionsForSingleMessage(moreOps []string) (*msitem.MessageItemRequestBuilde
return options, nil
}
func optionsForContactFolders(moreOps []string) (
*mscontactfolder.ContactFoldersRequestBuilderGetRequestConfiguration,
error,
) {
selecting, err := buildOptions(moreOps, folders)
if err != nil {
return nil, err
}
requestParameters := &mscontactfolder.ContactFoldersRequestBuilderGetQueryParameters{
Select: selecting,
}
options := &mscontactfolder.ContactFoldersRequestBuilderGetRequestConfiguration{
QueryParameters: requestParameters,
}
return options, nil
}
// optionsForMailFolders transforms the options into a more dynamic call for MailFolders.
// @param moreOps is a []string of options(e.g. "displayName", "isHidden")
// @return is first call in MailFolders().GetWithRequestConfigurationAndResponseHandler(options, handler)

View File

@ -87,6 +87,21 @@ type MailFolder struct {
DisplayName string
}
// CreateContactFolder makes a contact folder with the displayName of folderName.
// If successful, returns the created folder object.
func CreateContactFolder(gs graph.Service, user, folderName string) (models.ContactFolderable, error) {
requestBody := models.NewContactFolder()
requestBody.SetDisplayName(&folderName)
return gs.Client().UsersById(user).ContactFolders().Post(requestBody)
}
// DeleteContactFolder deletes the ContactFolder associated with the M365 ID if permissions are valid.
// Errors returned if the function call was not successful.
func DeleteContactFolder(gs graph.Service, user, folderID string) error {
return gs.Client().UsersById(user).ContactFoldersById(folderID).Delete()
}
// GetAllMailFolders retrieves all mail folders for the specified user.
// If nameContains is populated, only returns mail matching that property.
// Returns a slice of {ID, DisplayName} tuples.
@ -131,14 +146,29 @@ func GetAllMailFolders(gs graph.Service, user, nameContains string) ([]MailFolde
return mfs, err
}
// GetMailFolderID query function to retrieve the M365 ID based on the folder's displayName.
// GetFolderID query function to retrieve the M365 ID based on the folder's displayName.
// @param folderName the target folder's display name. Case sensitive
// @param category switches query and iteration to support multiple exchange applications
// @returns a *string if the folder exists. If the folder does not exist returns nil, error-> folder not found
func GetMailFolderID(service graph.Service, folderName, user string) (*string, error) {
var errs error
var folderID *string
func GetFolderID(service graph.Service, folderName, user string, category optionIdentifier) (*string, error) {
var (
errs error
folderID *string
query GraphQuery
transform absser.ParsableFactory
)
switch category {
case messages:
query = GetAllFolderNamesForUser
transform = models.CreateMailFolderCollectionResponseFromDiscriminatorValue
case contacts:
query = GetAllContactFolderNamesForUser
transform = models.CreateContactFolderFromDiscriminatorValue
default:
return nil, fmt.Errorf("unsupported category %s for GetFolderID()", category)
}
response, err := GetAllFolderNamesForUser(service, user)
response, err := query(service, user)
if err != nil {
return nil, errors.Wrapf(
err,
@ -146,31 +176,26 @@ func GetMailFolderID(service graph.Service, folderName, user string) (*string, e
user, support.ConnectorStackErrorTrace(err),
)
}
pageIterator, err := msgraphgocore.NewPageIterator(
response,
service.Adapter(),
models.CreateMailFolderCollectionResponseFromDiscriminatorValue,
transform,
)
if err != nil {
return nil, err
}
callbackFunc := func(folderItem any) bool {
folder, ok := folderItem.(models.MailFolderable)
if !ok {
errs = support.WrapAndAppend(service.Adapter().GetBaseUrl(), errors.New("HasFolder() iteration failure"), errs)
return true
callbackFunc := iterateFindFolderID(category,
&folderID,
folderName,
service.Adapter().GetBaseUrl(),
errs,
)
if err := pageIterator.Iterate(callbackFunc); err != nil {
return nil, support.WrapAndAppend(service.Adapter().GetBaseUrl(), err, errs)
}
if *folder.GetDisplayName() == folderName {
folderID = folder.GetId()
return false
}
return true
}
iterateError := pageIterator.Iterate(callbackFunc)
if iterateError != nil {
errs = support.WrapAndAppend(service.Adapter().GetBaseUrl(), iterateError, errs)
} else if folderID == nil {
if folderID == nil {
return nil, ErrFolderNotFound
}
@ -237,25 +262,52 @@ func SetupExchangeCollectionVars(scope selectors.ExchangeScope) (
return nil, nil, nil, errors.New("exchange scope option not supported")
}
// GetCopyRestoreFolder utility function to create an unique folder for the restore process
func GetCopyRestoreFolder(service graph.Service, user string) (*string, error) {
// GetCopyRestoreFolder utility function to create
// an unique folder for the restore process
// @param category: input from fullPath()[2]
// that defines the application the folder is created in.
func GetCopyRestoreFolder(
service graph.Service,
user, category string,
) (string, error) {
newFolder := fmt.Sprintf("Corso_Restore_%s", common.FormatNow(common.SimpleDateTimeFormat))
isFolder, err := GetMailFolderID(service, newFolder, user)
switch category {
case mailCategory, contactsCategory:
return establishFolder(service, newFolder, user, categoryToOptionIdentifier(category))
default:
return "", fmt.Errorf("%s category not supported", category)
}
}
func establishFolder(
service graph.Service,
folderName, user string,
optID optionIdentifier,
) (string, error) {
folderID, err := GetFolderID(service, folderName, user, optID)
if err == nil {
return *folderID, nil
}
// Experienced error other than folder does not exist
if !errors.Is(err, ErrFolderNotFound) {
return "", support.WrapAndAppend(user, err, err)
}
switch optID {
case messages:
fold, err := CreateMailFolder(service, user, folderName)
if err != nil {
// Verify unique folder was not found
if errors.Is(err, ErrFolderNotFound) {
fold, err := CreateMailFolder(service, user, newFolder)
return "", support.WrapAndAppend(user, err, err)
}
return *fold.GetId(), nil
case contacts:
fold, err := CreateContactFolder(service, user, folderName)
if err != nil {
return nil, support.WrapAndAppend(user, err, err)
return "", support.WrapAndAppend(user, err, err)
}
return fold.GetId(), nil
return *fold.GetId(), nil
default:
return "", fmt.Errorf("category: %s not supported for folder creation", optID)
}
return nil, err
}
return isFolder, nil
}
func RestoreExchangeObject(
@ -268,10 +320,8 @@ func RestoreExchangeObject(
) error {
var setting optionIdentifier
switch category {
case mailCategory:
setting = messages
case contactsCategory:
setting = contacts
case mailCategory, contactsCategory:
setting = categoryToOptionIdentifier(category)
default:
return fmt.Errorf("type: %s not supported for exchange restore", category)
}

View File

@ -12,12 +12,6 @@ import (
"github.com/alcionai/corso/pkg/selectors"
)
const (
mailCategory = "mail"
contactsCategory = "contacts"
eventsCategory = "events"
)
// descendable represents objects that implement msgraph-sdk-go/models.entityable
// and have the concept of a "parent folder".
type descendable interface {
@ -280,3 +274,45 @@ func IterateFilterFolderDirectoriesForCollections(
return true
}
}
// iterateFindFolderID is a utility function that supports finding
// M365 folders objects that matches the folderName. Iterator callback function
// will work on folderCollection responses whose objects implement
// the displayable interface. If folder exists, the function updates the
// folderID memory address that was passed in.
func iterateFindFolderID(
category optionIdentifier,
folderID **string,
folderName, errorIdentifier string,
errs error,
) func(any) bool {
return func(entry any) bool {
switch category {
case messages, contacts:
folder, ok := entry.(displayable)
if !ok {
errs = support.WrapAndAppend(
errorIdentifier,
errors.New("struct does not implement displayable"),
errs,
)
return true
}
// Display name not set on folder
if folder.GetDisplayName() == nil {
return true
}
name := *folder.GetDisplayName()
if folderName == name {
if folder.GetId() == nil {
return true // invalid folder
}
*folderID = folder.GetId()
return false
}
return true
default:
return false
}
}
}

View File

@ -34,7 +34,7 @@ func GetAllMessagesForUser(gs graph.Service, user string) (absser.Parsable, erro
// GetAllContactsForUser is a GraphQuery function for querying all the contacts in a user's account
func GetAllContactsForUser(gs graph.Service, user string) (absser.Parsable, error) {
selecting := []string{"id", "parentFolderId"}
selecting := []string{"parentFolderId"}
options, err := optionsForContacts(selecting)
if err != nil {
return nil, err
@ -46,7 +46,7 @@ func GetAllContactsForUser(gs graph.Service, user string) (absser.Parsable, erro
// GetAllFolderDisplayNamesForUser is a GraphQuery function for getting FolderId and display
// names for Mail Folder. All other information for the MailFolder object is omitted.
func GetAllFolderNamesForUser(gs graph.Service, user string) (absser.Parsable, error) {
options, err := optionsForMailFolders([]string{"id", "displayName"})
options, err := optionsForMailFolders([]string{"displayName"})
if err != nil {
return nil, err
}
@ -54,6 +54,17 @@ func GetAllFolderNamesForUser(gs graph.Service, user string) (absser.Parsable, e
return gs.Client().UsersById(user).MailFolders().GetWithRequestConfigurationAndResponseHandler(options, nil)
}
// GetAllContactFolderNamesForUser is a GraphQuery function for getting ContactFolderId
// and display names for contacts. All other information is omitted.
// Does not return the primary Contact Folder
func GetAllContactFolderNamesForUser(gs graph.Service, user string) (absser.Parsable, error) {
options, err := optionsForContactFolders([]string{"displayName"})
if err != nil {
return nil, err
}
return gs.Client().UsersById(user).ContactFolders().GetWithRequestConfigurationAndResponseHandler(options, nil)
}
// GetAllUsersForTenant is a GraphQuery for retrieving all the UserCollectionResponse with
// that contains the UserID and email for each user. All other information is omitted
func GetAllUsersForTenant(gs graph.Service, user string) (absser.Parsable, error) {

View File

@ -229,7 +229,7 @@ func (gc *GraphConnector) RestoreMessages(ctx context.Context, dcs []data.Collec
pathCounter = map[string]bool{}
attempts, successes int
errs error
folderID *string
folderID string
)
policy := control.Copy // TODO policy to be updated from external source after completion of refactoring
@ -237,10 +237,11 @@ func (gc *GraphConnector) RestoreMessages(ctx context.Context, dcs []data.Collec
directory := strings.Join(dc.FullPath(), "")
user := dc.FullPath()[1]
items := dc.Items()
category := dc.FullPath()[2]
if _, ok := pathCounter[directory]; !ok {
pathCounter[directory] = true
if policy == control.Copy {
folderID, errs = exchange.GetCopyRestoreFolder(&gc.graphService, user)
folderID, errs = exchange.GetCopyRestoreFolder(&gc.graphService, user, category)
if errs != nil {
return errs
}
@ -265,7 +266,7 @@ func (gc *GraphConnector) RestoreMessages(ctx context.Context, dcs []data.Collec
continue
}
category := dc.FullPath()[2]
err = exchange.RestoreExchangeObject(ctx, buf.Bytes(), category, policy, &gc.graphService, *folderID, user)
err = exchange.RestoreExchangeObject(ctx, buf.Bytes(), category, policy, &gc.graphService, folderID, user)
if err != nil {
errs = support.WrapAndAppend(itemData.UUID(), err, errs)

View File

@ -275,26 +275,28 @@ func (suite *GraphConnectorIntegrationSuite) TestAccessOfInboxAllUsers() {
// Exchange Functions
//-------------------------------------------------------
// TestCreateAndDeleteFolder ensures GraphConnector has the ability
// TestCreateAndDeleteMailFolder ensures GraphConnector has the ability
// to create and remove folders within the tenant
func (suite *GraphConnectorIntegrationSuite) TestCreateAndDeleteFolder() {
userID := tester.M365UserID(suite.T())
func (suite *GraphConnectorIntegrationSuite) TestCreateAndDeleteMailFolder() {
now := time.Now()
folderName := "TestFolder: " + common.FormatSimpleDateTime(now)
aFolder, err := exchange.CreateMailFolder(&suite.connector.graphService, userID, folderName)
aFolder, err := exchange.CreateMailFolder(&suite.connector.graphService, suite.user, folderName)
assert.NoError(suite.T(), err, support.ConnectorStackErrorTrace(err))
if aFolder != nil {
err = exchange.DeleteMailFolder(suite.connector.Service(), userID, *aFolder.GetId())
err = exchange.DeleteMailFolder(suite.connector.Service(), suite.user, *aFolder.GetId())
assert.NoError(suite.T(), err)
}
}
// TestGetMailFolderID verifies the ability to retrieve folder ID of folders
// at the top level of the file tree
func (suite *GraphConnectorIntegrationSuite) TestGetMailFolderID() {
userID := tester.M365UserID(suite.T())
folderName := "Inbox"
folderID, err := exchange.GetMailFolderID(&suite.connector.graphService, folderName, userID)
// TestCreateAndDeleteContactFolder ensures GraphConnector has the ability
// to create and remove contact folders within the tenant
func (suite *GraphConnectorIntegrationSuite) TestCreateAndDeleteContactFolder() {
now := time.Now()
folderName := "TestContactFolder: " + common.FormatSimpleDateTime(now)
aFolder, err := exchange.CreateContactFolder(suite.connector.Service(), suite.user, folderName)
assert.NoError(suite.T(), err)
if aFolder != nil {
err = exchange.DeleteContactFolder(suite.connector.Service(), suite.user, *aFolder.GetId())
assert.NoError(suite.T(), err)
assert.NotNil(suite.T(), folderID)
}
}