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:
parent
dbf14644ad
commit
de1ae9f30a
@ -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,
|
||||
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user