Shared mailbox (#3106)

<!-- PR description-->

Handle user's API to provide user purpose of users

#### Does this PR need a docs update or release note?

- [ ]  Yes, it's included
- [x] 🕐 Yes, but in a later PR
- [ ]  No

#### Type of change

<!--- Please check the type of change your PR introduces: --->
- [x] 🌻 Feature
- [ ] 🐛 Bugfix
- [ ] 🗺️ Documentation
- [ ] 🤖 Supportability/Tests
- [ ] 💻 CI/Deployment
- [ ] 🧹 Tech Debt/Cleanup

#### Issue(s)

<!-- Can reference multiple issues. Use one of the following "magic words" - "closes, fixes" to auto-close the Github issue. -->
* #<issue>

#### Test Plan

<!-- How will this be tested prior to merging.-->
- [x] 💪 Manual
- [x]  Unit test
- [x] 💚 E2E
This commit is contained in:
neha_gupta 2023-04-20 23:31:12 +05:30 committed by GitHub
parent dbf14644ad
commit de1ae9f30a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 374 additions and 5 deletions

View File

@ -69,6 +69,22 @@ func Users(
return users, nil
}
// UserDetails fetches detailed info like - userPurpose for all users in the tenant.
func GetUserInfo(
ctx context.Context,
acct account.Account,
userID string,
errs *fault.Bus,
) (*api.UserInfo, error) {
client, err := apiClient(ctx, acct)
if err != nil {
return nil, err
}
return client.Users().GetInfo(ctx, userID)
}
// User fetches a single user's data.
func User(
ctx context.Context,
gwi getWithInfoer,

View File

@ -197,14 +197,23 @@ func (suite *DiscoveryIntegrationSuite) TestUserInfo() {
path.ExchangeService: {},
path.OneDriveService: {},
},
HasMailBox: true,
HasOneDrive: true,
Mailbox: api.MailboxInfo{
Purpose: "user",
ErrGetMailBoxSetting: nil,
},
},
},
{
name: "user does not exist",
user: uuid.NewString(),
expect: &api.UserInfo{
DiscoveredServices: map[path.ServiceType]struct{}{
path.OneDriveService: {}, // currently statically populated
DiscoveredServices: map[path.ServiceType]struct{}{},
HasMailBox: false,
HasOneDrive: false,
Mailbox: api.MailboxInfo{
ErrGetMailBoxSetting: api.ErrMailBoxSettingsNotFound,
},
},
},
@ -218,7 +227,66 @@ func (suite *DiscoveryIntegrationSuite) TestUserInfo() {
result, err := discovery.UserInfo(ctx, uapi, test.user)
require.NoError(t, err, clues.ToCore(err))
assert.Equal(t, test.expect, result)
assert.Equal(t, test.expect.HasMailBox, result.HasMailBox)
assert.Equal(t, test.expect.HasOneDrive, result.HasOneDrive)
assert.Equal(t, test.expect.DiscoveredServices, result.DiscoveredServices)
})
}
}
func (suite *DiscoveryIntegrationSuite) TestUserWithoutDrive() {
t := suite.T()
acct := tester.NewM365Account(t)
userID := tester.M365UserID(t)
table := []struct {
name string
user string
expect *api.UserInfo
}{
{
name: "user without drive and exchange",
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,
},
},
},
{
name: "user with drive and exchange",
user: userID,
expect: &api.UserInfo{
DiscoveredServices: map[path.ServiceType]struct{}{
path.ExchangeService: {},
path.OneDriveService: {},
},
HasOneDrive: true,
HasMailBox: true,
Mailbox: api.MailboxInfo{
Purpose: "user",
ErrGetMailBoxSetting: nil,
},
},
},
}
for _, test := range table {
suite.Run(test.name, func() {
ctx, flush := tester.NewContext()
defer flush()
t := suite.T()
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)
})
}
}

View File

@ -38,6 +38,7 @@ const (
errCodeResourceNotFound = "ResourceNotFound"
errCodeRequestResourceNotFound = "Request_ResourceNotFound"
errCodeMailboxNotEnabledForRESTAPI = "MailboxNotEnabledForRESTAPI"
errCodeErrorAccessDenied = "ErrorAccessDenied"
)
const (
@ -106,6 +107,10 @@ func IsErrUserNotFound(err error) bool {
return hasErrorCode(err, errCodeRequestResourceNotFound)
}
func IsErrAccessDenied(err error) bool {
return hasErrorCode(err, errCodeErrorAccessDenied)
}
func IsErrTimeout(err error) bool {
switch err := err.(type) {
case *url.Error:

View File

@ -13,9 +13,15 @@ import (
"github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/logger"
"github.com/alcionai/corso/src/pkg/path"
)
// Variables
var (
ErrMailBoxSettingsNotFound = clues.New("mailbox settings not found")
)
// ---------------------------------------------------------------------------
// controller
// ---------------------------------------------------------------------------
@ -35,6 +41,50 @@ type Users struct {
type UserInfo struct {
DiscoveredServices map[path.ServiceType]struct{}
HasMailBox bool
HasOneDrive bool
Mailbox MailboxInfo
}
type MailboxInfo struct {
Purpose string
ArchiveFolder string
DateFormat string
TimeFormat string
DelegateMeetMsgDeliveryOpt string
Timezone string
AutomaticRepliesSetting AutomaticRepliesSettings
Language Language
WorkingHours WorkingHours
ErrGetMailBoxSetting error
}
type AutomaticRepliesSettings struct {
ExternalAudience string
ExternalReplyMessage string
InternalReplyMessage string
ScheduledEndDateTime timeInfo
ScheduledStartDateTime timeInfo
Status string
}
type timeInfo struct {
DateTime string
Timezone string
}
type Language struct {
Locale string
DisplayName string
}
type WorkingHours struct {
DaysOfWeek []string
StartTime string
EndTime string
TimeZone struct {
Name string
}
}
func newUserInfo() *UserInfo {
@ -193,19 +243,192 @@ func (c Users) GetInfo(ctx context.Context, userID string) (*UserInfo, error) {
}
)
// TODO: OneDrive
_, err = c.stable.Client().UsersById(userID).MailFolders().Get(ctx, &options)
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
}
userInfo.HasOneDrive = true
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")
}
logger.Ctx(ctx).Infof("resource owner does not have a drive")
delete(userInfo.DiscoveredServices, path.OneDriveService)
userInfo.HasOneDrive = false
}
err = c.getAdditionalData(ctx, userID, &userInfo.Mailbox)
if err != nil {
return nil, err
}
return userInfo, nil
}
// verify mailbox enabled for user
func (c Users) GetExchange(
ctx context.Context,
userID string,
options users.ItemMailFoldersRequestBuilderGetRequestConfiguration,
) error {
_, err := c.stable.Client().UsersById(userID).MailFolders().Get(ctx, &options)
if err != nil {
return err
}
return 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)
if err != nil {
return err
}
return nil
}
func (c Users) getAdditionalData(ctx context.Context, userID string, mailbox *MailboxInfo) error {
var (
rawURL = fmt.Sprintf("https://graph.microsoft.com/v1.0/users/%s/mailboxSettings", userID)
adapter = c.stable.Adapter()
mailBoundErr clues.Err
)
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")
}
if graph.IsErrAccessDenied(err) {
logger.Ctx(ctx).Info("err getting additional data: access denied")
mailbox.ErrGetMailBoxSetting = clues.New("access denied")
return nil
}
if graph.IsErrExchangeMailFolderNotFound(err) {
logger.Ctx(ctx).Info("err exchange mail folder not found")
mailbox.ErrGetMailBoxSetting = ErrMailBoxSettingsNotFound
return 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)
// 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)
// 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)
endDateTime := toMap(ctx, replySetting["scheduledEndDateTime"], &mailBoundErr)
mailbox.AutomaticRepliesSetting.ScheduledEndDateTime.DateTime = toString(
ctx,
endDateTime["dateTime"],
&mailBoundErr)
mailbox.AutomaticRepliesSetting.ScheduledEndDateTime.Timezone = toString(
ctx,
endDateTime["timeZone"],
&mailBoundErr)
// Language decode
language := toMap(ctx, additionalData["language"], &mailBoundErr)
mailbox.Language.DisplayName = toString(
ctx,
language["displayName"],
&mailBoundErr)
mailbox.Language.Locale = toString(ctx, language["locale"], &mailBoundErr)
// working hours
workingHours := toMap(ctx, additionalData["workingHours"], &mailBoundErr)
mailbox.WorkingHours.StartTime = toString(
ctx,
workingHours["startTime"],
&mailBoundErr)
mailbox.WorkingHours.EndTime = toString(
ctx,
workingHours["endTime"],
&mailBoundErr)
timeZone := toMap(ctx, workingHours["timeZone"], &mailBoundErr)
mailbox.WorkingHours.TimeZone.Name = toString(
ctx,
timeZone["name"],
&mailBoundErr)
days := toArray(ctx, workingHours["daysOfWeek"], &mailBoundErr)
for _, day := range days {
mailbox.WorkingHours.DaysOfWeek = append(mailbox.WorkingHours.DaysOfWeek,
toString(ctx, day, &mailBoundErr))
}
if mailBoundErr.Core().Msg != "" {
mailbox.ErrGetMailBoxSetting = &mailBoundErr
}
return nil
}
// ---------------------------------------------------------------------------
// helpers
// ---------------------------------------------------------------------------
@ -229,3 +452,51 @@ 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")
*mailBoxErr = *ErrMailBoxSettingsNotFound
return ""
}
value, ok := ptr.ValOK(dataPointer)
if !ok {
logger.Ctx(ctx).Info("error getting value from pointer for mailboxSettings")
*mailBoxErr = *ErrMailBoxSettingsNotFound
return ""
}
return value
}
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
}
return value
}
func toArray(ctx context.Context, data any, mailBoxErr *clues.Err) []interface{} {
value, ok := data.([]interface{})
if !ok {
logger.Ctx(ctx).Info("error getting mailboxSettings")
*mailBoxErr = *clues.New("mailbox settings not found")
return value
}
return value
}

View File

@ -32,6 +32,7 @@ type User struct {
PrincipalName string
ID string
Name string
Info api.UserInfo
}
type UserInfo struct {
@ -72,6 +73,13 @@ func Users(ctx context.Context, acct account.Account, errs *fault.Bus) ([]*User,
return nil, clues.Wrap(err, "formatting user data")
}
userInfo, err := discovery.GetUserInfo(ctx, acct, pu.ID, errs)
if err != nil {
return nil, clues.Wrap(err, "getting user details")
}
pu.Info = *userInfo
ret = append(ret, pu)
}

View File

@ -46,6 +46,7 @@ func (suite *M365IntegrationSuite) TestUsers() {
assert.NotEmpty(t, u.ID)
assert.NotEmpty(t, u.PrincipalName)
assert.NotEmpty(t, u.Name)
assert.NotEmpty(t, u.Info)
})
}
}