From b331f38654fc1342e5b2bcd054a039161361d0c2 Mon Sep 17 00:00:00 2001 From: Keepers Date: Mon, 24 Apr 2023 14:06:41 -0600 Subject: [PATCH] fixup mailbox info (#3189) #### Does this PR need a docs update or release note? - [x] :no_entry: No #### Type of change - [x] :bug: Bugfix - [x] :broom: Tech Debt/Cleanup #### Test Plan - [x] :muscle: Manual - [x] :green_heart: E2E --- .../connector/discovery/discovery_test.go | 57 ++- src/internal/connector/graph/errors.go | 65 ++-- src/internal/connector/graph/errors_test.go | 10 +- src/internal/connector/onedrive/drive_test.go | 3 +- src/pkg/services/m365/api/users.go | 349 +++++++++--------- 5 files changed, 246 insertions(+), 238 deletions(-) diff --git a/src/internal/connector/discovery/discovery_test.go b/src/internal/connector/discovery/discovery_test.go index 4c80ba2c6..9c889d28a 100644 --- a/src/internal/connector/discovery/discovery_test.go +++ b/src/internal/connector/discovery/discovery_test.go @@ -18,19 +18,19 @@ import ( "github.com/alcionai/corso/src/pkg/services/m365/api" ) -type DiscoveryIntegrationSuite struct { +type DiscoveryIntgSuite struct { tester.Suite } -func TestDiscoveryIntegrationSuite(t *testing.T) { - suite.Run(t, &DiscoveryIntegrationSuite{ +func TestDiscoveryIntgSuite(t *testing.T) { + suite.Run(t, &DiscoveryIntgSuite{ Suite: tester.NewIntegrationSuite( t, [][]string{tester.M365AcctCredEnvs}), }) } -func (suite *DiscoveryIntegrationSuite) TestUsers() { +func (suite *DiscoveryIntgSuite) TestUsers() { ctx, flush := tester.NewContext() defer flush() @@ -55,7 +55,7 @@ func (suite *DiscoveryIntegrationSuite) TestUsers() { assert.NotEmpty(t, users) } -func (suite *DiscoveryIntegrationSuite) TestUsers_InvalidCredentials() { +func (suite *DiscoveryIntgSuite) TestUsers_InvalidCredentials() { table := []struct { name string acct func(t *testing.T) account.Account @@ -101,7 +101,7 @@ func (suite *DiscoveryIntegrationSuite) TestUsers_InvalidCredentials() { } } -func (suite *DiscoveryIntegrationSuite) TestSites() { +func (suite *DiscoveryIntgSuite) TestSites() { ctx, flush := tester.NewContext() defer flush() @@ -120,7 +120,7 @@ func (suite *DiscoveryIntegrationSuite) TestSites() { assert.NotEmpty(t, sites) } -func (suite *DiscoveryIntegrationSuite) TestSites_InvalidCredentials() { +func (suite *DiscoveryIntgSuite) TestSites_InvalidCredentials() { ctx, flush := tester.NewContext() defer flush() @@ -171,10 +171,9 @@ func (suite *DiscoveryIntegrationSuite) TestSites_InvalidCredentials() { } } -func (suite *DiscoveryIntegrationSuite) TestUserInfo() { +func (suite *DiscoveryIntgSuite) TestUserInfo() { t := suite.T() acct := tester.NewM365Account(t) - userID := tester.M365UserID(t) creds, err := acct.M365Config() require.NoError(t, err) @@ -185,37 +184,34 @@ func (suite *DiscoveryIntegrationSuite) TestUserInfo() { uapi := cli.Users() table := []struct { - name string - user string - expect *api.UserInfo + name string + user string + expect *api.UserInfo + expectErr require.ErrorAssertionFunc }{ { name: "standard test user", - user: userID, + user: tester.M365UserID(t), expect: &api.UserInfo{ DiscoveredServices: map[path.ServiceType]struct{}{ path.ExchangeService: {}, path.OneDriveService: {}, }, - HasMailBox: true, - HasOneDrive: true, Mailbox: api.MailboxInfo{ Purpose: "user", ErrGetMailBoxSetting: nil, }, }, + expectErr: require.NoError, }, { name: "user does not exist", user: uuid.NewString(), expect: &api.UserInfo{ DiscoveredServices: map[path.ServiceType]struct{}{}, - HasMailBox: false, - HasOneDrive: false, - Mailbox: api.MailboxInfo{ - ErrGetMailBoxSetting: api.ErrMailBoxSettingsNotFound, - }, + Mailbox: api.MailboxInfo{}, }, + expectErr: require.NoError, }, } for _, test := range table { @@ -226,15 +222,18 @@ func (suite *DiscoveryIntegrationSuite) TestUserInfo() { t := suite.T() result, err := discovery.UserInfo(ctx, uapi, test.user) - require.NoError(t, err, clues.ToCore(err)) - assert.Equal(t, test.expect.HasMailBox, result.HasMailBox) - assert.Equal(t, test.expect.HasOneDrive, result.HasOneDrive) + test.expectErr(t, err, clues.ToCore(err)) + + if err != nil { + return + } + assert.Equal(t, test.expect.DiscoveredServices, result.DiscoveredServices) }) } } -func (suite *DiscoveryIntegrationSuite) TestUserWithoutDrive() { +func (suite *DiscoveryIntgSuite) TestUserWithoutDrive() { t := suite.T() acct := tester.NewM365Account(t) userID := tester.M365UserID(t) @@ -249,10 +248,8 @@ func (suite *DiscoveryIntegrationSuite) TestUserWithoutDrive() { user: "a53c26f7-5100-4acb-a910-4d20960b2c19", // User: testevents@10rqc2.onmicrosoft.com expect: &api.UserInfo{ DiscoveredServices: map[path.ServiceType]struct{}{}, - HasOneDrive: false, - HasMailBox: false, Mailbox: api.MailboxInfo{ - ErrGetMailBoxSetting: api.ErrMailBoxSettingsNotFound, + ErrGetMailBoxSetting: []error{api.ErrMailBoxSettingsNotFound}, }, }, }, @@ -264,11 +261,9 @@ func (suite *DiscoveryIntegrationSuite) TestUserWithoutDrive() { path.ExchangeService: {}, path.OneDriveService: {}, }, - HasOneDrive: true, - HasMailBox: true, Mailbox: api.MailboxInfo{ Purpose: "user", - ErrGetMailBoxSetting: nil, + ErrGetMailBoxSetting: []error{}, }, }, }, @@ -283,8 +278,6 @@ func (suite *DiscoveryIntegrationSuite) TestUserWithoutDrive() { result, err := discovery.GetUserInfo(ctx, acct, test.user, fault.New(true)) require.NoError(t, err, clues.ToCore(err)) assert.Equal(t, test.expect.DiscoveredServices, result.DiscoveredServices) - assert.Equal(t, test.expect.HasOneDrive, result.HasOneDrive) - assert.Equal(t, test.expect.HasMailBox, result.HasMailBox) assert.Equal(t, test.expect.Mailbox.ErrGetMailBoxSetting, result.Mailbox.ErrGetMailBoxSetting) assert.Equal(t, test.expect.Mailbox.Purpose, result.Mailbox.Purpose) }) diff --git a/src/internal/connector/graph/errors.go b/src/internal/connector/graph/errors.go index d5dca985a..513fa0b89 100644 --- a/src/internal/connector/graph/errors.go +++ b/src/internal/connector/graph/errors.go @@ -14,10 +14,10 @@ import ( "github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/microsoftgraph/msgraph-sdk-go/models/odataerrors" "github.com/pkg/errors" - "golang.org/x/exp/slices" "github.com/alcionai/corso/src/internal/common/ptr" "github.com/alcionai/corso/src/pkg/fault" + "github.com/alcionai/corso/src/pkg/filters" "github.com/alcionai/corso/src/pkg/logger" ) @@ -25,20 +25,22 @@ import ( // Error Interpretation Helpers // --------------------------------------------------------------------------- +type errorCode string + const ( - errCodeActivityLimitReached = "activityLimitReached" - errCodeItemNotFound = "ErrorItemNotFound" - errCodeItemNotFoundShort = "itemNotFound" - errCodeEmailFolderNotFound = "ErrorSyncFolderNotFound" - errCodeResyncRequired = "ResyncRequired" // alt: resyncRequired - errCodeMalwareDetected = "malwareDetected" - errCodeSyncFolderNotFound = "ErrorSyncFolderNotFound" - errCodeSyncStateNotFound = "SyncStateNotFound" - errCodeSyncStateInvalid = "SyncStateInvalid" - errCodeResourceNotFound = "ResourceNotFound" - errCodeRequestResourceNotFound = "Request_ResourceNotFound" - errCodeMailboxNotEnabledForRESTAPI = "MailboxNotEnabledForRESTAPI" - errCodeErrorAccessDenied = "ErrorAccessDenied" + activityLimitReached errorCode = "activityLimitReached" + emailFolderNotFound errorCode = "ErrorSyncFolderNotFound" + errorAccessDenied errorCode = "ErrorAccessDenied" + itemNotFound errorCode = "ErrorItemNotFound" + itemNotFoundShort errorCode = "itemNotFound" + mailboxNotEnabledForRESTAPI errorCode = "MailboxNotEnabledForRESTAPI" + malwareDetected errorCode = "malwareDetected" + requestResourceNotFound errorCode = "Request_ResourceNotFound" + resourceNotFound errorCode = "ResourceNotFound" + resyncRequired errorCode = "ResyncRequired" // alt: resyncRequired + syncFolderNotFound errorCode = "ErrorSyncFolderNotFound" + syncStateInvalid errorCode = "SyncStateInvalid" + syncStateNotFound errorCode = "SyncStateNotFound" ) const ( @@ -84,9 +86,9 @@ func IsErrDeletedInFlight(err error) bool { if hasErrorCode( err, - errCodeItemNotFound, - errCodeItemNotFoundShort, - errCodeSyncFolderNotFound, + itemNotFound, + itemNotFoundShort, + syncFolderNotFound, ) { return true } @@ -95,20 +97,24 @@ func IsErrDeletedInFlight(err error) bool { } func IsErrInvalidDelta(err error) bool { - return hasErrorCode(err, errCodeSyncStateNotFound, errCodeResyncRequired, errCodeSyncStateInvalid) || + return hasErrorCode(err, syncStateNotFound, resyncRequired, syncStateInvalid) || errors.Is(err, ErrInvalidDelta) } func IsErrExchangeMailFolderNotFound(err error) bool { - return hasErrorCode(err, errCodeResourceNotFound, errCodeMailboxNotEnabledForRESTAPI) + return hasErrorCode(err, resourceNotFound, mailboxNotEnabledForRESTAPI) } func IsErrUserNotFound(err error) bool { - return hasErrorCode(err, errCodeRequestResourceNotFound) + return hasErrorCode(err, requestResourceNotFound) +} + +func IsErrResourceNotFound(err error) bool { + return hasErrorCode(err, resourceNotFound) } func IsErrAccessDenied(err error) bool { - return hasErrorCode(err, errCodeErrorAccessDenied) + return hasErrorCode(err, errorAccessDenied) || clues.HasLabel(err, LabelStatus(http.StatusForbidden)) } func IsErrTimeout(err error) bool { @@ -143,7 +149,7 @@ func LabelStatus(statusCode int) string { // IsMalware is true if the graphAPI returns a "malware detected" error code. func IsMalware(err error) bool { - return hasErrorCode(err, errCodeMalwareDetected) + return hasErrorCode(err, malwareDetected) } func IsMalwareResp(ctx context.Context, resp *http.Response) bool { @@ -159,7 +165,7 @@ func IsMalwareResp(ctx context.Context, resp *http.Response) bool { return false } - if strings.Contains(string(respDump), errCodeMalwareDetected) { + if strings.Contains(string(respDump), string(malwareDetected)) { return true } @@ -170,7 +176,7 @@ func IsMalwareResp(ctx context.Context, resp *http.Response) bool { // error parsers // --------------------------------------------------------------------------- -func hasErrorCode(err error, codes ...string) bool { +func hasErrorCode(err error, codes ...errorCode) bool { if err == nil { return false } @@ -180,16 +186,17 @@ func hasErrorCode(err error, codes ...string) bool { return false } - if oDataError.GetError().GetCode() == nil { + code, ok := ptr.ValOK(oDataError.GetError().GetCode()) + if !ok { return false } - lcodes := []string{} - for _, c := range codes { - lcodes = append(lcodes, strings.ToLower(c)) + cs := make([]string, len(codes)) + for i, c := range codes { + cs[i] = string(c) } - return slices.Contains(lcodes, strings.ToLower(*oDataError.GetError().GetCode())) + return filters.Equal(cs).Compare(code) } // Wrap is a helper function that extracts ODataError metadata from diff --git a/src/internal/connector/graph/errors_test.go b/src/internal/connector/graph/errors_test.go index 56b2fba1f..c12230148 100644 --- a/src/internal/connector/graph/errors_test.go +++ b/src/internal/connector/graph/errors_test.go @@ -90,12 +90,12 @@ func (suite *GraphErrorsUnitSuite) TestIsErrDeletedInFlight() { }, { name: "not-found oDataErr", - err: odErr(errCodeItemNotFound), + err: odErr(string(itemNotFound)), expect: assert.True, }, { name: "sync-not-found oDataErr", - err: odErr(errCodeSyncFolderNotFound), + err: odErr(string(syncFolderNotFound)), expect: assert.True, }, } @@ -134,12 +134,12 @@ func (suite *GraphErrorsUnitSuite) TestIsErrInvalidDelta() { }, { name: "resync-required oDataErr", - err: odErr(errCodeResyncRequired), + err: odErr(string(resyncRequired)), expect: assert.True, }, { name: "sync state invalid oDataErr", - err: odErr(errCodeSyncStateInvalid), + err: odErr(string(syncStateInvalid)), expect: assert.True, }, // next two tests are to make sure the checks are case insensitive @@ -184,7 +184,7 @@ func (suite *GraphErrorsUnitSuite) TestIsErrUserNotFound() { }, { name: "request resource not found oDataErr", - err: odErr(errCodeRequestResourceNotFound), + err: odErr(string(requestResourceNotFound)), expect: assert.True, }, } diff --git a/src/internal/connector/onedrive/drive_test.go b/src/internal/connector/onedrive/drive_test.go index 06b460cff..fde067adf 100644 --- a/src/internal/connector/onedrive/drive_test.go +++ b/src/internal/connector/onedrive/drive_test.go @@ -286,8 +286,7 @@ func TestOneDriveDriveSuite(t *testing.T) { suite.Run(t, &OneDriveSuite{ Suite: tester.NewIntegrationSuite( t, - [][]string{tester.M365AcctCredEnvs}, - ), + [][]string{tester.M365AcctCredEnvs}), }) } diff --git a/src/pkg/services/m365/api/users.go b/src/pkg/services/m365/api/users.go index b2916f4fd..5d21a0333 100644 --- a/src/pkg/services/m365/api/users.go +++ b/src/pkg/services/m365/api/users.go @@ -3,6 +3,7 @@ package api import ( "context" "fmt" + "net/http" "github.com/alcionai/clues" abstractions "github.com/microsoft/kiota-abstractions-go" @@ -41,8 +42,6 @@ type Users struct { type UserInfo struct { DiscoveredServices map[path.ServiceType]struct{} - HasMailBox bool - HasOneDrive bool Mailbox MailboxInfo } @@ -56,7 +55,7 @@ type MailboxInfo struct { AutomaticRepliesSetting AutomaticRepliesSettings Language Language WorkingHours WorkingHours - ErrGetMailBoxSetting error + ErrGetMailBoxSetting []error } type AutomaticRepliesSettings struct { @@ -229,204 +228,212 @@ func (c Users) GetIDAndName(ctx context.Context, userID string) (string, string, func (c Users) GetInfo(ctx context.Context, userID string) (*UserInfo, error) { // Assume all services are enabled // then filter down to only services the user has enabled - var ( - err error - userInfo = newUserInfo() + userInfo := newUserInfo() - requestParameters = &users.ItemMailFoldersRequestBuilderGetQueryParameters{ - Select: []string{"id"}, - Top: ptr.To[int32](1), // if we get any folders, then we have access. - } - - options = users.ItemMailFoldersRequestBuilderGetRequestConfiguration{ - QueryParameters: requestParameters, - } - ) - - userInfo.HasMailBox = true - - err = c.GetExchange(ctx, userID, options) - if err != nil { - if !graph.IsErrExchangeMailFolderNotFound(err) { - logger.Ctx(ctx).Errorf("err getting user's mail folder: %s", err) - - return nil, graph.Wrap(ctx, err, "getting user's mail folder") - } - - logger.Ctx(ctx).Infof("resource owner does not have a mailbox enabled") - delete(userInfo.DiscoveredServices, path.ExchangeService) - - userInfo.HasMailBox = false + requestParameters := users.ItemMailFoldersRequestBuilderGetQueryParameters{ + Select: []string{"id"}, + Top: ptr.To[int32](1), // if we get any folders, then we have access. } - userInfo.HasOneDrive = true + options := users.ItemMailFoldersRequestBuilderGetRequestConfiguration{ + QueryParameters: &requestParameters, + } - err = c.GetOnedrive(ctx, userID) - if err != nil { - err = graph.Stack(ctx, err) - - if !clues.HasLabel(err, graph.LabelsMysiteNotFound) { - logger.Ctx(ctx).Errorf("err getting user's onedrive's data: %s", err) - - return nil, graph.Wrap(ctx, err, "getting user's onedrive's data") + if _, err := c.GetMailFolders(ctx, userID, options); err != nil { + if graph.IsErrUserNotFound(err) { + logger.CtxErr(ctx, err).Error("user not found") + return nil, err } - logger.Ctx(ctx).Infof("resource owner does not have a drive") + if !graph.IsErrExchangeMailFolderNotFound(err) || + clues.HasLabel(err, graph.LabelStatus(http.StatusNotFound)) { + logger.CtxErr(ctx, err).Error("getting user's mail folder") + return nil, err + } + + logger.Ctx(ctx).Info("resource owner does not have a mailbox enabled") + delete(userInfo.DiscoveredServices, path.ExchangeService) + } + + if _, err := c.GetDrives(ctx, userID); err != nil { + if !clues.HasLabel(err, graph.LabelsMysiteNotFound) { + logger.CtxErr(ctx, err).Error("getting user's drives") + + return nil, graph.Wrap(ctx, err, "getting user's drives") + } + + logger.Ctx(ctx).Info("resource owner does not have a drive") delete(userInfo.DiscoveredServices, path.OneDriveService) - userInfo.HasOneDrive = false } - err = c.getAdditionalData(ctx, userID, &userInfo.Mailbox) + mbxInfo, err := c.getMailboxSettings(ctx, userID) if err != nil { return nil, err } + userInfo.Mailbox = mbxInfo + return userInfo, nil } -// verify mailbox enabled for user -func (c Users) GetExchange( +// TODO: remove when exchange api goes into this package +func (c Users) GetMailFolders( ctx context.Context, userID string, options users.ItemMailFoldersRequestBuilderGetRequestConfiguration, -) error { - _, err := c.stable.Client().UsersById(userID).MailFolders().Get(ctx, &options) +) (models.MailFolderCollectionResponseable, error) { + mailFolders, err := c.stable.Client().UsersById(userID).MailFolders().Get(ctx, &options) if err != nil { - return err + return nil, graph.Wrap(ctx, err, "getting MailFolders") } - return nil + return mailFolders, nil } -// verify onedrive enabled for user -func (c Users) GetOnedrive(ctx context.Context, userID string) error { - _, err := c.stable.Client().UsersById(userID).Drives().Get(ctx, nil) +// TODO: remove when drive api goes into this package +func (c Users) GetDrives(ctx context.Context, userID string) (models.DriveCollectionResponseable, error) { + drives, err := c.stable.Client().UsersById(userID).Drives().Get(ctx, nil) if err != nil { - return err + return nil, graph.Wrap(ctx, err, "getting drives") } - return nil + return drives, nil } -func (c Users) getAdditionalData(ctx context.Context, userID string, mailbox *MailboxInfo) error { +func (c Users) getMailboxSettings( + ctx context.Context, + userID string, +) (MailboxInfo, error) { var ( - rawURL = fmt.Sprintf("https://graph.microsoft.com/v1.0/users/%s/mailboxSettings", userID) - adapter = c.stable.Adapter() - mailBoundErr clues.Err + rawURL = fmt.Sprintf("https://graph.microsoft.com/v1.0/users/%s/mailboxSettings", userID) + adapter = c.stable.Adapter() + mi = MailboxInfo{ + ErrGetMailBoxSetting: []error{}, + } ) settings, err := users.NewUserItemRequestBuilder(rawURL, adapter).Get(ctx, nil) if err != nil && !(graph.IsErrAccessDenied(err) || graph.IsErrExchangeMailFolderNotFound(err)) { logger.CtxErr(ctx, err).Error("getting mailbox settings") - - return graph.Wrap(ctx, err, "getting additional data") + return mi, graph.Wrap(ctx, err, "getting additional data") } if graph.IsErrAccessDenied(err) { logger.Ctx(ctx).Info("err getting additional data: access denied") - mailbox.ErrGetMailBoxSetting = clues.New("access denied") + mi.ErrGetMailBoxSetting = append(mi.ErrGetMailBoxSetting, clues.New("access denied")) - return nil + return mi, nil } if graph.IsErrExchangeMailFolderNotFound(err) { - logger.Ctx(ctx).Info("err exchange mail folder not found") + logger.Ctx(ctx).Info("mailfolders not found") - mailbox.ErrGetMailBoxSetting = ErrMailBoxSettingsNotFound + mi.ErrGetMailBoxSetting = append(mi.ErrGetMailBoxSetting, ErrMailBoxSettingsNotFound) - return nil + return mi, nil } additionalData := settings.GetAdditionalData() - mailbox.ArchiveFolder = toString(ctx, additionalData["archiveFolder"], &mailBoundErr) - mailbox.Timezone = toString(ctx, additionalData["timeZone"], &mailBoundErr) - mailbox.DateFormat = toString(ctx, additionalData["dateFormat"], &mailBoundErr) - mailbox.TimeFormat = toString(ctx, additionalData["timeFormat"], &mailBoundErr) - mailbox.Purpose = toString(ctx, additionalData["userPurpose"], &mailBoundErr) - mailbox.DelegateMeetMsgDeliveryOpt = toString( - ctx, - additionalData["delegateMeetingMessageDeliveryOptions"], - &mailBoundErr) + mi.ArchiveFolder, err = toString(ctx, "archiveFolder", additionalData) + mi.ErrGetMailBoxSetting = appendIfErr(mi.ErrGetMailBoxSetting, err) + + mi.Timezone, err = toString(ctx, "timeZone", additionalData) + mi.ErrGetMailBoxSetting = appendIfErr(mi.ErrGetMailBoxSetting, err) + + mi.DateFormat, err = toString(ctx, "dateFormat", additionalData) + mi.ErrGetMailBoxSetting = appendIfErr(mi.ErrGetMailBoxSetting, err) + + mi.TimeFormat, err = toString(ctx, "timeFormat", additionalData) + mi.ErrGetMailBoxSetting = appendIfErr(mi.ErrGetMailBoxSetting, err) + + mi.Purpose, err = toString(ctx, "userPurpose", additionalData) + mi.ErrGetMailBoxSetting = appendIfErr(mi.ErrGetMailBoxSetting, err) + + mi.DelegateMeetMsgDeliveryOpt, err = toString(ctx, "delegateMeetingMessageDeliveryOptions", additionalData) + mi.ErrGetMailBoxSetting = appendIfErr(mi.ErrGetMailBoxSetting, err) // decode automatic replies settings - replySetting := toMap(ctx, additionalData["automaticRepliesSetting"], &mailBoundErr) - mailbox.AutomaticRepliesSetting.Status = toString( - ctx, - replySetting["status"], - &mailBoundErr) - mailbox.AutomaticRepliesSetting.ExternalAudience = toString( - ctx, - replySetting["externalAudience"], - &mailBoundErr) - mailbox.AutomaticRepliesSetting.ExternalReplyMessage = toString( - ctx, - replySetting["externalReplyMessage"], - &mailBoundErr) - mailbox.AutomaticRepliesSetting.InternalReplyMessage = toString( - ctx, - replySetting["internalReplyMessage"], - &mailBoundErr) + replySetting, err := toT[map[string]any](ctx, "automaticRepliesSetting", additionalData) + mi.ErrGetMailBoxSetting = appendIfErr(mi.ErrGetMailBoxSetting, err) + + mi.AutomaticRepliesSetting.Status, err = toString(ctx, "status", replySetting) + mi.ErrGetMailBoxSetting = appendIfErr(mi.ErrGetMailBoxSetting, err) + + mi.AutomaticRepliesSetting.ExternalAudience, err = toString(ctx, "externalAudience", replySetting) + mi.ErrGetMailBoxSetting = appendIfErr(mi.ErrGetMailBoxSetting, err) + + mi.AutomaticRepliesSetting.ExternalReplyMessage, err = toString(ctx, "externalReplyMessage", replySetting) + mi.ErrGetMailBoxSetting = appendIfErr(mi.ErrGetMailBoxSetting, err) + + mi.AutomaticRepliesSetting.InternalReplyMessage, err = toString(ctx, "internalReplyMessage", replySetting) + mi.ErrGetMailBoxSetting = appendIfErr(mi.ErrGetMailBoxSetting, err) // decode scheduledStartDateTime - startDateTime := toMap(ctx, replySetting["scheduledStartDateTime"], &mailBoundErr) - mailbox.AutomaticRepliesSetting.ScheduledStartDateTime.DateTime = toString( - ctx, - startDateTime["dateTime"], - &mailBoundErr) - mailbox.AutomaticRepliesSetting.ScheduledStartDateTime.Timezone = toString( - ctx, - startDateTime["timeZone"], - &mailBoundErr) + startDateTime, err := toT[map[string]any](ctx, "scheduledStartDateTime", replySetting) + mi.ErrGetMailBoxSetting = appendIfErr(mi.ErrGetMailBoxSetting, err) - endDateTime := toMap(ctx, replySetting["scheduledEndDateTime"], &mailBoundErr) - mailbox.AutomaticRepliesSetting.ScheduledEndDateTime.DateTime = toString( - ctx, - endDateTime["dateTime"], - &mailBoundErr) - mailbox.AutomaticRepliesSetting.ScheduledEndDateTime.Timezone = toString( - ctx, - endDateTime["timeZone"], - &mailBoundErr) + mi.AutomaticRepliesSetting.ScheduledStartDateTime.DateTime, err = toString(ctx, "dateTime", startDateTime) + mi.ErrGetMailBoxSetting = appendIfErr(mi.ErrGetMailBoxSetting, err) + + mi.AutomaticRepliesSetting.ScheduledStartDateTime.Timezone, err = toString(ctx, "timeZone", startDateTime) + mi.ErrGetMailBoxSetting = appendIfErr(mi.ErrGetMailBoxSetting, err) + + endDateTime, err := toT[map[string]any](ctx, "scheduledEndDateTime", replySetting) + mi.ErrGetMailBoxSetting = appendIfErr(mi.ErrGetMailBoxSetting, err) + + mi.AutomaticRepliesSetting.ScheduledEndDateTime.DateTime, err = toString(ctx, "dateTime", endDateTime) + mi.ErrGetMailBoxSetting = appendIfErr(mi.ErrGetMailBoxSetting, err) + + mi.AutomaticRepliesSetting.ScheduledEndDateTime.Timezone, err = toString(ctx, "timeZone", endDateTime) + mi.ErrGetMailBoxSetting = appendIfErr(mi.ErrGetMailBoxSetting, err) // Language decode - language := toMap(ctx, additionalData["language"], &mailBoundErr) - mailbox.Language.DisplayName = toString( - ctx, - language["displayName"], - &mailBoundErr) - mailbox.Language.Locale = toString(ctx, language["locale"], &mailBoundErr) + language, err := toT[map[string]any](ctx, "language", additionalData) + mi.ErrGetMailBoxSetting = appendIfErr(mi.ErrGetMailBoxSetting, err) + + mi.Language.DisplayName, err = toString(ctx, "displayName", language) + mi.ErrGetMailBoxSetting = appendIfErr(mi.ErrGetMailBoxSetting, err) + + mi.Language.Locale, err = toString(ctx, "locale", language) + mi.ErrGetMailBoxSetting = appendIfErr(mi.ErrGetMailBoxSetting, err) // working hours - workingHours := toMap(ctx, additionalData["workingHours"], &mailBoundErr) - mailbox.WorkingHours.StartTime = toString( - ctx, - workingHours["startTime"], - &mailBoundErr) - mailbox.WorkingHours.EndTime = toString( - ctx, - workingHours["endTime"], - &mailBoundErr) + workingHours, err := toT[map[string]any](ctx, "workingHours", additionalData) + mi.ErrGetMailBoxSetting = appendIfErr(mi.ErrGetMailBoxSetting, err) - timeZone := toMap(ctx, workingHours["timeZone"], &mailBoundErr) - mailbox.WorkingHours.TimeZone.Name = toString( - ctx, - timeZone["name"], - &mailBoundErr) + mi.WorkingHours.StartTime, err = toString(ctx, "startTime", workingHours) + mi.ErrGetMailBoxSetting = appendIfErr(mi.ErrGetMailBoxSetting, err) + + mi.WorkingHours.EndTime, err = toString(ctx, "endTime", workingHours) + mi.ErrGetMailBoxSetting = appendIfErr(mi.ErrGetMailBoxSetting, err) + + timeZone, err := toT[map[string]any](ctx, "timeZone", workingHours) + mi.ErrGetMailBoxSetting = appendIfErr(mi.ErrGetMailBoxSetting, err) + + mi.WorkingHours.TimeZone.Name, err = toString(ctx, "name", timeZone) + mi.ErrGetMailBoxSetting = appendIfErr(mi.ErrGetMailBoxSetting, err) + + days, err := toT[[]any](ctx, "daysOfWeek", workingHours) + mi.ErrGetMailBoxSetting = appendIfErr(mi.ErrGetMailBoxSetting, err) - days := toArray(ctx, workingHours["daysOfWeek"], &mailBoundErr) for _, day := range days { - mailbox.WorkingHours.DaysOfWeek = append(mailbox.WorkingHours.DaysOfWeek, - toString(ctx, day, &mailBoundErr)) + s, err := anyToString(ctx, "dayOfTheWeek", day) + mi.ErrGetMailBoxSetting = appendIfErr(mi.ErrGetMailBoxSetting, err) + mi.WorkingHours.DaysOfWeek = append(mi.WorkingHours.DaysOfWeek, s) } - if mailBoundErr.Core().Msg != "" { - mailbox.ErrGetMailBoxSetting = &mailBoundErr + return mi, nil +} + +func appendIfErr(errs []error, err error) []error { + if err == nil { + return errs } - return nil + return append(errs, err) } // --------------------------------------------------------------------------- @@ -453,50 +460,52 @@ func validateUser(item any) (models.Userable, error) { return m, nil } -func toString(ctx context.Context, data any, mailBoxErr *clues.Err) string { - dataPointer, ok := data.(*string) - if !ok { - logger.Ctx(ctx).Info("error getting data from mailboxSettings") +func toString(ctx context.Context, key string, data map[string]any) (string, error) { + ctx = clues.Add(ctx, "setting_name", key) - *mailBoxErr = *ErrMailBoxSettingsNotFound - - return "" + if len(data) == 0 { + logger.Ctx(ctx).Info("not found: ", key) + return "", ErrMailBoxSettingsNotFound } - value, ok := ptr.ValOK(dataPointer) - if !ok { - logger.Ctx(ctx).Info("error getting value from pointer for mailboxSettings") - - *mailBoxErr = *ErrMailBoxSettingsNotFound - - return "" - } - - return value + return anyToString(ctx, key, data[key]) } -func toMap(ctx context.Context, data any, mailBoxErr *clues.Err) map[string]interface{} { - value, ok := data.(map[string]interface{}) - if !ok { - logger.Ctx(ctx).Info("error getting mailboxSettings") - - *mailBoxErr = *clues.New("mailbox settings not found") - - return value +func anyToString(ctx context.Context, key string, val any) (string, error) { + if val == nil { + logger.Ctx(ctx).Info("nil value: ", key) + return "", ErrMailBoxSettingsNotFound } - return value -} - -func toArray(ctx context.Context, data any, mailBoxErr *clues.Err) []interface{} { - value, ok := data.([]interface{}) + sp, ok := val.(*string) if !ok { - logger.Ctx(ctx).Info("error getting mailboxSettings") - - *mailBoxErr = *clues.New("mailbox settings not found") - - return value + logger.Ctx(ctx).Info("value is not a *string: ", key) + return "", ErrMailBoxSettingsNotFound } - return value + return ptr.Val(sp), nil +} + +func toT[T any](ctx context.Context, key string, data map[string]any) (T, error) { + ctx = clues.Add(ctx, "setting_name", key) + + if len(data) == 0 { + logger.Ctx(ctx).Info("not found: ", key) + return *new(T), ErrMailBoxSettingsNotFound + } + + val := data[key] + + if data == nil { + logger.Ctx(ctx).Info("nil value: ", key) + return *new(T), ErrMailBoxSettingsNotFound + } + + value, ok := val.(T) + if !ok { + logger.Ctx(ctx).Info(fmt.Sprintf("unexpected type for %s: %T", key, val)) + return *new(T), ErrMailBoxSettingsNotFound + } + + return value, nil }