diff --git a/CHANGELOG.md b/CHANGELOG.md index 352b8703c..56f24db17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] (alpha) +### Fixed + +- Check if the user specified for an exchange backup operation has a mailbox. + ## [v0.1.0] (alpha) - 2023-01-13 diff --git a/src/internal/connector/data_collections.go b/src/internal/connector/data_collections.go index abd3436b3..2ba2e59a4 100644 --- a/src/internal/connector/data_collections.go +++ b/src/internal/connector/data_collections.go @@ -7,7 +7,9 @@ import ( "github.com/pkg/errors" + "github.com/alcionai/corso/src/internal/connector/discovery" "github.com/alcionai/corso/src/internal/connector/exchange" + "github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/onedrive" "github.com/alcionai/corso/src/internal/connector/sharepoint" "github.com/alcionai/corso/src/internal/connector/support" @@ -15,6 +17,7 @@ import ( D "github.com/alcionai/corso/src/internal/diagnostics" "github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/logger" + "github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/selectors" ) @@ -41,6 +44,15 @@ func (gc *GraphConnector) DataCollections( return nil, err } + serviceEnabled, err := checkServiceEnabled(ctx, gc.Service, path.ServiceType(sels.Service), sels.DiscreteOwner) + if err != nil { + return nil, err + } + + if !serviceEnabled { + return []data.Collection{}, nil + } + switch sels.Service { case selectors.ServiceExchange: colls, err := exchange.DataCollections( @@ -124,6 +136,29 @@ func verifyBackupInputs(sels selectors.Selector, userPNs, siteIDs []string) erro return nil } +func checkServiceEnabled( + ctx context.Context, + gs graph.Servicer, + service path.ServiceType, + resource string, +) (bool, error) { + if service == path.SharePointService { + // No "enabled" check required for sharepoint + return true, nil + } + + _, info, err := discovery.User(ctx, gs, resource) + if err != nil { + return false, err + } + + if _, ok := info.DiscoveredServices[service]; !ok { + return false, nil + } + + return true, nil +} + // --------------------------------------------------------------------------- // OneDrive // --------------------------------------------------------------------------- diff --git a/src/internal/connector/discovery/discovery.go b/src/internal/connector/discovery/discovery.go index 17ad10eb2..a9f2266d1 100644 --- a/src/internal/connector/discovery/discovery.go +++ b/src/internal/connector/discovery/discovery.go @@ -10,6 +10,7 @@ import ( "github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/support" + "github.com/alcionai/corso/src/pkg/path" ) const ( @@ -64,6 +65,49 @@ func Users(ctx context.Context, gs graph.Servicer, tenantID string) ([]models.Us return users, iterErrs } +type UserInfo struct { + DiscoveredServices map[path.ServiceType]struct{} +} + +func User(ctx context.Context, gs graph.Servicer, userID string) (models.Userable, *UserInfo, error) { + user, err := gs.Client().UsersById(userID).Get(ctx, nil) + if err != nil { + return nil, nil, errors.Wrapf( + err, + "retrieving resource for tenant: %s", + support.ConnectorStackErrorTrace(err), + ) + } + + // Assume all services are enabled + userInfo := &UserInfo{ + DiscoveredServices: map[path.ServiceType]struct{}{ + path.ExchangeService: {}, + path.OneDriveService: {}, + }, + } + + // Discover which services the user has enabled + + // Exchange: Query `MailFolders` + _, err = gs.Client().UsersById(userID).MailFolders().Get(ctx, nil) + if err != nil { + if !graph.IsErrExchangeMailFolderNotFound(err) { + return nil, nil, errors.Wrapf( + err, + "retrieving mail folders for tenant: %s", + support.ConnectorStackErrorTrace(err), + ) + } + + delete(userInfo.DiscoveredServices, path.ExchangeService) + } + + // TODO: OneDrive + + return user, userInfo, nil +} + // parseUser extracts information from `models.Userable` we care about func parseUser(item interface{}) (models.Userable, error) { m, ok := item.(models.Userable) diff --git a/src/internal/connector/graph/errors.go b/src/internal/connector/graph/errors.go index cf2df3556..1b0d86b85 100644 --- a/src/internal/connector/graph/errors.go +++ b/src/internal/connector/graph/errors.go @@ -15,11 +15,13 @@ import ( // --------------------------------------------------------------------------- const ( - errCodeItemNotFound = "ErrorItemNotFound" - errCodeEmailFolderNotFound = "ErrorSyncFolderNotFound" - errCodeResyncRequired = "ResyncRequired" - errCodeSyncFolderNotFound = "ErrorSyncFolderNotFound" - errCodeSyncStateNotFound = "SyncStateNotFound" + errCodeItemNotFound = "ErrorItemNotFound" + errCodeEmailFolderNotFound = "ErrorSyncFolderNotFound" + errCodeResyncRequired = "ResyncRequired" + errCodeSyncFolderNotFound = "ErrorSyncFolderNotFound" + errCodeSyncStateNotFound = "SyncStateNotFound" + errCodeResourceNotFound = "ResourceNotFound" + errCodeMailboxNotEnabledForRESTAPI = "MailboxNotEnabledForRESTAPI" ) // The folder or item was deleted between the time we identified @@ -69,6 +71,10 @@ func asInvalidDelta(err error) bool { return errors.As(err, &e) } +func IsErrExchangeMailFolderNotFound(err error) bool { + return hasErrorCode(err, errCodeResourceNotFound, errCodeMailboxNotEnabledForRESTAPI) +} + // Timeout errors are identified for tracking the need to retry calls. // Other delay errors, like throttling, are already handled by the // graph client's built-in retries.