diff --git a/src/internal/connector/exchange/api/contacts.go b/src/internal/connector/exchange/api/contacts.go index 76c287efa..a9af29650 100644 --- a/src/internal/connector/exchange/api/contacts.go +++ b/src/internal/connector/exchange/api/contacts.go @@ -55,18 +55,39 @@ func (c Contacts) CreateContactFolder( return mdl, nil } -// TODO: Add pagination or just reuse EnumerateContainer logic -// with base id at root -func (c Contacts) GetContactFolders( +// GetContactFolderByDisplayName fetches contact folder which has the displayName of +// folderName. This is only done at root level. +func (c Contacts) GetContactFolderByDisplayName( ctx context.Context, - user string, -) (models.ContactFolderCollectionResponseable, error) { - mdl, err := c.Stable.Client().UsersById(user).ContactFolders().Get(ctx, nil) - if err != nil { - return nil, graph.Wrap(ctx, err, "creating contact folder") + user, folderName string, +) (models.ContactFolderable, error) { + filter := fmt.Sprintf("displayName eq '%s'", folderName) + options := &users.ItemContactFoldersRequestBuilderGetRequestConfiguration{ + QueryParameters: &users.ItemContactFoldersRequestBuilderGetQueryParameters{ + Filter: &filter, + }, } - return mdl, nil + resp, err := c.Stable.Client().UsersById(user).ContactFolders().Get(ctx, options) + if err != nil { + return nil, graph.Wrap(ctx, err, "getting contact folder"). + With("folder name", folderName) + } + + // We only expect one folder with provided folderName. Return an error if + // multiple folders exist(unlikely) or if no folder was found + if len(resp.GetValue()) != 1 { + return nil, graph.Wrap(ctx, err, "unexpected number of folders"). + With("folder count", len(resp.GetValue())). + With("folder name", folderName) + } + + fold := resp.GetValue()[0] + if err := checkIDAndName(fold); err != nil { + return nil, err + } + + return fold, nil } // DeleteContainer deletes the ContactFolder associated with the M365 ID if permissions are valid. diff --git a/src/internal/connector/exchange/service_restore.go b/src/internal/connector/exchange/service_restore.go index 934a27fd9..45eaaae30 100644 --- a/src/internal/connector/exchange/service_restore.go +++ b/src/internal/connector/exchange/service_restore.go @@ -649,28 +649,29 @@ func establishContactsRestoreLocation( return cached, nil } - folders[0] = "Corso_Restore_02-May-2023_00:06:19" ctx = clues.Add(ctx, "is_new_cache", isNewCache) - // This is where destination folder gets created by corso in graph - // This is the POST operation which may fail if the folder exists - temp, err := ac.Contacts().CreateContactFolder(ctx, user, folders[0]) - if err != nil { - // TODO:: Add status code check too - if graph.IsErrFolderExists(err) { - result, _ := ac.Contacts().GetContactFolders(ctx, user) + fold, err := ac.Contacts().CreateContactFolder(ctx, user, folders[0]) + // 409 handling: Fetch folder if it exists. + // This is rare, but it may happen if an earlier CreateContactFolder POST failed + // with 5xx, potentially leaving dirty state in graph. + if graph.IsErrFolderExists(err) { + fold, err = ac.Contacts().GetContactFolderByDisplayName(ctx, user, folders[0]) + if err != nil { + return "", err } + } else if err != nil { return "", err } - folderID := ptr.Val(temp.GetId()) + folderID := ptr.Val(fold.GetId()) if isNewCache { if err := cfc.Populate(ctx, errs, folderID, folders[0]); err != nil { return "", clues.Wrap(err, "populating contact cache") } - if err = cfc.AddToCache(ctx, temp); err != nil { + if err = cfc.AddToCache(ctx, fold); err != nil { return "", clues.Wrap(err, "adding contact folder to cache") } }