From c70207b1f85e0cbc7a9364b778c6985b1b1f88d4 Mon Sep 17 00:00:00 2001 From: Keepers Date: Thu, 15 Jun 2023 18:38:16 -0600 Subject: [PATCH] separate pager and plain apis for exchange (#3627) Separates the pager and enumerattion functionality from the rest of the exchange api funcs for each category. Purely code movement, no logic changes. --- #### Does this PR need a docs update or release note? - [x] :no_entry: No #### Type of change - [x] :broom: Tech Debt/Cleanup #### Issue(s) * #3562 #### Test Plan - [x] :zap: Unit test - [x] :green_heart: E2E --- src/pkg/services/m365/api/contacts.go | 213 --------------- src/pkg/services/m365/api/contacts_pager.go | 226 ++++++++++++++++ src/pkg/services/m365/api/events.go | 229 ---------------- src/pkg/services/m365/api/events_pager.go | 243 +++++++++++++++++ src/pkg/services/m365/api/mail.go | 259 ------------------- src/pkg/services/m365/api/mail_pager.go | 273 ++++++++++++++++++++ 6 files changed, 742 insertions(+), 701 deletions(-) create mode 100644 src/pkg/services/m365/api/contacts_pager.go create mode 100644 src/pkg/services/m365/api/events_pager.go create mode 100644 src/pkg/services/m365/api/mail_pager.go diff --git a/src/pkg/services/m365/api/contacts.go b/src/pkg/services/m365/api/contacts.go index 9d2c253d5..c253212cd 100644 --- a/src/pkg/services/m365/api/contacts.go +++ b/src/pkg/services/m365/api/contacts.go @@ -14,7 +14,6 @@ import ( "github.com/alcionai/corso/src/internal/m365/graph" "github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/fault" - "github.com/alcionai/corso/src/pkg/selectors" ) // --------------------------------------------------------------------------- @@ -137,79 +136,6 @@ func (c Contacts) PatchFolder( return nil } -// --------------------------------------------------------------------------- -// container pager -// --------------------------------------------------------------------------- - -// EnumerateContainers iterates through all of the users current -// contacts folders, converting each to a graph.CacheFolder, and calling -// fn(cf) on each one. -// Folder hierarchy is represented in its current state, and does -// not contain historical data. -func (c Contacts) EnumerateContainers( - ctx context.Context, - userID, baseContainerID string, - fn func(graph.CachedContainer) error, - errs *fault.Bus, -) error { - config := &users.ItemContactFoldersItemChildFoldersRequestBuilderGetRequestConfiguration{ - QueryParameters: &users.ItemContactFoldersItemChildFoldersRequestBuilderGetQueryParameters{ - Select: idAnd(displayName, parentFolderID), - }, - } - - el := errs.Local() - builder := c.Stable. - Client(). - Users(). - ByUserId(userID). - ContactFolders(). - ByContactFolderId(baseContainerID). - ChildFolders() - - for { - if el.Failure() != nil { - break - } - - resp, err := builder.Get(ctx, config) - if err != nil { - return graph.Stack(ctx, err) - } - - for _, fold := range resp.GetValue() { - if el.Failure() != nil { - return el.Failure() - } - - if err := graph.CheckIDNameAndParentFolderID(fold); err != nil { - errs.AddRecoverable(ctx, graph.Stack(ctx, err).Label(fault.LabelForceNoBackupCreation)) - continue - } - - fctx := clues.Add( - ctx, - "container_id", ptr.Val(fold.GetId()), - "container_display_name", ptr.Val(fold.GetDisplayName())) - - temp := graph.NewCacheFolder(fold, nil, nil) - if err := fn(&temp); err != nil { - errs.AddRecoverable(ctx, graph.Stack(fctx, err).Label(fault.LabelForceNoBackupCreation)) - continue - } - } - - link, ok := ptr.ValOK(resp.GetOdataNextLink()) - if !ok { - break - } - - builder = users.NewItemContactFoldersItemChildFoldersRequestBuilder(link, c.Stable.Adapter()) - } - - return el.Failure() -} - // --------------------------------------------------------------------------- // items // --------------------------------------------------------------------------- @@ -284,145 +210,6 @@ func (c Contacts) DeleteItem( return nil } -// --------------------------------------------------------------------------- -// item pager -// --------------------------------------------------------------------------- - -var _ itemPager = &contactPager{} - -type contactPager struct { - gs graph.Servicer - builder *users.ItemContactFoldersItemContactsRequestBuilder - options *users.ItemContactFoldersItemContactsRequestBuilderGetRequestConfiguration -} - -func (c Contacts) NewContactPager( - ctx context.Context, - userID, containerID string, - immutableIDs bool, -) itemPager { - config := &users.ItemContactFoldersItemContactsRequestBuilderGetRequestConfiguration{ - QueryParameters: &users.ItemContactFoldersItemContactsRequestBuilderGetQueryParameters{ - Select: idAnd(parentFolderID), - }, - Headers: newPreferHeaders(preferPageSize(maxNonDeltaPageSize), preferImmutableIDs(immutableIDs)), - } - - builder := c.Stable. - Client(). - Users(). - ByUserId(userID). - ContactFolders(). - ByContactFolderId(containerID). - Contacts() - - return &contactPager{c.Stable, builder, config} -} - -func (p *contactPager) getPage(ctx context.Context) (DeltaPageLinker, error) { - resp, err := p.builder.Get(ctx, p.options) - if err != nil { - return nil, graph.Stack(ctx, err) - } - - return EmptyDeltaLinker[models.Contactable]{PageLinkValuer: resp}, nil -} - -func (p *contactPager) setNext(nextLink string) { - p.builder = users.NewItemContactFoldersItemContactsRequestBuilder(nextLink, p.gs.Adapter()) -} - -// non delta pagers don't need reset -func (p *contactPager) reset(context.Context) {} - -func (p *contactPager) valuesIn(pl PageLinker) ([]getIDAndAddtler, error) { - return toValues[models.Contactable](pl) -} - -// --------------------------------------------------------------------------- -// delta item pager -// --------------------------------------------------------------------------- - -var _ itemPager = &contactDeltaPager{} - -type contactDeltaPager struct { - gs graph.Servicer - userID string - containerID string - builder *users.ItemContactFoldersItemContactsDeltaRequestBuilder - options *users.ItemContactFoldersItemContactsDeltaRequestBuilderGetRequestConfiguration -} - -func getContactDeltaBuilder( - ctx context.Context, - gs graph.Servicer, - userID, containerID string, - options *users.ItemContactFoldersItemContactsDeltaRequestBuilderGetRequestConfiguration, -) *users.ItemContactFoldersItemContactsDeltaRequestBuilder { - builder := gs.Client().Users().ByUserId(userID).ContactFolders().ByContactFolderId(containerID).Contacts().Delta() - return builder -} - -func (c Contacts) NewContactDeltaPager( - ctx context.Context, - userID, containerID, oldDelta string, - immutableIDs bool, -) itemPager { - options := &users.ItemContactFoldersItemContactsDeltaRequestBuilderGetRequestConfiguration{ - QueryParameters: &users.ItemContactFoldersItemContactsDeltaRequestBuilderGetQueryParameters{ - Select: idAnd(parentFolderID), - }, - Headers: newPreferHeaders(preferPageSize(maxDeltaPageSize), preferImmutableIDs(immutableIDs)), - } - - var builder *users.ItemContactFoldersItemContactsDeltaRequestBuilder - if oldDelta != "" { - builder = users.NewItemContactFoldersItemContactsDeltaRequestBuilder(oldDelta, c.Stable.Adapter()) - } else { - builder = getContactDeltaBuilder(ctx, c.Stable, userID, containerID, options) - } - - return &contactDeltaPager{c.Stable, userID, containerID, builder, options} -} - -func (p *contactDeltaPager) getPage(ctx context.Context) (DeltaPageLinker, error) { - resp, err := p.builder.Get(ctx, p.options) - if err != nil { - return nil, graph.Stack(ctx, err) - } - - return resp, nil -} - -func (p *contactDeltaPager) setNext(nextLink string) { - p.builder = users.NewItemContactFoldersItemContactsDeltaRequestBuilder(nextLink, p.gs.Adapter()) -} - -func (p *contactDeltaPager) reset(ctx context.Context) { - p.builder = getContactDeltaBuilder(ctx, p.gs, p.userID, p.containerID, p.options) -} - -func (p *contactDeltaPager) valuesIn(pl PageLinker) ([]getIDAndAddtler, error) { - return toValues[models.Contactable](pl) -} - -func (c Contacts) GetAddedAndRemovedItemIDs( - ctx context.Context, - userID, containerID, oldDelta string, - immutableIDs bool, - canMakeDeltaQueries bool, -) ([]string, []string, DeltaUpdate, error) { - ctx = clues.Add( - ctx, - "category", selectors.ExchangeContact, - "container_id", containerID) - - pager := c.NewContactPager(ctx, userID, containerID, immutableIDs) - deltaPager := c.NewContactDeltaPager(ctx, userID, containerID, oldDelta, immutableIDs) - - return getAddedAndRemovedItemIDs(ctx, c.Stable, pager, deltaPager, oldDelta, canMakeDeltaQueries) -} - // --------------------------------------------------------------------------- // Serialization // --------------------------------------------------------------------------- diff --git a/src/pkg/services/m365/api/contacts_pager.go b/src/pkg/services/m365/api/contacts_pager.go new file mode 100644 index 000000000..da79b3ce9 --- /dev/null +++ b/src/pkg/services/m365/api/contacts_pager.go @@ -0,0 +1,226 @@ +package api + +import ( + "context" + + "github.com/alcionai/clues" + "github.com/microsoftgraph/msgraph-sdk-go/models" + "github.com/microsoftgraph/msgraph-sdk-go/users" + + "github.com/alcionai/corso/src/internal/common/ptr" + "github.com/alcionai/corso/src/internal/m365/graph" + "github.com/alcionai/corso/src/pkg/fault" + "github.com/alcionai/corso/src/pkg/selectors" +) + +// --------------------------------------------------------------------------- +// container pager +// --------------------------------------------------------------------------- + +// EnumerateContainers iterates through all of the users current +// contacts folders, converting each to a graph.CacheFolder, and calling +// fn(cf) on each one. +// Folder hierarchy is represented in its current state, and does +// not contain historical data. +func (c Contacts) EnumerateContainers( + ctx context.Context, + userID, baseContainerID string, + fn func(graph.CachedContainer) error, + errs *fault.Bus, +) error { + config := &users.ItemContactFoldersItemChildFoldersRequestBuilderGetRequestConfiguration{ + QueryParameters: &users.ItemContactFoldersItemChildFoldersRequestBuilderGetQueryParameters{ + Select: idAnd(displayName, parentFolderID), + }, + } + + el := errs.Local() + builder := c.Stable. + Client(). + Users(). + ByUserId(userID). + ContactFolders(). + ByContactFolderId(baseContainerID). + ChildFolders() + + for { + if el.Failure() != nil { + break + } + + resp, err := builder.Get(ctx, config) + if err != nil { + return graph.Stack(ctx, err) + } + + for _, fold := range resp.GetValue() { + if el.Failure() != nil { + return el.Failure() + } + + if err := graph.CheckIDNameAndParentFolderID(fold); err != nil { + errs.AddRecoverable(ctx, graph.Stack(ctx, err).Label(fault.LabelForceNoBackupCreation)) + continue + } + + fctx := clues.Add( + ctx, + "container_id", ptr.Val(fold.GetId()), + "container_display_name", ptr.Val(fold.GetDisplayName())) + + temp := graph.NewCacheFolder(fold, nil, nil) + if err := fn(&temp); err != nil { + errs.AddRecoverable(ctx, graph.Stack(fctx, err).Label(fault.LabelForceNoBackupCreation)) + continue + } + } + + link, ok := ptr.ValOK(resp.GetOdataNextLink()) + if !ok { + break + } + + builder = users.NewItemContactFoldersItemChildFoldersRequestBuilder(link, c.Stable.Adapter()) + } + + return el.Failure() +} + +// --------------------------------------------------------------------------- +// item pager +// --------------------------------------------------------------------------- + +var _ itemPager = &contactPager{} + +type contactPager struct { + gs graph.Servicer + builder *users.ItemContactFoldersItemContactsRequestBuilder + options *users.ItemContactFoldersItemContactsRequestBuilderGetRequestConfiguration +} + +func (c Contacts) NewContactPager( + ctx context.Context, + userID, containerID string, + immutableIDs bool, +) itemPager { + config := &users.ItemContactFoldersItemContactsRequestBuilderGetRequestConfiguration{ + QueryParameters: &users.ItemContactFoldersItemContactsRequestBuilderGetQueryParameters{ + Select: idAnd(parentFolderID), + }, + Headers: newPreferHeaders(preferPageSize(maxNonDeltaPageSize), preferImmutableIDs(immutableIDs)), + } + + builder := c.Stable. + Client(). + Users(). + ByUserId(userID). + ContactFolders(). + ByContactFolderId(containerID). + Contacts() + + return &contactPager{c.Stable, builder, config} +} + +func (p *contactPager) getPage(ctx context.Context) (DeltaPageLinker, error) { + resp, err := p.builder.Get(ctx, p.options) + if err != nil { + return nil, graph.Stack(ctx, err) + } + + return EmptyDeltaLinker[models.Contactable]{PageLinkValuer: resp}, nil +} + +func (p *contactPager) setNext(nextLink string) { + p.builder = users.NewItemContactFoldersItemContactsRequestBuilder(nextLink, p.gs.Adapter()) +} + +// non delta pagers don't need reset +func (p *contactPager) reset(context.Context) {} + +func (p *contactPager) valuesIn(pl PageLinker) ([]getIDAndAddtler, error) { + return toValues[models.Contactable](pl) +} + +// --------------------------------------------------------------------------- +// delta item pager +// --------------------------------------------------------------------------- + +var _ itemPager = &contactDeltaPager{} + +type contactDeltaPager struct { + gs graph.Servicer + userID string + containerID string + builder *users.ItemContactFoldersItemContactsDeltaRequestBuilder + options *users.ItemContactFoldersItemContactsDeltaRequestBuilderGetRequestConfiguration +} + +func getContactDeltaBuilder( + ctx context.Context, + gs graph.Servicer, + userID, containerID string, + options *users.ItemContactFoldersItemContactsDeltaRequestBuilderGetRequestConfiguration, +) *users.ItemContactFoldersItemContactsDeltaRequestBuilder { + builder := gs.Client().Users().ByUserId(userID).ContactFolders().ByContactFolderId(containerID).Contacts().Delta() + return builder +} + +func (c Contacts) NewContactDeltaPager( + ctx context.Context, + userID, containerID, oldDelta string, + immutableIDs bool, +) itemPager { + options := &users.ItemContactFoldersItemContactsDeltaRequestBuilderGetRequestConfiguration{ + QueryParameters: &users.ItemContactFoldersItemContactsDeltaRequestBuilderGetQueryParameters{ + Select: idAnd(parentFolderID), + }, + Headers: newPreferHeaders(preferPageSize(maxDeltaPageSize), preferImmutableIDs(immutableIDs)), + } + + var builder *users.ItemContactFoldersItemContactsDeltaRequestBuilder + if oldDelta != "" { + builder = users.NewItemContactFoldersItemContactsDeltaRequestBuilder(oldDelta, c.Stable.Adapter()) + } else { + builder = getContactDeltaBuilder(ctx, c.Stable, userID, containerID, options) + } + + return &contactDeltaPager{c.Stable, userID, containerID, builder, options} +} + +func (p *contactDeltaPager) getPage(ctx context.Context) (DeltaPageLinker, error) { + resp, err := p.builder.Get(ctx, p.options) + if err != nil { + return nil, graph.Stack(ctx, err) + } + + return resp, nil +} + +func (p *contactDeltaPager) setNext(nextLink string) { + p.builder = users.NewItemContactFoldersItemContactsDeltaRequestBuilder(nextLink, p.gs.Adapter()) +} + +func (p *contactDeltaPager) reset(ctx context.Context) { + p.builder = getContactDeltaBuilder(ctx, p.gs, p.userID, p.containerID, p.options) +} + +func (p *contactDeltaPager) valuesIn(pl PageLinker) ([]getIDAndAddtler, error) { + return toValues[models.Contactable](pl) +} + +func (c Contacts) GetAddedAndRemovedItemIDs( + ctx context.Context, + userID, containerID, oldDelta string, + immutableIDs bool, + canMakeDeltaQueries bool, +) ([]string, []string, DeltaUpdate, error) { + ctx = clues.Add( + ctx, + "category", selectors.ExchangeContact, + "container_id", containerID) + + pager := c.NewContactPager(ctx, userID, containerID, immutableIDs) + deltaPager := c.NewContactDeltaPager(ctx, userID, containerID, oldDelta, immutableIDs) + + return getAddedAndRemovedItemIDs(ctx, c.Stable, pager, deltaPager, oldDelta, canMakeDeltaQueries) +} diff --git a/src/pkg/services/m365/api/events.go b/src/pkg/services/m365/api/events.go index 37f40107b..574e2de21 100644 --- a/src/pkg/services/m365/api/events.go +++ b/src/pkg/services/m365/api/events.go @@ -18,7 +18,6 @@ import ( "github.com/alcionai/corso/src/internal/m365/graph" "github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/fault" - "github.com/alcionai/corso/src/pkg/path" ) // --------------------------------------------------------------------------- @@ -190,86 +189,6 @@ func (c Events) PatchCalendar( return nil } -// --------------------------------------------------------------------------- -// container pager -// --------------------------------------------------------------------------- - -// EnumerateContainers iterates through all of the users current -// calendars, converting each to a graph.CacheFolder, and -// calling fn(cf) on each one. -// Folder hierarchy is represented in its current state, and does -// not contain historical data. -func (c Events) EnumerateContainers( - ctx context.Context, - userID, baseContainerID string, - fn func(graph.CachedContainer) error, - errs *fault.Bus, -) error { - var ( - el = errs.Local() - config = &users.ItemCalendarsRequestBuilderGetRequestConfiguration{ - QueryParameters: &users.ItemCalendarsRequestBuilderGetQueryParameters{ - Select: idAnd("name"), - }, - } - builder = c.Stable. - Client(). - Users(). - ByUserId(userID). - Calendars() - ) - - for { - if el.Failure() != nil { - break - } - - resp, err := builder.Get(ctx, config) - if err != nil { - return graph.Stack(ctx, err) - } - - for _, cal := range resp.GetValue() { - if el.Failure() != nil { - break - } - - cd := CalendarDisplayable{Calendarable: cal} - if err := graph.CheckIDAndName(cd); err != nil { - errs.AddRecoverable(ctx, graph.Stack(ctx, err).Label(fault.LabelForceNoBackupCreation)) - continue - } - - fctx := clues.Add( - ctx, - "container_id", ptr.Val(cal.GetId()), - "container_name", ptr.Val(cal.GetName())) - - temp := graph.NewCacheFolder( - cd, - path.Builder{}.Append(ptr.Val(cd.GetId())), // storage path - path.Builder{}.Append(ptr.Val(cd.GetDisplayName()))) // display location - if err := fn(&temp); err != nil { - errs.AddRecoverable(ctx, graph.Stack(fctx, err).Label(fault.LabelForceNoBackupCreation)) - continue - } - } - - link, ok := ptr.ValOK(resp.GetOdataNextLink()) - if !ok { - break - } - - builder = users.NewItemCalendarsRequestBuilder(link, c.Stable.Adapter()) - } - - return el.Failure() -} - -const ( - eventBetaDeltaURLTemplate = "https://graph.microsoft.com/beta/users/%s/calendars/%s/events/delta" -) - // --------------------------------------------------------------------------- // items // --------------------------------------------------------------------------- @@ -434,154 +353,6 @@ func (c Events) PostLargeAttachment( return us, nil } -// --------------------------------------------------------------------------- -// item pager -// --------------------------------------------------------------------------- - -var _ itemPager = &eventPager{} - -type eventPager struct { - gs graph.Servicer - builder *users.ItemCalendarsItemEventsRequestBuilder - options *users.ItemCalendarsItemEventsRequestBuilderGetRequestConfiguration -} - -func (c Events) NewEventPager( - ctx context.Context, - userID, containerID string, - immutableIDs bool, -) (itemPager, error) { - options := &users.ItemCalendarsItemEventsRequestBuilderGetRequestConfiguration{ - Headers: newPreferHeaders(preferPageSize(maxNonDeltaPageSize), preferImmutableIDs(immutableIDs)), - } - - builder := c.Stable. - Client(). - Users(). - ByUserId(userID). - Calendars(). - ByCalendarId(containerID). - Events() - - return &eventPager{c.Stable, builder, options}, nil -} - -func (p *eventPager) getPage(ctx context.Context) (DeltaPageLinker, error) { - resp, err := p.builder.Get(ctx, p.options) - if err != nil { - return nil, graph.Stack(ctx, err) - } - - return EmptyDeltaLinker[models.Eventable]{PageLinkValuer: resp}, nil -} - -func (p *eventPager) setNext(nextLink string) { - p.builder = users.NewItemCalendarsItemEventsRequestBuilder(nextLink, p.gs.Adapter()) -} - -// non delta pagers don't need reset -func (p *eventPager) reset(context.Context) {} - -func (p *eventPager) valuesIn(pl PageLinker) ([]getIDAndAddtler, error) { - return toValues[models.Eventable](pl) -} - -// --------------------------------------------------------------------------- -// delta item pager -// --------------------------------------------------------------------------- - -var _ itemPager = &eventDeltaPager{} - -type eventDeltaPager struct { - gs graph.Servicer - userID string - containerID string - builder *users.ItemCalendarsItemEventsDeltaRequestBuilder - options *users.ItemCalendarsItemEventsDeltaRequestBuilderGetRequestConfiguration -} - -func (c Events) NewEventDeltaPager( - ctx context.Context, - userID, containerID, oldDelta string, - immutableIDs bool, -) (itemPager, error) { - options := &users.ItemCalendarsItemEventsDeltaRequestBuilderGetRequestConfiguration{ - Headers: newPreferHeaders(preferPageSize(maxDeltaPageSize), preferImmutableIDs(immutableIDs)), - } - - var builder *users.ItemCalendarsItemEventsDeltaRequestBuilder - - if oldDelta == "" { - builder = getEventDeltaBuilder(ctx, c.Stable, userID, containerID, options) - } else { - builder = users.NewItemCalendarsItemEventsDeltaRequestBuilder(oldDelta, c.Stable.Adapter()) - } - - return &eventDeltaPager{c.Stable, userID, containerID, builder, options}, nil -} - -func getEventDeltaBuilder( - ctx context.Context, - gs graph.Servicer, - userID, containerID string, - options *users.ItemCalendarsItemEventsDeltaRequestBuilderGetRequestConfiguration, -) *users.ItemCalendarsItemEventsDeltaRequestBuilder { - // Graph SDK only supports delta queries against events on the beta version, so we're - // manufacturing use of the beta version url to make the call instead. - // See: https://learn.microsoft.com/ko-kr/graph/api/event-delta?view=graph-rest-beta&tabs=http - // Note that the delta item body is skeletal compared to the actual event struct. Lucky - // for us, we only need the item ID. As a result, even though we hacked the version, the - // response body parses properly into the v1.0 structs and complies with our wanted interfaces. - // Likewise, the NextLink and DeltaLink odata tags carry our hack forward, so the rest of the code - // works as intended (until, at least, we want to _not_ call the beta anymore). - rawURL := fmt.Sprintf(eventBetaDeltaURLTemplate, userID, containerID) - builder := users.NewItemCalendarsItemEventsDeltaRequestBuilder(rawURL, gs.Adapter()) - - return builder -} - -func (p *eventDeltaPager) getPage(ctx context.Context) (DeltaPageLinker, error) { - resp, err := p.builder.Get(ctx, p.options) - if err != nil { - return nil, graph.Stack(ctx, err) - } - - return resp, nil -} - -func (p *eventDeltaPager) setNext(nextLink string) { - p.builder = users.NewItemCalendarsItemEventsDeltaRequestBuilder(nextLink, p.gs.Adapter()) -} - -func (p *eventDeltaPager) reset(ctx context.Context) { - p.builder = getEventDeltaBuilder(ctx, p.gs, p.userID, p.containerID, p.options) -} - -func (p *eventDeltaPager) valuesIn(pl PageLinker) ([]getIDAndAddtler, error) { - return toValues[models.Eventable](pl) -} - -func (c Events) GetAddedAndRemovedItemIDs( - ctx context.Context, - userID, containerID, oldDelta string, - immutableIDs bool, - canMakeDeltaQueries bool, -) ([]string, []string, DeltaUpdate, error) { - ctx = clues.Add(ctx, "container_id", containerID) - - pager, err := c.NewEventPager(ctx, userID, containerID, immutableIDs) - if err != nil { - return nil, nil, DeltaUpdate{}, graph.Wrap(ctx, err, "creating non-delta pager") - } - - deltaPager, err := c.NewEventDeltaPager(ctx, userID, containerID, oldDelta, immutableIDs) - if err != nil { - return nil, nil, DeltaUpdate{}, graph.Wrap(ctx, err, "creating delta pager") - } - - return getAddedAndRemovedItemIDs(ctx, c.Stable, pager, deltaPager, oldDelta, canMakeDeltaQueries) -} - // --------------------------------------------------------------------------- // Serialization // --------------------------------------------------------------------------- diff --git a/src/pkg/services/m365/api/events_pager.go b/src/pkg/services/m365/api/events_pager.go new file mode 100644 index 000000000..bb390a288 --- /dev/null +++ b/src/pkg/services/m365/api/events_pager.go @@ -0,0 +1,243 @@ +package api + +import ( + "context" + "fmt" + + "github.com/alcionai/clues" + "github.com/microsoftgraph/msgraph-sdk-go/models" + "github.com/microsoftgraph/msgraph-sdk-go/users" + + "github.com/alcionai/corso/src/internal/common/ptr" + "github.com/alcionai/corso/src/internal/m365/graph" + "github.com/alcionai/corso/src/pkg/fault" + "github.com/alcionai/corso/src/pkg/path" +) + +const ( + eventBetaDeltaURLTemplate = "https://graph.microsoft.com/beta/users/%s/calendars/%s/events/delta" +) + +// --------------------------------------------------------------------------- +// container pager +// --------------------------------------------------------------------------- + +// EnumerateContainers iterates through all of the users current +// calendars, converting each to a graph.CacheFolder, and +// calling fn(cf) on each one. +// Folder hierarchy is represented in its current state, and does +// not contain historical data. +func (c Events) EnumerateContainers( + ctx context.Context, + userID, baseContainerID string, + fn func(graph.CachedContainer) error, + errs *fault.Bus, +) error { + var ( + el = errs.Local() + config = &users.ItemCalendarsRequestBuilderGetRequestConfiguration{ + QueryParameters: &users.ItemCalendarsRequestBuilderGetQueryParameters{ + Select: idAnd("name"), + }, + } + builder = c.Stable. + Client(). + Users(). + ByUserId(userID). + Calendars() + ) + + for { + if el.Failure() != nil { + break + } + + resp, err := builder.Get(ctx, config) + if err != nil { + return graph.Stack(ctx, err) + } + + for _, cal := range resp.GetValue() { + if el.Failure() != nil { + break + } + + cd := CalendarDisplayable{Calendarable: cal} + if err := graph.CheckIDAndName(cd); err != nil { + errs.AddRecoverable(ctx, graph.Stack(ctx, err).Label(fault.LabelForceNoBackupCreation)) + continue + } + + fctx := clues.Add( + ctx, + "container_id", ptr.Val(cal.GetId()), + "container_name", ptr.Val(cal.GetName())) + + temp := graph.NewCacheFolder( + cd, + path.Builder{}.Append(ptr.Val(cd.GetId())), // storage path + path.Builder{}.Append(ptr.Val(cd.GetDisplayName()))) // display location + if err := fn(&temp); err != nil { + errs.AddRecoverable(ctx, graph.Stack(fctx, err).Label(fault.LabelForceNoBackupCreation)) + continue + } + } + + link, ok := ptr.ValOK(resp.GetOdataNextLink()) + if !ok { + break + } + + builder = users.NewItemCalendarsRequestBuilder(link, c.Stable.Adapter()) + } + + return el.Failure() +} + +// --------------------------------------------------------------------------- +// item pager +// --------------------------------------------------------------------------- + +var _ itemPager = &eventPager{} + +type eventPager struct { + gs graph.Servicer + builder *users.ItemCalendarsItemEventsRequestBuilder + options *users.ItemCalendarsItemEventsRequestBuilderGetRequestConfiguration +} + +func (c Events) NewEventPager( + ctx context.Context, + userID, containerID string, + immutableIDs bool, +) (itemPager, error) { + options := &users.ItemCalendarsItemEventsRequestBuilderGetRequestConfiguration{ + Headers: newPreferHeaders(preferPageSize(maxNonDeltaPageSize), preferImmutableIDs(immutableIDs)), + } + + builder := c.Stable. + Client(). + Users(). + ByUserId(userID). + Calendars(). + ByCalendarId(containerID). + Events() + + return &eventPager{c.Stable, builder, options}, nil +} + +func (p *eventPager) getPage(ctx context.Context) (DeltaPageLinker, error) { + resp, err := p.builder.Get(ctx, p.options) + if err != nil { + return nil, graph.Stack(ctx, err) + } + + return EmptyDeltaLinker[models.Eventable]{PageLinkValuer: resp}, nil +} + +func (p *eventPager) setNext(nextLink string) { + p.builder = users.NewItemCalendarsItemEventsRequestBuilder(nextLink, p.gs.Adapter()) +} + +// non delta pagers don't need reset +func (p *eventPager) reset(context.Context) {} + +func (p *eventPager) valuesIn(pl PageLinker) ([]getIDAndAddtler, error) { + return toValues[models.Eventable](pl) +} + +// --------------------------------------------------------------------------- +// delta item pager +// --------------------------------------------------------------------------- + +var _ itemPager = &eventDeltaPager{} + +type eventDeltaPager struct { + gs graph.Servicer + userID string + containerID string + builder *users.ItemCalendarsItemEventsDeltaRequestBuilder + options *users.ItemCalendarsItemEventsDeltaRequestBuilderGetRequestConfiguration +} + +func (c Events) NewEventDeltaPager( + ctx context.Context, + userID, containerID, oldDelta string, + immutableIDs bool, +) (itemPager, error) { + options := &users.ItemCalendarsItemEventsDeltaRequestBuilderGetRequestConfiguration{ + Headers: newPreferHeaders(preferPageSize(maxDeltaPageSize), preferImmutableIDs(immutableIDs)), + } + + var builder *users.ItemCalendarsItemEventsDeltaRequestBuilder + + if oldDelta == "" { + builder = getEventDeltaBuilder(ctx, c.Stable, userID, containerID, options) + } else { + builder = users.NewItemCalendarsItemEventsDeltaRequestBuilder(oldDelta, c.Stable.Adapter()) + } + + return &eventDeltaPager{c.Stable, userID, containerID, builder, options}, nil +} + +func getEventDeltaBuilder( + ctx context.Context, + gs graph.Servicer, + userID, containerID string, + options *users.ItemCalendarsItemEventsDeltaRequestBuilderGetRequestConfiguration, +) *users.ItemCalendarsItemEventsDeltaRequestBuilder { + // Graph SDK only supports delta queries against events on the beta version, so we're + // manufacturing use of the beta version url to make the call instead. + // See: https://learn.microsoft.com/ko-kr/graph/api/event-delta?view=graph-rest-beta&tabs=http + // Note that the delta item body is skeletal compared to the actual event struct. Lucky + // for us, we only need the item ID. As a result, even though we hacked the version, the + // response body parses properly into the v1.0 structs and complies with our wanted interfaces. + // Likewise, the NextLink and DeltaLink odata tags carry our hack forward, so the rest of the code + // works as intended (until, at least, we want to _not_ call the beta anymore). + rawURL := fmt.Sprintf(eventBetaDeltaURLTemplate, userID, containerID) + builder := users.NewItemCalendarsItemEventsDeltaRequestBuilder(rawURL, gs.Adapter()) + + return builder +} + +func (p *eventDeltaPager) getPage(ctx context.Context) (DeltaPageLinker, error) { + resp, err := p.builder.Get(ctx, p.options) + if err != nil { + return nil, graph.Stack(ctx, err) + } + + return resp, nil +} + +func (p *eventDeltaPager) setNext(nextLink string) { + p.builder = users.NewItemCalendarsItemEventsDeltaRequestBuilder(nextLink, p.gs.Adapter()) +} + +func (p *eventDeltaPager) reset(ctx context.Context) { + p.builder = getEventDeltaBuilder(ctx, p.gs, p.userID, p.containerID, p.options) +} + +func (p *eventDeltaPager) valuesIn(pl PageLinker) ([]getIDAndAddtler, error) { + return toValues[models.Eventable](pl) +} + +func (c Events) GetAddedAndRemovedItemIDs( + ctx context.Context, + userID, containerID, oldDelta string, + immutableIDs bool, + canMakeDeltaQueries bool, +) ([]string, []string, DeltaUpdate, error) { + ctx = clues.Add(ctx, "container_id", containerID) + + pager, err := c.NewEventPager(ctx, userID, containerID, immutableIDs) + if err != nil { + return nil, nil, DeltaUpdate{}, graph.Wrap(ctx, err, "creating non-delta pager") + } + + deltaPager, err := c.NewEventDeltaPager(ctx, userID, containerID, oldDelta, immutableIDs) + if err != nil { + return nil, nil, DeltaUpdate{}, graph.Wrap(ctx, err, "creating delta pager") + } + + return getAddedAndRemovedItemIDs(ctx, c.Stable, pager, deltaPager, oldDelta, canMakeDeltaQueries) +} diff --git a/src/pkg/services/m365/api/mail.go b/src/pkg/services/m365/api/mail.go index 6645bb27e..f08cbb7c5 100644 --- a/src/pkg/services/m365/api/mail.go +++ b/src/pkg/services/m365/api/mail.go @@ -17,7 +17,6 @@ import ( "github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/logger" - "github.com/alcionai/corso/src/pkg/selectors" ) const ( @@ -188,109 +187,6 @@ func (c Mail) PatchFolder( return nil } -// --------------------------------------------------------------------------- -// container pager -// --------------------------------------------------------------------------- - -type mailFolderPager struct { - service graph.Servicer - builder *users.ItemMailFoldersRequestBuilder -} - -func (c Mail) NewMailFolderPager(userID string) mailFolderPager { - // v1.0 non delta /mailFolders endpoint does not return any of the nested folders - rawURL := fmt.Sprintf(mailFoldersBetaURLTemplate, userID) - builder := users.NewItemMailFoldersRequestBuilder(rawURL, c.Stable.Adapter()) - - return mailFolderPager{c.Stable, builder} -} - -func (p *mailFolderPager) getPage(ctx context.Context) (PageLinker, error) { - page, err := p.builder.Get(ctx, nil) - if err != nil { - return nil, graph.Stack(ctx, err) - } - - return page, nil -} - -func (p *mailFolderPager) setNext(nextLink string) { - p.builder = users.NewItemMailFoldersRequestBuilder(nextLink, p.service.Adapter()) -} - -func (p *mailFolderPager) valuesIn(pl PageLinker) ([]models.MailFolderable, error) { - // Ideally this should be `users.ItemMailFoldersResponseable`, but - // that is not a thing as stable returns different result - page, ok := pl.(models.MailFolderCollectionResponseable) - if !ok { - return nil, clues.New("converting to ItemMailFoldersResponseable") - } - - return page.GetValue(), nil -} - -// EnumerateContainers iterates through all of the users current -// mail folders, converting each to a graph.CacheFolder, and calling -// fn(cf) on each one. -// Folder hierarchy is represented in its current state, and does -// not contain historical data. -func (c Mail) EnumerateContainers( - ctx context.Context, - userID, baseContainerID string, - fn func(graph.CachedContainer) error, - errs *fault.Bus, -) error { - el := errs.Local() - pgr := c.NewMailFolderPager(userID) - - for { - if el.Failure() != nil { - break - } - - page, err := pgr.getPage(ctx) - if err != nil { - return graph.Stack(ctx, err) - } - - resp, err := pgr.valuesIn(page) - if err != nil { - return graph.Stack(ctx, err) - } - - for _, fold := range resp { - if el.Failure() != nil { - break - } - - if err := graph.CheckIDNameAndParentFolderID(fold); err != nil { - errs.AddRecoverable(ctx, graph.Stack(ctx, err).Label(fault.LabelForceNoBackupCreation)) - continue - } - - fctx := clues.Add( - ctx, - "container_id", ptr.Val(fold.GetId()), - "container_name", ptr.Val(fold.GetDisplayName())) - - temp := graph.NewCacheFolder(fold, nil, nil) - if err := fn(&temp); err != nil { - errs.AddRecoverable(ctx, graph.Stack(fctx, err).Label(fault.LabelForceNoBackupCreation)) - continue - } - } - - link, ok := ptr.ValOK(page.GetOdataNextLink()) - if !ok { - break - } - - pgr.setNext(link) - } - - return el.Failure() -} - // --------------------------------------------------------------------------- // items // --------------------------------------------------------------------------- @@ -549,161 +445,6 @@ func (c Mail) PostLargeAttachment( return us, nil } -// --------------------------------------------------------------------------- -// item pager -// --------------------------------------------------------------------------- - -var _ itemPager = &mailPager{} - -type mailPager struct { - gs graph.Servicer - builder *users.ItemMailFoldersItemMessagesRequestBuilder - options *users.ItemMailFoldersItemMessagesRequestBuilderGetRequestConfiguration -} - -func (c Mail) NewMailPager( - ctx context.Context, - userID, containerID string, - immutableIDs bool, -) itemPager { - config := &users.ItemMailFoldersItemMessagesRequestBuilderGetRequestConfiguration{ - QueryParameters: &users.ItemMailFoldersItemMessagesRequestBuilderGetQueryParameters{ - Select: idAnd("isRead"), - }, - Headers: newPreferHeaders(preferPageSize(maxNonDeltaPageSize), preferImmutableIDs(immutableIDs)), - } - - builder := c.Stable. - Client(). - Users(). - ByUserId(userID). - MailFolders(). - ByMailFolderId(containerID). - Messages() - - return &mailPager{c.Stable, builder, config} -} - -func (p *mailPager) getPage(ctx context.Context) (DeltaPageLinker, error) { - page, err := p.builder.Get(ctx, p.options) - if err != nil { - return nil, graph.Stack(ctx, err) - } - - return EmptyDeltaLinker[models.Messageable]{PageLinkValuer: page}, nil -} - -func (p *mailPager) setNext(nextLink string) { - p.builder = users.NewItemMailFoldersItemMessagesRequestBuilder(nextLink, p.gs.Adapter()) -} - -// non delta pagers don't have reset -func (p *mailPager) reset(context.Context) {} - -func (p *mailPager) valuesIn(pl PageLinker) ([]getIDAndAddtler, error) { - return toValues[models.Messageable](pl) -} - -// --------------------------------------------------------------------------- -// delta item pager -// --------------------------------------------------------------------------- - -var _ itemPager = &mailDeltaPager{} - -type mailDeltaPager struct { - gs graph.Servicer - userID string - containerID string - builder *users.ItemMailFoldersItemMessagesDeltaRequestBuilder - options *users.ItemMailFoldersItemMessagesDeltaRequestBuilderGetRequestConfiguration -} - -func getMailDeltaBuilder( - ctx context.Context, - gs graph.Servicer, - user, containerID string, - options *users.ItemMailFoldersItemMessagesDeltaRequestBuilderGetRequestConfiguration, -) *users.ItemMailFoldersItemMessagesDeltaRequestBuilder { - builder := gs. - Client(). - Users(). - ByUserId(user). - MailFolders(). - ByMailFolderId(containerID). - Messages(). - Delta() - - return builder -} - -func (c Mail) NewMailDeltaPager( - ctx context.Context, - userID, containerID, oldDelta string, - immutableIDs bool, -) itemPager { - config := &users.ItemMailFoldersItemMessagesDeltaRequestBuilderGetRequestConfiguration{ - QueryParameters: &users.ItemMailFoldersItemMessagesDeltaRequestBuilderGetQueryParameters{ - Select: idAnd("isRead"), - }, - Headers: newPreferHeaders(preferPageSize(maxDeltaPageSize), preferImmutableIDs(immutableIDs)), - } - - var builder *users.ItemMailFoldersItemMessagesDeltaRequestBuilder - - if len(oldDelta) > 0 { - builder = users.NewItemMailFoldersItemMessagesDeltaRequestBuilder(oldDelta, c.Stable.Adapter()) - } else { - builder = getMailDeltaBuilder(ctx, c.Stable, userID, containerID, config) - } - - return &mailDeltaPager{c.Stable, userID, containerID, builder, config} -} - -func (p *mailDeltaPager) getPage(ctx context.Context) (DeltaPageLinker, error) { - page, err := p.builder.Get(ctx, p.options) - if err != nil { - return nil, graph.Stack(ctx, err) - } - - return page, nil -} - -func (p *mailDeltaPager) setNext(nextLink string) { - p.builder = users.NewItemMailFoldersItemMessagesDeltaRequestBuilder(nextLink, p.gs.Adapter()) -} - -func (p *mailDeltaPager) reset(ctx context.Context) { - p.builder = p.gs. - Client(). - Users(). - ByUserId(p.userID). - MailFolders(). - ByMailFolderId(p.containerID). - Messages(). - Delta() -} - -func (p *mailDeltaPager) valuesIn(pl PageLinker) ([]getIDAndAddtler, error) { - return toValues[models.Messageable](pl) -} - -func (c Mail) GetAddedAndRemovedItemIDs( - ctx context.Context, - userID, containerID, oldDelta string, - immutableIDs bool, - canMakeDeltaQueries bool, -) ([]string, []string, DeltaUpdate, error) { - ctx = clues.Add( - ctx, - "category", selectors.ExchangeMail, - "container_id", containerID) - - pager := c.NewMailPager(ctx, userID, containerID, immutableIDs) - deltaPager := c.NewMailDeltaPager(ctx, userID, containerID, oldDelta, immutableIDs) - - return getAddedAndRemovedItemIDs(ctx, c.Stable, pager, deltaPager, oldDelta, canMakeDeltaQueries) -} - // --------------------------------------------------------------------------- // Serialization // --------------------------------------------------------------------------- diff --git a/src/pkg/services/m365/api/mail_pager.go b/src/pkg/services/m365/api/mail_pager.go new file mode 100644 index 000000000..71ce09663 --- /dev/null +++ b/src/pkg/services/m365/api/mail_pager.go @@ -0,0 +1,273 @@ +package api + +import ( + "context" + "fmt" + + "github.com/alcionai/clues" + "github.com/microsoftgraph/msgraph-sdk-go/models" + "github.com/microsoftgraph/msgraph-sdk-go/users" + + "github.com/alcionai/corso/src/internal/common/ptr" + "github.com/alcionai/corso/src/internal/m365/graph" + "github.com/alcionai/corso/src/pkg/fault" + "github.com/alcionai/corso/src/pkg/selectors" +) + +// --------------------------------------------------------------------------- +// container pager +// --------------------------------------------------------------------------- + +type mailFolderPager struct { + service graph.Servicer + builder *users.ItemMailFoldersRequestBuilder +} + +func (c Mail) NewMailFolderPager(userID string) mailFolderPager { + // v1.0 non delta /mailFolders endpoint does not return any of the nested folders + rawURL := fmt.Sprintf(mailFoldersBetaURLTemplate, userID) + builder := users.NewItemMailFoldersRequestBuilder(rawURL, c.Stable.Adapter()) + + return mailFolderPager{c.Stable, builder} +} + +func (p *mailFolderPager) getPage(ctx context.Context) (PageLinker, error) { + page, err := p.builder.Get(ctx, nil) + if err != nil { + return nil, graph.Stack(ctx, err) + } + + return page, nil +} + +func (p *mailFolderPager) setNext(nextLink string) { + p.builder = users.NewItemMailFoldersRequestBuilder(nextLink, p.service.Adapter()) +} + +func (p *mailFolderPager) valuesIn(pl PageLinker) ([]models.MailFolderable, error) { + // Ideally this should be `users.ItemMailFoldersResponseable`, but + // that is not a thing as stable returns different result + page, ok := pl.(models.MailFolderCollectionResponseable) + if !ok { + return nil, clues.New("converting to ItemMailFoldersResponseable") + } + + return page.GetValue(), nil +} + +// EnumerateContainers iterates through all of the users current +// mail folders, converting each to a graph.CacheFolder, and calling +// fn(cf) on each one. +// Folder hierarchy is represented in its current state, and does +// not contain historical data. +func (c Mail) EnumerateContainers( + ctx context.Context, + userID, baseContainerID string, + fn func(graph.CachedContainer) error, + errs *fault.Bus, +) error { + el := errs.Local() + pgr := c.NewMailFolderPager(userID) + + for { + if el.Failure() != nil { + break + } + + page, err := pgr.getPage(ctx) + if err != nil { + return graph.Stack(ctx, err) + } + + resp, err := pgr.valuesIn(page) + if err != nil { + return graph.Stack(ctx, err) + } + + for _, fold := range resp { + if el.Failure() != nil { + break + } + + if err := graph.CheckIDNameAndParentFolderID(fold); err != nil { + errs.AddRecoverable(ctx, graph.Stack(ctx, err).Label(fault.LabelForceNoBackupCreation)) + continue + } + + fctx := clues.Add( + ctx, + "container_id", ptr.Val(fold.GetId()), + "container_name", ptr.Val(fold.GetDisplayName())) + + temp := graph.NewCacheFolder(fold, nil, nil) + if err := fn(&temp); err != nil { + errs.AddRecoverable(ctx, graph.Stack(fctx, err).Label(fault.LabelForceNoBackupCreation)) + continue + } + } + + link, ok := ptr.ValOK(page.GetOdataNextLink()) + if !ok { + break + } + + pgr.setNext(link) + } + + return el.Failure() +} + +// --------------------------------------------------------------------------- +// item pager +// --------------------------------------------------------------------------- + +var _ itemPager = &mailPager{} + +type mailPager struct { + gs graph.Servicer + builder *users.ItemMailFoldersItemMessagesRequestBuilder + options *users.ItemMailFoldersItemMessagesRequestBuilderGetRequestConfiguration +} + +func (c Mail) NewMailPager( + ctx context.Context, + userID, containerID string, + immutableIDs bool, +) itemPager { + config := &users.ItemMailFoldersItemMessagesRequestBuilderGetRequestConfiguration{ + QueryParameters: &users.ItemMailFoldersItemMessagesRequestBuilderGetQueryParameters{ + Select: idAnd("isRead"), + }, + Headers: newPreferHeaders(preferPageSize(maxNonDeltaPageSize), preferImmutableIDs(immutableIDs)), + } + + builder := c.Stable. + Client(). + Users(). + ByUserId(userID). + MailFolders(). + ByMailFolderId(containerID). + Messages() + + return &mailPager{c.Stable, builder, config} +} + +func (p *mailPager) getPage(ctx context.Context) (DeltaPageLinker, error) { + page, err := p.builder.Get(ctx, p.options) + if err != nil { + return nil, graph.Stack(ctx, err) + } + + return EmptyDeltaLinker[models.Messageable]{PageLinkValuer: page}, nil +} + +func (p *mailPager) setNext(nextLink string) { + p.builder = users.NewItemMailFoldersItemMessagesRequestBuilder(nextLink, p.gs.Adapter()) +} + +// non delta pagers don't have reset +func (p *mailPager) reset(context.Context) {} + +func (p *mailPager) valuesIn(pl PageLinker) ([]getIDAndAddtler, error) { + return toValues[models.Messageable](pl) +} + +// --------------------------------------------------------------------------- +// delta item pager +// --------------------------------------------------------------------------- + +var _ itemPager = &mailDeltaPager{} + +type mailDeltaPager struct { + gs graph.Servicer + userID string + containerID string + builder *users.ItemMailFoldersItemMessagesDeltaRequestBuilder + options *users.ItemMailFoldersItemMessagesDeltaRequestBuilderGetRequestConfiguration +} + +func getMailDeltaBuilder( + ctx context.Context, + gs graph.Servicer, + user, containerID string, + options *users.ItemMailFoldersItemMessagesDeltaRequestBuilderGetRequestConfiguration, +) *users.ItemMailFoldersItemMessagesDeltaRequestBuilder { + builder := gs. + Client(). + Users(). + ByUserId(user). + MailFolders(). + ByMailFolderId(containerID). + Messages(). + Delta() + + return builder +} + +func (c Mail) NewMailDeltaPager( + ctx context.Context, + userID, containerID, oldDelta string, + immutableIDs bool, +) itemPager { + config := &users.ItemMailFoldersItemMessagesDeltaRequestBuilderGetRequestConfiguration{ + QueryParameters: &users.ItemMailFoldersItemMessagesDeltaRequestBuilderGetQueryParameters{ + Select: idAnd("isRead"), + }, + Headers: newPreferHeaders(preferPageSize(maxDeltaPageSize), preferImmutableIDs(immutableIDs)), + } + + var builder *users.ItemMailFoldersItemMessagesDeltaRequestBuilder + + if len(oldDelta) > 0 { + builder = users.NewItemMailFoldersItemMessagesDeltaRequestBuilder(oldDelta, c.Stable.Adapter()) + } else { + builder = getMailDeltaBuilder(ctx, c.Stable, userID, containerID, config) + } + + return &mailDeltaPager{c.Stable, userID, containerID, builder, config} +} + +func (p *mailDeltaPager) getPage(ctx context.Context) (DeltaPageLinker, error) { + page, err := p.builder.Get(ctx, p.options) + if err != nil { + return nil, graph.Stack(ctx, err) + } + + return page, nil +} + +func (p *mailDeltaPager) setNext(nextLink string) { + p.builder = users.NewItemMailFoldersItemMessagesDeltaRequestBuilder(nextLink, p.gs.Adapter()) +} + +func (p *mailDeltaPager) reset(ctx context.Context) { + p.builder = p.gs. + Client(). + Users(). + ByUserId(p.userID). + MailFolders(). + ByMailFolderId(p.containerID). + Messages(). + Delta() +} + +func (p *mailDeltaPager) valuesIn(pl PageLinker) ([]getIDAndAddtler, error) { + return toValues[models.Messageable](pl) +} + +func (c Mail) GetAddedAndRemovedItemIDs( + ctx context.Context, + userID, containerID, oldDelta string, + immutableIDs bool, + canMakeDeltaQueries bool, +) ([]string, []string, DeltaUpdate, error) { + ctx = clues.Add( + ctx, + "category", selectors.ExchangeMail, + "container_id", containerID) + + pager := c.NewMailPager(ctx, userID, containerID, immutableIDs) + deltaPager := c.NewMailDeltaPager(ctx, userID, containerID, oldDelta, immutableIDs) + + return getAddedAndRemovedItemIDs(ctx, c.Stable, pager, deltaPager, oldDelta, canMakeDeltaQueries) +}