Contact Restore: Create / Delete Folders
Contact folder creation and deletion enabled by the PR. Test suites expanded
This commit is contained in:
parent
3f6f6604a2
commit
b2d3e62536
@ -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() {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
assert.NotNil(suite.T(), folderID)
|
||||
if aFolder != nil {
|
||||
err = exchange.DeleteContactFolder(suite.connector.Service(), suite.user, *aFolder.GetId())
|
||||
assert.NoError(suite.T(), err)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user