MM-39058 - invite people to join team and channels (#19849)
* MM-39058-invite-to-team-from-add-channel * fix tests by validating the memberInvite is not nil * fix i18n texts * fix lint problem * fix translation lines * fix the data structure * modify api4-team_local file to match with the expected structure * fix unit tests, fix translation tests * remove go routine cause not necesary * add unit test for invite to team and channel * remove unnecessary validation * allow both data structures, simple string array and object with memberInvite struct * fix texts * fix linter * fix problems with graceful invites workflow * handle error while parsing body * fix unit tests * take the address just once * rename channels to channelIds * fix unit tests * add tests and fix local channels invite support Co-authored-by: Pablo Velez Vidal <pablo.velez@mattermost.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
0e4b0b6939
Коммит
4b354685e9
24
api4/team.go
24
api4/team.go
@@ -1275,7 +1275,18 @@ func inviteUsersToTeam(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
emailList := model.ArrayFromJSON(r.Body)
|
bf, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
c.Err = model.NewAppError("Api4.inviteUsersToTeams", "api.team.invite_members_to_team_and_channels.invalid_body.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
memberInvite := &model.MemberInvite{}
|
||||||
|
if jsonErr := json.Unmarshal(bf, memberInvite); jsonErr != nil {
|
||||||
|
c.Err = model.NewAppError("Api4.inviteUsersToTeams", "api.team.invite_members_to_team_and_channels.invalid_body_parsing.app_error", nil, jsonErr.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emailList := memberInvite.Emails
|
||||||
|
|
||||||
for i := range emailList {
|
for i := range emailList {
|
||||||
emailList[i] = strings.ToLower(emailList[i])
|
emailList[i] = strings.ToLower(emailList[i])
|
||||||
@@ -1292,11 +1303,16 @@ func inviteUsersToTeam(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
auditRec.AddMeta("count", len(emailList))
|
auditRec.AddMeta("count", len(emailList))
|
||||||
auditRec.AddMeta("emails", emailList)
|
auditRec.AddMeta("emails", emailList)
|
||||||
|
|
||||||
|
if len(memberInvite.ChannelIds) > 0 {
|
||||||
|
auditRec.AddMeta("channel_count", len(memberInvite.ChannelIds))
|
||||||
|
auditRec.AddMeta("channels", memberInvite.ChannelIds)
|
||||||
|
}
|
||||||
|
|
||||||
if graceful {
|
if graceful {
|
||||||
var invitesWithError []*model.EmailInviteWithError
|
var invitesWithError []*model.EmailInviteWithError
|
||||||
var err *model.AppError
|
var err *model.AppError
|
||||||
if emailList != nil {
|
if emailList != nil {
|
||||||
invitesWithError, err = c.App.InviteNewUsersToTeamGracefully(emailList, c.Params.TeamId, c.AppContext.Session().UserId, "")
|
invitesWithError, err = c.App.InviteNewUsersToTeamGracefully(memberInvite, c.Params.TeamId, c.AppContext.Session().UserId, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
if invitesWithError != nil {
|
if invitesWithError != nil {
|
||||||
@@ -1322,6 +1338,10 @@ func inviteUsersToTeam(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
"scheduledAt": strconv.FormatInt(scheduledAt, 10),
|
"scheduledAt": strconv.FormatInt(scheduledAt, 10),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(memberInvite.ChannelIds) > 0 {
|
||||||
|
jobData["channelList"] = model.ArrayToJSON(memberInvite.ChannelIds)
|
||||||
|
}
|
||||||
|
|
||||||
// we then manually schedule the job to send another invite after 48 hours
|
// we then manually schedule the job to send another invite after 48 hours
|
||||||
_, e := c.App.Srv().Jobs.CreateJob(model.JobTypeResendInvitationEmail, jobData)
|
_, e := c.App.Srv().Jobs.CreateJob(model.JobTypeResendInvitationEmail, jobData)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ package api4
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -76,7 +77,19 @@ func localInviteUsersToTeam(c *Context, w http.ResponseWriter, r *http.Request)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
emailList := model.ArrayFromJSON(r.Body)
|
bf, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
c.Err = model.NewAppError("Api4.inviteUsersToTeams", "api.team.invite_members_to_team_and_channels.invalid_body.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
memberInvite := &model.MemberInvite{}
|
||||||
|
if jsonErr := json.Unmarshal(bf, memberInvite); jsonErr != nil {
|
||||||
|
c.Err = model.NewAppError("Api4.inviteUsersToTeams", "api.team.invite_members_to_team_and_channels.invalid_body_parsing.app_error", nil, jsonErr.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emailList := memberInvite.Emails
|
||||||
|
|
||||||
if len(emailList) == 0 {
|
if len(emailList) == 0 {
|
||||||
c.SetInvalidParam("user_email")
|
c.SetInvalidParam("user_email")
|
||||||
return
|
return
|
||||||
@@ -96,6 +109,11 @@ func localInviteUsersToTeam(c *Context, w http.ResponseWriter, r *http.Request)
|
|||||||
auditRec.AddMeta("count", len(emailList))
|
auditRec.AddMeta("count", len(emailList))
|
||||||
auditRec.AddMeta("emails", emailList)
|
auditRec.AddMeta("emails", emailList)
|
||||||
|
|
||||||
|
if len(memberInvite.ChannelIds) > 0 {
|
||||||
|
auditRec.AddMeta("channel_count", len(memberInvite.ChannelIds))
|
||||||
|
auditRec.AddMeta("channels", memberInvite.ChannelIds)
|
||||||
|
}
|
||||||
|
|
||||||
team, nErr := c.App.Srv().Store.Team().Get(c.Params.TeamId)
|
team, nErr := c.App.Srv().Store.Team().Get(c.Params.TeamId)
|
||||||
if nErr != nil {
|
if nErr != nil {
|
||||||
var nfErr *store.ErrNotFound
|
var nfErr *store.ErrNotFound
|
||||||
@@ -110,6 +128,14 @@ func localInviteUsersToTeam(c *Context, w http.ResponseWriter, r *http.Request)
|
|||||||
|
|
||||||
allowedDomains := []string{team.AllowedDomains, *c.App.Config().TeamSettings.RestrictCreationToDomains}
|
allowedDomains := []string{team.AllowedDomains, *c.App.Config().TeamSettings.RestrictCreationToDomains}
|
||||||
|
|
||||||
|
var channels []*model.Channel
|
||||||
|
if len(memberInvite.ChannelIds) > 0 {
|
||||||
|
channels, err = c.App.Srv().Store.Channel().GetChannelsByIds(memberInvite.ChannelIds, false)
|
||||||
|
if err != nil {
|
||||||
|
c.Err = model.NewAppError("prepareLocalInviteNewUsersToTeam", "app.channel.get_channels_by_ids.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if r.URL.Query().Get("graceful") != "" {
|
if r.URL.Query().Get("graceful") != "" {
|
||||||
var invitesWithErrors []*model.EmailInviteWithError
|
var invitesWithErrors []*model.EmailInviteWithError
|
||||||
var goodEmails, errList []string
|
var goodEmails, errList []string
|
||||||
@@ -128,8 +154,16 @@ func localInviteUsersToTeam(c *Context, w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
auditRec.AddMeta("errors", errList)
|
auditRec.AddMeta("errors", errList)
|
||||||
if len(goodEmails) > 0 {
|
if len(goodEmails) > 0 {
|
||||||
err := c.App.Srv().EmailService.SendInviteEmails(team, "Administrator", "mmctl "+model.NewId(), goodEmails, *c.App.Config().ServiceSettings.SiteURL, nil, false)
|
var eErr error
|
||||||
if err != nil {
|
var invitesWithErrors2 []*model.EmailInviteWithError
|
||||||
|
if len(channels) > 0 {
|
||||||
|
invitesWithErrors2, eErr = c.App.Srv().EmailService.SendInviteEmailsToTeamAndChannels(team, channels, "Administrator", "mmctl "+model.NewId(), nil, goodEmails, *c.App.Config().ServiceSettings.SiteURL, nil, memberInvite.Message, true)
|
||||||
|
invitesWithErrors = append(invitesWithErrors, invitesWithErrors2...)
|
||||||
|
} else {
|
||||||
|
eErr = c.App.Srv().EmailService.SendInviteEmails(team, "Administrator", "mmctl "+model.NewId(), goodEmails, *c.App.Config().ServiceSettings.SiteURL, nil, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if eErr != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, email.NoRateLimiterError):
|
case errors.Is(err, email.NoRateLimiterError):
|
||||||
c.Err = model.NewAppError("SendInviteEmails", "app.email.no_rate_limiter.app_error", nil, fmt.Sprintf("team_id=%s", team.Id), http.StatusInternalServerError)
|
c.Err = model.NewAppError("SendInviteEmails", "app.email.no_rate_limiter.app_error", nil, fmt.Sprintf("team_id=%s", team.Id), http.StatusInternalServerError)
|
||||||
|
|||||||
@@ -2982,7 +2982,8 @@ func TestInviteUsersToTeam(t *testing.T) {
|
|||||||
user1 := th.GenerateTestEmail()
|
user1 := th.GenerateTestEmail()
|
||||||
user2 := th.GenerateTestEmail()
|
user2 := th.GenerateTestEmail()
|
||||||
|
|
||||||
emailList := []string{user1, user2}
|
memberInvite := &model.MemberInvite{Emails: []string{user1, user2}}
|
||||||
|
emailList := memberInvite.Emails
|
||||||
|
|
||||||
//Delete all the messages before check the sample email
|
//Delete all the messages before check the sample email
|
||||||
mail.DeleteMailBox(user1)
|
mail.DeleteMailBox(user1)
|
||||||
@@ -3018,7 +3019,7 @@ func TestInviteUsersToTeam(t *testing.T) {
|
|||||||
require.True(t, strings.ContainsAny(resultsMailbox[len(resultsMailbox)-1].To[0], email), "Wrong To recipient")
|
require.True(t, strings.ContainsAny(resultsMailbox[len(resultsMailbox)-1].To[0], email), "Wrong To recipient")
|
||||||
resultsEmail, err := mail.GetMessageFromMailbox(email, resultsMailbox[len(resultsMailbox)-1].ID)
|
resultsEmail, err := mail.GetMessageFromMailbox(email, resultsMailbox[len(resultsMailbox)-1].ID)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
require.Equalf(t, resultsEmail.Subject, expectedSubject, "Wrong Subject, actual: %s, expected: %s", resultsEmail.Subject, expectedSubject)
|
require.Equalf(t, resultsEmail.Subject, expectedSubject, "Wrong Subject, \nactual: %s, \nexpected: %s", resultsEmail.Subject, expectedSubject)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3034,6 +3035,18 @@ func TestInviteUsersToTeam(t *testing.T) {
|
|||||||
"SiteName": th.App.ClientConfig()["SiteName"]})
|
"SiteName": th.App.ClientConfig()["SiteName"]})
|
||||||
checkEmail(t, expectedSubject)
|
checkEmail(t, expectedSubject)
|
||||||
|
|
||||||
|
// Test the invite to team and channel
|
||||||
|
mail.DeleteMailBox(user1)
|
||||||
|
mail.DeleteMailBox(user2)
|
||||||
|
_, _, err = th.SystemAdminClient.InviteUsersToTeamAndChannelsGracefully(th.BasicTeam.Id, []string{user1, user2}, []string{th.BasicChannel.Id}, "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
expectedSubject = i18n.T("api.templates.invite_team_and_channel_subject",
|
||||||
|
map[string]interface{}{"SenderName": th.SystemAdminUser.GetDisplayName(nameFormat),
|
||||||
|
"TeamDisplayName": th.BasicTeam.DisplayName,
|
||||||
|
"ChannelName": th.BasicChannel.DisplayName,
|
||||||
|
"SiteName": th.App.ClientConfig()["SiteName"]})
|
||||||
|
checkEmail(t, expectedSubject)
|
||||||
|
|
||||||
mail.DeleteMailBox(user1)
|
mail.DeleteMailBox(user1)
|
||||||
mail.DeleteMailBox(user2)
|
mail.DeleteMailBox(user2)
|
||||||
_, err = th.LocalClient.InviteUsersToTeam(th.BasicTeam.Id, emailList)
|
_, err = th.LocalClient.InviteUsersToTeam(th.BasicTeam.Id, emailList)
|
||||||
@@ -3044,6 +3057,18 @@ func TestInviteUsersToTeam(t *testing.T) {
|
|||||||
"SiteName": th.App.ClientConfig()["SiteName"]})
|
"SiteName": th.App.ClientConfig()["SiteName"]})
|
||||||
checkEmail(t, expectedSubject)
|
checkEmail(t, expectedSubject)
|
||||||
|
|
||||||
|
// Test the invite local to team and channel
|
||||||
|
mail.DeleteMailBox(user1)
|
||||||
|
mail.DeleteMailBox(user2)
|
||||||
|
_, _, err = th.LocalClient.InviteUsersToTeamAndChannelsGracefully(th.BasicTeam.Id, []string{user1, user2}, []string{th.BasicChannel.Id}, "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
expectedSubject = i18n.T("api.templates.invite_team_and_channel_subject",
|
||||||
|
map[string]interface{}{"SenderName": "Administrator",
|
||||||
|
"TeamDisplayName": th.BasicTeam.DisplayName,
|
||||||
|
"ChannelName": th.BasicChannel.DisplayName,
|
||||||
|
"SiteName": th.App.ClientConfig()["SiteName"]})
|
||||||
|
checkEmail(t, expectedSubject)
|
||||||
|
|
||||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.RestrictCreationToDomains = "@global.com,@common.com" })
|
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.RestrictCreationToDomains = "@global.com,@common.com" })
|
||||||
|
|
||||||
th.TestForAllClients(t, func(t *testing.T, client *model.Client4) {
|
th.TestForAllClients(t, func(t *testing.T, client *model.Client4) {
|
||||||
|
|||||||
@@ -817,7 +817,7 @@ type AppIface interface {
|
|||||||
InviteGuestsToChannels(teamID string, guestsInvite *model.GuestsInvite, senderId string) *model.AppError
|
InviteGuestsToChannels(teamID string, guestsInvite *model.GuestsInvite, senderId string) *model.AppError
|
||||||
InviteGuestsToChannelsGracefully(teamID string, guestsInvite *model.GuestsInvite, senderId string) ([]*model.EmailInviteWithError, *model.AppError)
|
InviteGuestsToChannelsGracefully(teamID string, guestsInvite *model.GuestsInvite, senderId string) ([]*model.EmailInviteWithError, *model.AppError)
|
||||||
InviteNewUsersToTeam(emailList []string, teamID, senderId string) *model.AppError
|
InviteNewUsersToTeam(emailList []string, teamID, senderId string) *model.AppError
|
||||||
InviteNewUsersToTeamGracefully(emailList []string, teamID, senderId string, reminderInterval string) ([]*model.EmailInviteWithError, *model.AppError)
|
InviteNewUsersToTeamGracefully(memberInvite *model.MemberInvite, teamID, senderId string, reminderInterval string) ([]*model.EmailInviteWithError, *model.AppError)
|
||||||
IsCRTEnabledForUser(userID string) bool
|
IsCRTEnabledForUser(userID string) bool
|
||||||
IsFirstUserAccount() bool
|
IsFirstUserAccount() bool
|
||||||
IsLeader() bool
|
IsLeader() bool
|
||||||
|
|||||||
@@ -606,6 +606,153 @@ func (es *Service) SendGuestInviteEmails(team *model.Team, channels []*model.Cha
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (es *Service) SendInviteEmailsToTeamAndChannels(
|
||||||
|
team *model.Team,
|
||||||
|
channels []*model.Channel,
|
||||||
|
senderName string,
|
||||||
|
senderUserId string,
|
||||||
|
senderProfileImage []byte,
|
||||||
|
invites []string,
|
||||||
|
siteURL string,
|
||||||
|
reminderData *model.TeamInviteReminderData,
|
||||||
|
message string,
|
||||||
|
errorWhenNotSent bool,
|
||||||
|
) ([]*model.EmailInviteWithError, error) {
|
||||||
|
if es.perHourEmailRateLimiter == nil {
|
||||||
|
return nil, NoRateLimiterError
|
||||||
|
}
|
||||||
|
rateLimited, result, err := es.perHourEmailRateLimiter.RateLimit(senderUserId, len(invites))
|
||||||
|
if err != nil {
|
||||||
|
return nil, SetupRateLimiterError
|
||||||
|
}
|
||||||
|
|
||||||
|
if rateLimited {
|
||||||
|
mlog.Error("rate limit exceeded", mlog.Duration("RetryAfter", result.RetryAfter), mlog.Duration("ResetAfter", result.ResetAfter), mlog.String("user_id", senderUserId),
|
||||||
|
mlog.String("team_id", team.Id), mlog.String("retry_after_secs", fmt.Sprintf("%f", result.RetryAfter.Seconds())), mlog.String("reset_after_secs", fmt.Sprintf("%f", result.ResetAfter.Seconds())))
|
||||||
|
return nil, RateLimitExceededError
|
||||||
|
}
|
||||||
|
|
||||||
|
channelsLen := len(channels)
|
||||||
|
|
||||||
|
subject := i18n.T("api.templates.invite_team_and_channels_subject", map[string]interface{}{
|
||||||
|
"SenderName": senderName,
|
||||||
|
"TeamDisplayName": team.DisplayName,
|
||||||
|
"ChannelsLen": channelsLen,
|
||||||
|
"SiteName": es.config().TeamSettings.SiteName})
|
||||||
|
|
||||||
|
title := i18n.T("api.templates.invite_team_and_channels_body.title", map[string]interface{}{
|
||||||
|
"SenderName": senderName,
|
||||||
|
"ChannelsLen": channelsLen,
|
||||||
|
"TeamDisplayName": team.DisplayName})
|
||||||
|
|
||||||
|
if channelsLen == 1 {
|
||||||
|
channelName := channels[0].DisplayName
|
||||||
|
|
||||||
|
subject = i18n.T("api.templates.invite_team_and_channel_subject",
|
||||||
|
map[string]interface{}{"SenderName": senderName,
|
||||||
|
"TeamDisplayName": team.DisplayName,
|
||||||
|
"ChannelName": channelName,
|
||||||
|
"SiteName": es.config().TeamSettings.SiteName},
|
||||||
|
)
|
||||||
|
|
||||||
|
title = i18n.T("api.templates.invite_team_and_channel_body.title", map[string]interface{}{
|
||||||
|
"SenderName": senderName,
|
||||||
|
"ChannelName": channelName,
|
||||||
|
"TeamDisplayName": team.DisplayName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var invitesWithErrors []*model.EmailInviteWithError
|
||||||
|
for _, invite := range invites {
|
||||||
|
if invite == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
channelIDs := []string{}
|
||||||
|
for _, channel := range channels {
|
||||||
|
channelIDs = append(channelIDs, channel.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := es.NewEmailTemplateData("")
|
||||||
|
data.Props["SiteURL"] = siteURL
|
||||||
|
data.Props["SubTitle"] = i18n.T("api.templates.invite_body.subTitle")
|
||||||
|
data.Props["Button"] = i18n.T("api.templates.invite_body.button")
|
||||||
|
data.Props["SenderName"] = senderName
|
||||||
|
data.Props["InviteFooterTitle"] = i18n.T("api.templates.invite_body_footer.title")
|
||||||
|
data.Props["InviteFooterInfo"] = i18n.T("api.templates.invite_body_footer.info")
|
||||||
|
data.Props["InviteFooterLearnMore"] = i18n.T("api.templates.invite_body_footer.learn_more")
|
||||||
|
|
||||||
|
if message != "" {
|
||||||
|
message = bluemonday.NewPolicy().Sanitize(message)
|
||||||
|
}
|
||||||
|
data.Props["Message"] = message
|
||||||
|
|
||||||
|
token := model.NewToken(
|
||||||
|
TokenTypeTeamInvitation,
|
||||||
|
model.MapToJSON(map[string]string{
|
||||||
|
"teamId": team.Id,
|
||||||
|
"email": invite,
|
||||||
|
"channels": strings.Join(channelIDs, " "),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
tokenProps := make(map[string]string)
|
||||||
|
tokenProps["email"] = invite
|
||||||
|
tokenProps["display_name"] = team.DisplayName
|
||||||
|
tokenProps["name"] = team.Name
|
||||||
|
|
||||||
|
if reminderData != nil {
|
||||||
|
reminder := i18n.T("api.templates.invite_body.title.reminder")
|
||||||
|
title = fmt.Sprintf("%s: %s", reminder, title)
|
||||||
|
tokenProps["reminder_interval"] = reminderData.Interval
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Props["Title"] = title
|
||||||
|
|
||||||
|
tokenData := model.MapToJSON(tokenProps)
|
||||||
|
|
||||||
|
if err := es.store.Token().Save(token); err != nil {
|
||||||
|
mlog.Error("Failed to send invite email successfully ", mlog.Err(err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
data.Props["ButtonURL"] = fmt.Sprintf("%s/signup_user_complete/?d=%s&t=%s", siteURL, url.QueryEscape(tokenData), url.QueryEscape(token.Token))
|
||||||
|
|
||||||
|
senderPhoto := ""
|
||||||
|
embeddedFiles := make(map[string]io.Reader)
|
||||||
|
if message != "" {
|
||||||
|
if senderProfileImage != nil {
|
||||||
|
senderPhoto = "user-avatar.png"
|
||||||
|
embeddedFiles = map[string]io.Reader{
|
||||||
|
senderPhoto: bytes.NewReader(senderProfileImage),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pData := postData{
|
||||||
|
SenderName: senderName,
|
||||||
|
Message: template.HTML(message),
|
||||||
|
SenderPhoto: senderPhoto,
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Props["Posts"] = []postData{pData}
|
||||||
|
|
||||||
|
body, err := es.templatesContainer.RenderToString("invite_body", data)
|
||||||
|
if err != nil {
|
||||||
|
mlog.Error("Failed to send invite email successfully ", mlog.Err(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if nErr := es.SendMailWithEmbeddedFiles(invite, subject, body, embeddedFiles); nErr != nil {
|
||||||
|
mlog.Error("Failed to send invite email successfully", mlog.Err(nErr))
|
||||||
|
if errorWhenNotSent {
|
||||||
|
inviteWithError := &model.EmailInviteWithError{
|
||||||
|
Email: invite,
|
||||||
|
Error: &model.AppError{Message: nErr.Error()},
|
||||||
|
}
|
||||||
|
invitesWithErrors = append(invitesWithErrors, inviteWithError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return invitesWithErrors, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (es *Service) NewEmailTemplateData(locale string) templates.Data {
|
func (es *Service) NewEmailTemplateData(locale string) templates.Data {
|
||||||
var localT i18n.TranslateFunc
|
var localT i18n.TranslateFunc
|
||||||
if locale != "" {
|
if locale != "" {
|
||||||
|
|||||||
@@ -237,6 +237,29 @@ func (_m *ServiceInterface) SendInviteEmails(team *model.Team, senderName string
|
|||||||
return r0
|
return r0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendInviteEmailsToTeamAndChannels provides a mock function with given fields: team, channels, senderName, senderUserId, senderProfileImage, invites, siteURL, reminderData, message, errorWhenNotSent
|
||||||
|
func (_m *ServiceInterface) SendInviteEmailsToTeamAndChannels(team *model.Team, channels []*model.Channel, senderName string, senderUserId string, senderProfileImage []byte, invites []string, siteURL string, reminderData *model.TeamInviteReminderData, message string, errorWhenNotSent bool) ([]*model.EmailInviteWithError, error) {
|
||||||
|
ret := _m.Called(team, channels, senderName, senderUserId, senderProfileImage, invites, siteURL, reminderData, message, errorWhenNotSent)
|
||||||
|
|
||||||
|
var r0 []*model.EmailInviteWithError
|
||||||
|
if rf, ok := ret.Get(0).(func(*model.Team, []*model.Channel, string, string, []byte, []string, string, *model.TeamInviteReminderData, string, bool) []*model.EmailInviteWithError); ok {
|
||||||
|
r0 = rf(team, channels, senderName, senderUserId, senderProfileImage, invites, siteURL, reminderData, message, errorWhenNotSent)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]*model.EmailInviteWithError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(*model.Team, []*model.Channel, string, string, []byte, []string, string, *model.TeamInviteReminderData, string, bool) error); ok {
|
||||||
|
r1 = rf(team, channels, senderName, senderUserId, senderProfileImage, invites, siteURL, reminderData, message, errorWhenNotSent)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
// SendLicenseInactivityEmail provides a mock function with given fields: _a0, name, locale, siteURL
|
// SendLicenseInactivityEmail provides a mock function with given fields: _a0, name, locale, siteURL
|
||||||
func (_m *ServiceInterface) SendLicenseInactivityEmail(_a0 string, name string, locale string, siteURL string) error {
|
func (_m *ServiceInterface) SendLicenseInactivityEmail(_a0 string, name string, locale string, siteURL string) error {
|
||||||
ret := _m.Called(_a0, name, locale, siteURL)
|
ret := _m.Called(_a0, name, locale, siteURL)
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ type ServiceInterface interface {
|
|||||||
SendMfaChangeEmail(email string, activated bool, locale, siteURL string) error
|
SendMfaChangeEmail(email string, activated bool, locale, siteURL string) error
|
||||||
SendInviteEmails(team *model.Team, senderName string, senderUserId string, invites []string, siteURL string, reminderData *model.TeamInviteReminderData, errorWhenNotSent bool) error
|
SendInviteEmails(team *model.Team, senderName string, senderUserId string, invites []string, siteURL string, reminderData *model.TeamInviteReminderData, errorWhenNotSent bool) error
|
||||||
SendGuestInviteEmails(team *model.Team, channels []*model.Channel, senderName string, senderUserId string, senderProfileImage []byte, invites []string, siteURL string, message string, errorWhenNotSent bool) error
|
SendGuestInviteEmails(team *model.Team, channels []*model.Channel, senderName string, senderUserId string, senderProfileImage []byte, invites []string, siteURL string, message string, errorWhenNotSent bool) error
|
||||||
|
SendInviteEmailsToTeamAndChannels(team *model.Team, channels []*model.Channel, senderName string, senderUserId string, senderProfileImage []byte, invites []string, siteURL string, reminderData *model.TeamInviteReminderData, message string, errorWhenNotSent bool) ([]*model.EmailInviteWithError, error)
|
||||||
SendDeactivateAccountEmail(email string, locale, siteURL string) error
|
SendDeactivateAccountEmail(email string, locale, siteURL string) error
|
||||||
SendNotificationMail(to, subject, htmlBody string) error
|
SendNotificationMail(to, subject, htmlBody string) error
|
||||||
SendMailWithEmbeddedFiles(to, subject, htmlBody string, embeddedFiles map[string]io.Reader) error
|
SendMailWithEmbeddedFiles(to, subject, htmlBody string, embeddedFiles map[string]io.Reader) error
|
||||||
|
|||||||
@@ -25,16 +25,17 @@ func TestSendInviteEmailRateLimits(t *testing.T) {
|
|||||||
*cfg.ServiceSettings.EnableEmailInvitations = true
|
*cfg.ServiceSettings.EnableEmailInvitations = true
|
||||||
})
|
})
|
||||||
|
|
||||||
emailList := make([]string, 22)
|
memberInvite := &model.MemberInvite{}
|
||||||
|
memberInvite.Emails = make([]string, 22)
|
||||||
for i := 0; i < 22; i++ {
|
for i := 0; i < 22; i++ {
|
||||||
emailList[i] = "test-" + strconv.Itoa(i) + "@common.com"
|
memberInvite.Emails[i] = "test-" + strconv.Itoa(i) + "@common.com"
|
||||||
}
|
}
|
||||||
err = th.App.InviteNewUsersToTeam(emailList, th.BasicTeam.Id, th.BasicUser.Id)
|
err = th.App.InviteNewUsersToTeam(memberInvite.Emails, th.BasicTeam.Id, th.BasicUser.Id)
|
||||||
require.NotNil(t, err)
|
require.NotNil(t, err)
|
||||||
assert.Equal(t, "app.email.rate_limit_exceeded.app_error", err.Id)
|
assert.Equal(t, "app.email.rate_limit_exceeded.app_error", err.Id)
|
||||||
assert.Equal(t, http.StatusRequestEntityTooLarge, err.StatusCode)
|
assert.Equal(t, http.StatusRequestEntityTooLarge, err.StatusCode)
|
||||||
|
|
||||||
_, err = th.App.InviteNewUsersToTeamGracefully(emailList, th.BasicTeam.Id, th.BasicUser.Id, "")
|
_, err = th.App.InviteNewUsersToTeamGracefully(memberInvite, th.BasicTeam.Id, th.BasicUser.Id, "")
|
||||||
require.NotNil(t, err)
|
require.NotNil(t, err)
|
||||||
assert.Equal(t, "app.email.rate_limit_exceeded.app_error", err.Id)
|
assert.Equal(t, "app.email.rate_limit_exceeded.app_error", err.Id)
|
||||||
assert.Equal(t, http.StatusRequestEntityTooLarge, err.StatusCode)
|
assert.Equal(t, http.StatusRequestEntityTooLarge, err.StatusCode)
|
||||||
|
|||||||
@@ -10966,7 +10966,7 @@ func (a *OpenTracingAppLayer) InviteNewUsersToTeam(emailList []string, teamID st
|
|||||||
return resultVar0
|
return resultVar0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *OpenTracingAppLayer) InviteNewUsersToTeamGracefully(emailList []string, teamID string, senderId string, reminderInterval string) ([]*model.EmailInviteWithError, *model.AppError) {
|
func (a *OpenTracingAppLayer) InviteNewUsersToTeamGracefully(memberInvite *model.MemberInvite, teamID string, senderId string, reminderInterval string) ([]*model.EmailInviteWithError, *model.AppError) {
|
||||||
origCtx := a.ctx
|
origCtx := a.ctx
|
||||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.InviteNewUsersToTeamGracefully")
|
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.InviteNewUsersToTeamGracefully")
|
||||||
|
|
||||||
@@ -10978,7 +10978,7 @@ func (a *OpenTracingAppLayer) InviteNewUsersToTeamGracefully(emailList []string,
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
defer span.Finish()
|
defer span.Finish()
|
||||||
resultVar0, resultVar1 := a.app.InviteNewUsersToTeamGracefully(emailList, teamID, senderId, reminderInterval)
|
resultVar0, resultVar1 := a.app.InviteNewUsersToTeamGracefully(memberInvite, teamID, senderId, reminderInterval)
|
||||||
|
|
||||||
if resultVar1 != nil {
|
if resultVar1 != nil {
|
||||||
span.LogFields(spanlog.Error(resultVar1))
|
span.LogFields(spanlog.Error(resultVar1))
|
||||||
|
|||||||
58
app/team.go
58
app/team.go
@@ -1207,7 +1207,7 @@ func (a *App) postRemoveFromTeamMessage(c *request.Context, user *model.User, ch
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) prepareInviteNewUsersToTeam(teamID, senderId string) (*model.User, *model.Team, *model.AppError) {
|
func (a *App) prepareInviteNewUsersToTeam(teamID, senderId string, channelIds []string) (*model.User, *model.Team, []*model.Channel, *model.AppError) {
|
||||||
tchan := make(chan store.StoreResult, 1)
|
tchan := make(chan store.StoreResult, 1)
|
||||||
go func() {
|
go func() {
|
||||||
team, err := a.Srv().Store.Team().Get(teamID)
|
team, err := a.Srv().Store.Team().Get(teamID)
|
||||||
@@ -1222,14 +1222,22 @@ func (a *App) prepareInviteNewUsersToTeam(teamID, senderId string) (*model.User,
|
|||||||
close(uchan)
|
close(uchan)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
var channels []*model.Channel
|
||||||
|
var err error
|
||||||
|
if len(channelIds) > 0 {
|
||||||
|
channels, err = a.Srv().Store.Channel().GetChannelsByIds(channelIds, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, model.NewAppError("prepareInviteNewUsersToTeam", "app.channel.get_channels_by_ids.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
result := <-tchan
|
result := <-tchan
|
||||||
if result.NErr != nil {
|
if result.NErr != nil {
|
||||||
var nfErr *store.ErrNotFound
|
var nfErr *store.ErrNotFound
|
||||||
switch {
|
switch {
|
||||||
case errors.As(result.NErr, &nfErr):
|
case errors.As(result.NErr, &nfErr):
|
||||||
return nil, nil, model.NewAppError("prepareInviteNewUsersToTeam", "app.team.get_by_invite_id.finding.app_error", nil, nfErr.Error(), http.StatusNotFound)
|
return nil, nil, nil, model.NewAppError("prepareInviteNewUsersToTeam", "app.team.get_by_invite_id.finding.app_error", nil, nfErr.Error(), http.StatusNotFound)
|
||||||
default:
|
default:
|
||||||
return nil, nil, model.NewAppError("prepareInviteNewUsersToTeam", "app.team.get_by_invite_id.finding.app_error", nil, result.NErr.Error(), http.StatusInternalServerError)
|
return nil, nil, nil, model.NewAppError("prepareInviteNewUsersToTeam", "app.team.get_by_invite_id.finding.app_error", nil, result.NErr.Error(), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
team := result.Data.(*model.Team)
|
team := result.Data.(*model.Team)
|
||||||
@@ -1239,25 +1247,34 @@ func (a *App) prepareInviteNewUsersToTeam(teamID, senderId string) (*model.User,
|
|||||||
var nfErr *store.ErrNotFound
|
var nfErr *store.ErrNotFound
|
||||||
switch {
|
switch {
|
||||||
case errors.As(result.NErr, &nfErr):
|
case errors.As(result.NErr, &nfErr):
|
||||||
return nil, nil, model.NewAppError("prepareInviteNewUsersToTeam", MissingAccountError, nil, nfErr.Error(), http.StatusNotFound)
|
return nil, nil, nil, model.NewAppError("prepareInviteNewUsersToTeam", MissingAccountError, nil, nfErr.Error(), http.StatusNotFound)
|
||||||
default:
|
default:
|
||||||
return nil, nil, model.NewAppError("prepareInviteNewUsersToTeam", "app.user.get.app_error", nil, result.NErr.Error(), http.StatusInternalServerError)
|
return nil, nil, nil, model.NewAppError("prepareInviteNewUsersToTeam", "app.user.get.app_error", nil, result.NErr.Error(), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
user := result.Data.(*model.User)
|
user := result.Data.(*model.User)
|
||||||
return user, team, nil
|
|
||||||
|
for _, channel := range channels {
|
||||||
|
if channel.TeamId != teamID {
|
||||||
|
return nil, nil, nil, model.NewAppError("prepareInviteGuestsToChannels", "api.team.invite_guests.channel_in_invalid_team.app_error", nil, "", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, team, channels, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) InviteNewUsersToTeamGracefully(emailList []string, teamID, senderId string, reminderInterval string) ([]*model.EmailInviteWithError, *model.AppError) {
|
func (a *App) InviteNewUsersToTeamGracefully(memberInvite *model.MemberInvite, teamID, senderId string, reminderInterval string) ([]*model.EmailInviteWithError, *model.AppError) {
|
||||||
if !*a.Config().ServiceSettings.EnableEmailInvitations {
|
if !*a.Config().ServiceSettings.EnableEmailInvitations {
|
||||||
return nil, model.NewAppError("InviteNewUsersToTeam", "api.team.invite_members.disabled.app_error", nil, "", http.StatusNotImplemented)
|
return nil, model.NewAppError("InviteNewUsersToTeam", "api.team.invite_members.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emailList := memberInvite.Emails
|
||||||
|
|
||||||
if len(emailList) == 0 {
|
if len(emailList) == 0 {
|
||||||
err := model.NewAppError("InviteNewUsersToTeam", "api.team.invite_members.no_one.app_error", nil, "", http.StatusBadRequest)
|
err := model.NewAppError("InviteNewUsersToTeam", "api.team.invite_members.no_one.app_error", nil, "", http.StatusBadRequest)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
user, team, err := a.prepareInviteNewUsersToTeam(teamID, senderId)
|
user, team, channels, err := a.prepareInviteNewUsersToTeam(teamID, senderId, memberInvite.ChannelIds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1284,25 +1301,36 @@ func (a *App) InviteNewUsersToTeamGracefully(emailList []string, teamID, senderI
|
|||||||
|
|
||||||
if len(goodEmails) > 0 {
|
if len(goodEmails) > 0 {
|
||||||
nameFormat := *a.Config().TeamSettings.TeammateNameDisplay
|
nameFormat := *a.Config().TeamSettings.TeammateNameDisplay
|
||||||
eErr := a.Srv().EmailService.SendInviteEmails(team, user.GetDisplayName(nameFormat), user.Id, goodEmails, a.GetSiteURL(), reminderData, true)
|
senderProfileImage, _, err := a.GetProfileImage(user)
|
||||||
|
if err != nil {
|
||||||
|
a.Log().Warn("Unable to get the sender user profile image.", mlog.String("user_id", user.Id), mlog.String("team_id", team.Id), mlog.Err(err))
|
||||||
|
}
|
||||||
|
var eErr error
|
||||||
|
var invitesWithErrors2 []*model.EmailInviteWithError
|
||||||
|
if len(channels) > 0 {
|
||||||
|
invitesWithErrors2, eErr = a.Srv().EmailService.SendInviteEmailsToTeamAndChannels(team, channels, user.GetDisplayName(nameFormat), user.Id, senderProfileImage, goodEmails, a.GetSiteURL(), reminderData, memberInvite.Message, true)
|
||||||
|
inviteListWithErrors = append(inviteListWithErrors, invitesWithErrors2...)
|
||||||
|
} else {
|
||||||
|
eErr = a.Srv().EmailService.SendInviteEmails(team, user.GetDisplayName(nameFormat), user.Id, goodEmails, a.GetSiteURL(), reminderData, true)
|
||||||
|
}
|
||||||
if eErr != nil {
|
if eErr != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(eErr, email.SendMailError):
|
case errors.Is(eErr, email.SendMailError):
|
||||||
for i := range inviteListWithErrors {
|
for i := range inviteListWithErrors {
|
||||||
if inviteListWithErrors[i].Error == nil {
|
if inviteListWithErrors[i].Error == nil {
|
||||||
if *a.Config().EmailSettings.SMTPServer == model.EmailSMTPDefaultServer && *a.Config().EmailSettings.SMTPPort == model.EmailSMTPDefaultPort {
|
if *a.Config().EmailSettings.SMTPServer == model.EmailSMTPDefaultServer && *a.Config().EmailSettings.SMTPPort == model.EmailSMTPDefaultPort {
|
||||||
inviteListWithErrors[i].Error = model.NewAppError("InviteGuestsToChannelsGracefully", "api.team.invite_members.unable_to_send_email_with_defaults.app_error", nil, "", http.StatusInternalServerError)
|
inviteListWithErrors[i].Error = model.NewAppError("InviteNewUsersToTeamGracefully", "api.team.invite_members.unable_to_send_email_with_defaults.app_error", nil, "", http.StatusInternalServerError)
|
||||||
} else {
|
} else {
|
||||||
inviteListWithErrors[i].Error = model.NewAppError("SendInviteEmails", "api.team.invite_members.unable_to_send_email.app_error", nil, "", http.StatusInternalServerError)
|
inviteListWithErrors[i].Error = model.NewAppError("InviteNewUsersToTeamGracefully", "api.team.invite_members.unable_to_send_email.app_error", nil, "", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case errors.Is(eErr, email.NoRateLimiterError):
|
case errors.Is(eErr, email.NoRateLimiterError):
|
||||||
return nil, model.NewAppError("SendInviteEmails", "app.email.no_rate_limiter.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s", user.Id, team.Id), http.StatusInternalServerError)
|
return nil, model.NewAppError("InviteNewUsersToTeamGracefully", "app.email.no_rate_limiter.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s", user.Id, team.Id), http.StatusInternalServerError)
|
||||||
case errors.Is(eErr, email.SetupRateLimiterError):
|
case errors.Is(eErr, email.SetupRateLimiterError):
|
||||||
return nil, model.NewAppError("SendInviteEmails", "app.email.setup_rate_limiter.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s, error=%v", user.Id, team.Id, eErr), http.StatusInternalServerError)
|
return nil, model.NewAppError("InviteNewUsersToTeamGracefully", "app.email.setup_rate_limiter.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s, error=%v", user.Id, team.Id, eErr), http.StatusInternalServerError)
|
||||||
default:
|
default:
|
||||||
return nil, model.NewAppError("SendInviteEmails", "app.email.rate_limit_exceeded.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s, error=%v", user.Id, team.Id, eErr), http.StatusRequestEntityTooLarge)
|
return nil, model.NewAppError("InviteNewUsersToTeamGracefully", "app.email.rate_limit_exceeded.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s, error=%v", user.Id, team.Id, eErr), http.StatusRequestEntityTooLarge)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1440,7 +1468,7 @@ func (a *App) InviteNewUsersToTeam(emailList []string, teamID, senderId string)
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
user, team, err := a.prepareInviteNewUsersToTeam(teamID, senderId)
|
user, team, _, err := a.prepareInviteNewUsersToTeam(teamID, senderId, []string{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1255,6 +1255,79 @@ func TestInviteNewUsersToTeamGracefully(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("it return list of email with no error on success", func(t *testing.T) {
|
t.Run("it return list of email with no error on success", func(t *testing.T) {
|
||||||
emailServiceMock := emailmocks.ServiceInterface{}
|
emailServiceMock := emailmocks.ServiceInterface{}
|
||||||
|
memberInvite := &model.MemberInvite{
|
||||||
|
Emails: []string{"idontexist@mattermost.com"},
|
||||||
|
}
|
||||||
|
emailServiceMock.On("SendInviteEmails",
|
||||||
|
mock.AnythingOfType("*model.Team"),
|
||||||
|
mock.AnythingOfType("string"),
|
||||||
|
mock.AnythingOfType("string"),
|
||||||
|
memberInvite.Emails,
|
||||||
|
"",
|
||||||
|
mock.Anything,
|
||||||
|
true,
|
||||||
|
).Once().Return(nil)
|
||||||
|
th.App.Srv().EmailService = &emailServiceMock
|
||||||
|
|
||||||
|
res, err := th.App.InviteNewUsersToTeamGracefully(memberInvite, th.BasicTeam.Id, th.BasicUser.Id, "")
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Len(t, res, 1)
|
||||||
|
require.Nil(t, res[0].Error)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("it should assign errors to emails when failing to send", func(t *testing.T) {
|
||||||
|
emailServiceMock := emailmocks.ServiceInterface{}
|
||||||
|
memberInvite := &model.MemberInvite{
|
||||||
|
Emails: []string{"idontexist@mattermost.com"},
|
||||||
|
}
|
||||||
|
emailServiceMock.On("SendInviteEmails",
|
||||||
|
mock.AnythingOfType("*model.Team"),
|
||||||
|
mock.AnythingOfType("string"),
|
||||||
|
mock.AnythingOfType("string"),
|
||||||
|
memberInvite.Emails,
|
||||||
|
"",
|
||||||
|
mock.Anything,
|
||||||
|
true,
|
||||||
|
).Once().Return(email.SendMailError)
|
||||||
|
th.App.Srv().EmailService = &emailServiceMock
|
||||||
|
|
||||||
|
res, err := th.App.InviteNewUsersToTeamGracefully(memberInvite, th.BasicTeam.Id, th.BasicUser.Id, "")
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Len(t, res, 1)
|
||||||
|
require.NotNil(t, res[0].Error)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("it return list of email with no error when inviting to team and channels using memberInvite struct", func(t *testing.T) {
|
||||||
|
emailServiceMock := emailmocks.ServiceInterface{}
|
||||||
|
memberInvite := &model.MemberInvite{
|
||||||
|
Emails: []string{"idontexist@mattermost.com"},
|
||||||
|
ChannelIds: []string{th.BasicChannel.Id},
|
||||||
|
}
|
||||||
|
emailServiceMock.On("SendInviteEmailsToTeamAndChannels",
|
||||||
|
mock.AnythingOfType("*model.Team"),
|
||||||
|
mock.AnythingOfType("[]*model.Channel"),
|
||||||
|
mock.AnythingOfType("string"),
|
||||||
|
mock.AnythingOfType("string"),
|
||||||
|
mock.AnythingOfType("[]uint8"),
|
||||||
|
memberInvite.Emails,
|
||||||
|
"",
|
||||||
|
mock.Anything,
|
||||||
|
mock.AnythingOfType("string"),
|
||||||
|
true,
|
||||||
|
).Once().Return([]*model.EmailInviteWithError{}, nil)
|
||||||
|
th.App.Srv().EmailService = &emailServiceMock
|
||||||
|
|
||||||
|
res, err := th.App.InviteNewUsersToTeamGracefully(memberInvite, th.BasicTeam.Id, th.BasicUser.Id, "")
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Len(t, res, 1)
|
||||||
|
require.Nil(t, res[0].Error)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("it return list of email with no error when inviting to team and channels using plain emails array", func(t *testing.T) {
|
||||||
|
emailServiceMock := emailmocks.ServiceInterface{}
|
||||||
|
memberInvite := &model.MemberInvite{
|
||||||
|
Emails: []string{"idontexist@mattermost.com"},
|
||||||
|
}
|
||||||
emailServiceMock.On("SendInviteEmails",
|
emailServiceMock.On("SendInviteEmails",
|
||||||
mock.AnythingOfType("*model.Team"),
|
mock.AnythingOfType("*model.Team"),
|
||||||
mock.AnythingOfType("string"),
|
mock.AnythingOfType("string"),
|
||||||
@@ -1266,30 +1339,11 @@ func TestInviteNewUsersToTeamGracefully(t *testing.T) {
|
|||||||
).Once().Return(nil)
|
).Once().Return(nil)
|
||||||
th.App.Srv().EmailService = &emailServiceMock
|
th.App.Srv().EmailService = &emailServiceMock
|
||||||
|
|
||||||
res, err := th.App.InviteNewUsersToTeamGracefully([]string{"idontexist@mattermost.com"}, th.BasicTeam.Id, th.BasicUser.Id, "")
|
res, err := th.App.InviteNewUsersToTeamGracefully(memberInvite, th.BasicTeam.Id, th.BasicUser.Id, "")
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
require.Len(t, res, 1)
|
require.Len(t, res, 1)
|
||||||
require.Nil(t, res[0].Error)
|
require.Nil(t, res[0].Error)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("it should assign errors to emails when failing to send", func(t *testing.T) {
|
|
||||||
emailServiceMock := emailmocks.ServiceInterface{}
|
|
||||||
emailServiceMock.On("SendInviteEmails",
|
|
||||||
mock.AnythingOfType("*model.Team"),
|
|
||||||
mock.AnythingOfType("string"),
|
|
||||||
mock.AnythingOfType("string"),
|
|
||||||
[]string{"idontexist@mattermost.com"},
|
|
||||||
"",
|
|
||||||
mock.Anything,
|
|
||||||
true,
|
|
||||||
).Once().Return(email.SendMailError)
|
|
||||||
th.App.Srv().EmailService = &emailServiceMock
|
|
||||||
|
|
||||||
res, err := th.App.InviteNewUsersToTeamGracefully([]string{"idontexist@mattermost.com"}, th.BasicTeam.Id, th.BasicUser.Id, "")
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.Len(t, res, 1)
|
|
||||||
require.NotNil(t, res[0].Error)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInviteGuestsToChannelsGracefully(t *testing.T) {
|
func TestInviteGuestsToChannelsGracefully(t *testing.T) {
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ func (a *App) CreateUserWithToken(c *request.Context, user *model.User, token *m
|
|||||||
|
|
||||||
a.AddDirectChannels(team.Id, ruser)
|
a.AddDirectChannels(team.Id, ruser)
|
||||||
|
|
||||||
if token.Type == TokenTypeGuestInvitation {
|
if token.Type == TokenTypeGuestInvitation || (token.Type == TokenTypeTeamInvitation && len(channels) > 0) {
|
||||||
for _, channel := range channels {
|
for _, channel := range channels {
|
||||||
_, err := a.AddChannelMember(c, ruser.Id, channel, ChannelMemberOpts{})
|
_, err := a.AddChannelMember(c, ruser.Id, channel, ChannelMemberOpts{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
32
i18n/en.json
32
i18n/en.json
@@ -2971,6 +2971,14 @@
|
|||||||
"id": "api.team.invite_members.unable_to_send_email_with_defaults.app_error",
|
"id": "api.team.invite_members.unable_to_send_email_with_defaults.app_error",
|
||||||
"translation": "SMTP is not configured in System Console"
|
"translation": "SMTP is not configured in System Console"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "api.team.invite_members_to_team_and_channels.invalid_body.app_error",
|
||||||
|
"translation": "Invalid request body."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.team.invite_members_to_team_and_channels.invalid_body_parsing.app_error",
|
||||||
|
"translation": "Error while parsing the body data."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "api.team.is_team_creation_allowed.disabled.app_error",
|
"id": "api.team.is_team_creation_allowed.disabled.app_error",
|
||||||
"translation": "Team creation has been disabled. Please ask your System Administrator for details."
|
"translation": "Team creation has been disabled. Please ask your System Administrator for details."
|
||||||
@@ -3311,6 +3319,22 @@
|
|||||||
"id": "api.templates.invite_subject",
|
"id": "api.templates.invite_subject",
|
||||||
"translation": "[{{ .SiteName }}] {{ .SenderName }} invited you to join {{ .TeamDisplayName }} Team"
|
"translation": "[{{ .SiteName }}] {{ .SenderName }} invited you to join {{ .TeamDisplayName }} Team"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "api.templates.invite_team_and_channel_body.title",
|
||||||
|
"translation": "{{ .SenderName }} invited you to join {{ .ChannelName }} on the {{ .TeamDisplayName }} Team"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.templates.invite_team_and_channel_subject",
|
||||||
|
"translation": "[{{ .SiteName }}] {{ .SenderName }} invited you to join {{ .ChannelName }} on the {{ .TeamDisplayName }} Team"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.templates.invite_team_and_channels_body.title",
|
||||||
|
"translation": "{{ .SenderName }} invited you to join {{ .ChannelsLen }} channels on the {{ .TeamDisplayName }} Team"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.templates.invite_team_and_channels_subject",
|
||||||
|
"translation": "[{{ .SiteName }}] {{ .SenderName }} invited you to join {{ .ChannelsLen }} channels on the {{ .TeamDisplayName }} Team"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "api.templates.license_up_for_renewal_renew_now",
|
"id": "api.templates.license_up_for_renewal_renew_now",
|
||||||
"translation": "Renew now"
|
"translation": "Renew now"
|
||||||
@@ -8471,6 +8495,14 @@
|
|||||||
"id": "model.link_metadata.is_valid.url.app_error",
|
"id": "model.link_metadata.is_valid.url.app_error",
|
||||||
"translation": "Link metadata URL must be set."
|
"translation": "Link metadata URL must be set."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "model.member.is_valid.channel.app_error",
|
||||||
|
"translation": "Channel name is not valid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "model.member.is_valid.emails.app_error",
|
||||||
|
"translation": "Email list is empty"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "model.oauth.is_valid.app_id.app_error",
|
"id": "model.oauth.is_valid.app_id.app_error",
|
||||||
"translation": "Invalid app id."
|
"translation": "Invalid app id."
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ type AppIface interface {
|
|||||||
configservice.ConfigService
|
configservice.ConfigService
|
||||||
GetUserByEmail(email string) (*model.User, *model.AppError)
|
GetUserByEmail(email string) (*model.User, *model.AppError)
|
||||||
GetTeamMembersByIds(teamID string, userIDs []string, restrictions *model.ViewUsersRestrictions) ([]*model.TeamMember, *model.AppError)
|
GetTeamMembersByIds(teamID string, userIDs []string, restrictions *model.ViewUsersRestrictions) ([]*model.TeamMember, *model.AppError)
|
||||||
InviteNewUsersToTeamGracefully(emailList []string, teamID, senderId string, reminderInterval string) ([]*model.EmailInviteWithError, *model.AppError)
|
InviteNewUsersToTeamGracefully(memberInvite *model.MemberInvite, teamID, senderId string, reminderInterval string) ([]*model.EmailInviteWithError, *model.AppError)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResendInvitationEmailWorker struct {
|
type ResendInvitationEmailWorker struct {
|
||||||
@@ -116,6 +116,17 @@ func (rseworker *ResendInvitationEmailWorker) cleanEmailData(emailStringData str
|
|||||||
return emails, nil
|
return emails, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (rseworker *ResendInvitationEmailWorker) cleanChannelsData(channelStringData string) ([]string, error) {
|
||||||
|
// channelStringData looks like this ["uuuiiiiidddd","uuuiiiiidddd"]
|
||||||
|
channels := []string{}
|
||||||
|
err := json.Unmarshal([]byte(channelStringData), &channels)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return channels, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (rseworker *ResendInvitationEmailWorker) removeAlreadyJoined(teamID string, emailList []string) []string {
|
func (rseworker *ResendInvitationEmailWorker) removeAlreadyJoined(teamID string, emailList []string) []string {
|
||||||
var notJoinedYet []string
|
var notJoinedYet []string
|
||||||
for _, email := range emailList {
|
for _, email := range emailList {
|
||||||
@@ -161,6 +172,7 @@ func (rseworker *ResendInvitationEmailWorker) TearDown(job *model.Job) {
|
|||||||
func (rseworker *ResendInvitationEmailWorker) ResendEmails(job *model.Job, interval string) {
|
func (rseworker *ResendInvitationEmailWorker) ResendEmails(job *model.Job, interval string) {
|
||||||
teamID := job.Data["teamID"]
|
teamID := job.Data["teamID"]
|
||||||
emailListData := job.Data["emailList"]
|
emailListData := job.Data["emailList"]
|
||||||
|
channelListData := job.Data["channelList"]
|
||||||
|
|
||||||
emailList, err := rseworker.cleanEmailData(emailListData)
|
emailList, err := rseworker.cleanEmailData(emailListData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -169,9 +181,24 @@ func (rseworker *ResendInvitationEmailWorker) ResendEmails(job *model.Job, inter
|
|||||||
rseworker.setJobError(job, appErr)
|
rseworker.setJobError(job, appErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
channelList, err := rseworker.cleanChannelsData(channelListData)
|
||||||
|
if err != nil {
|
||||||
|
appErr := model.NewAppError("worker: "+rseworker.name, "job_id: "+job.Id, nil, err.Error(), http.StatusInternalServerError)
|
||||||
|
mlog.Error("Worker: Failed to clean channel string data", mlog.String("worker", rseworker.name), mlog.String("job_id", job.Id), mlog.String("error", appErr.Error()))
|
||||||
|
rseworker.setJobError(job, appErr)
|
||||||
|
}
|
||||||
|
|
||||||
emailList = rseworker.removeAlreadyJoined(teamID, emailList)
|
emailList = rseworker.removeAlreadyJoined(teamID, emailList)
|
||||||
|
|
||||||
_, appErr := rseworker.app.InviteNewUsersToTeamGracefully(emailList, teamID, job.Data["senderID"], interval)
|
memberInvite := model.MemberInvite{
|
||||||
|
Emails: emailList,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(channelList) > 0 {
|
||||||
|
memberInvite.ChannelIds = channelList
|
||||||
|
}
|
||||||
|
|
||||||
|
_, appErr := rseworker.app.InviteNewUsersToTeamGracefully(&memberInvite, teamID, job.Data["senderID"], interval)
|
||||||
if appErr != nil {
|
if appErr != nil {
|
||||||
mlog.Error("Worker: Failed to send emails", mlog.String("worker", rseworker.name), mlog.String("job_id", job.Id), mlog.String("error", appErr.Error()))
|
mlog.Error("Worker: Failed to send emails", mlog.String("worker", rseworker.name), mlog.String("job_id", job.Id), mlog.String("error", appErr.Error()))
|
||||||
rseworker.setJobError(job, appErr)
|
rseworker.setJobError(job, appErr)
|
||||||
|
|||||||
@@ -2638,6 +2638,30 @@ func (c *Client4) InviteGuestsToTeam(teamId string, userEmails []string, channel
|
|||||||
// InviteUsersToTeam invite users by email to the team.
|
// InviteUsersToTeam invite users by email to the team.
|
||||||
func (c *Client4) InviteUsersToTeamGracefully(teamId string, userEmails []string) ([]*EmailInviteWithError, *Response, error) {
|
func (c *Client4) InviteUsersToTeamGracefully(teamId string, userEmails []string) ([]*EmailInviteWithError, *Response, error) {
|
||||||
r, err := c.DoAPIPost(c.teamRoute(teamId)+"/invite/email?graceful="+c.boolString(true), ArrayToJSON(userEmails))
|
r, err := c.DoAPIPost(c.teamRoute(teamId)+"/invite/email?graceful="+c.boolString(true), ArrayToJSON(userEmails))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, BuildResponse(r), err
|
||||||
|
}
|
||||||
|
defer closeBody(r)
|
||||||
|
var list []*EmailInviteWithError
|
||||||
|
if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil {
|
||||||
|
return nil, nil, NewAppError("InviteUsersToTeamGracefully", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
return list, BuildResponse(r), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InviteUsersToTeam invite users by email to the team.
|
||||||
|
func (c *Client4) InviteUsersToTeamAndChannelsGracefully(teamId string, userEmails []string, channelIds []string, message string) ([]*EmailInviteWithError, *Response, error) {
|
||||||
|
memberInvite := MemberInvite{
|
||||||
|
Emails: userEmails,
|
||||||
|
ChannelIds: channelIds,
|
||||||
|
Message: message,
|
||||||
|
}
|
||||||
|
buf, err := json.Marshal(memberInvite)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, NewAppError("InviteMembersToTeamAndChannels", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
r, err := c.DoAPIPostBytes(c.teamRoute(teamId)+"/invite/email?graceful="+c.boolString(true), buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, BuildResponse(r), err
|
return nil, BuildResponse(r), err
|
||||||
}
|
}
|
||||||
|
|||||||
49
model/member_invite.go
Обычный файл
49
model/member_invite.go
Обычный файл
@@ -0,0 +1,49 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MemberInvite struct {
|
||||||
|
Emails []string `json:"emails"`
|
||||||
|
ChannelIds []string `json:"channelIds,omitempty"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValid validates that the invitation info is loaded correctly and with the correct structure
|
||||||
|
func (i *MemberInvite) IsValid() *AppError {
|
||||||
|
if len(i.Emails) == 0 {
|
||||||
|
return NewAppError("MemberInvite.IsValid", "model.member.is_valid.emails.app_error", nil, "", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(i.ChannelIds) > 0 {
|
||||||
|
for _, channel := range i.ChannelIds {
|
||||||
|
if len(channel) != 26 {
|
||||||
|
return NewAppError("MemberInvite.IsValid", "model.member.is_valid.channel.app_error", nil, "channel="+channel, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *MemberInvite) UnmarshalJSON(b []byte) error {
|
||||||
|
var emails []string
|
||||||
|
if err := json.Unmarshal(b, &emails); err == nil {
|
||||||
|
*i = MemberInvite{}
|
||||||
|
i.Emails = emails
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type TempMemberInvite MemberInvite
|
||||||
|
var o2 TempMemberInvite
|
||||||
|
if err := json.Unmarshal(b, &o2); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*i = MemberInvite(o2)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Ссылка в новой задаче
Block a user