mattermost/server/channels/app/email/service.go
Jesse Hallam f6d5d9e1bc
Some checks are pending
API / build (push) Waiting to run
Server CI / Compute Go Version (push) Waiting to run
Server CI / Check mocks (push) Blocked by required conditions
Server CI / Check go mod tidy (push) Blocked by required conditions
Server CI / check-style (push) Blocked by required conditions
Server CI / Check serialization methods for hot structs (push) Blocked by required conditions
Server CI / Vet API (push) Blocked by required conditions
Server CI / Check migration files (push) Blocked by required conditions
Server CI / Generate email templates (push) Blocked by required conditions
Server CI / Check store layers (push) Blocked by required conditions
Server CI / Check mmctl docs (push) Blocked by required conditions
Server CI / Postgres with binary parameters (push) Blocked by required conditions
Server CI / Postgres (shard 0) (push) Blocked by required conditions
Server CI / Postgres (shard 1) (push) Blocked by required conditions
Server CI / Postgres (shard 2) (push) Blocked by required conditions
Server CI / Postgres (shard 3) (push) Blocked by required conditions
Server CI / Merge Postgres Test Results (push) Blocked by required conditions
Server CI / Postgres (FIPS) (push) Blocked by required conditions
Server CI / Generate Test Coverage (push) Blocked by required conditions
Server CI / Run mmctl tests (push) Blocked by required conditions
Server CI / Run mmctl tests (FIPS) (push) Blocked by required conditions
Server CI / Build mattermost server app (push) Blocked by required conditions
Tools CI / check-style (mattermost-govet) (push) Waiting to run
Tools CI / Test (mattermost-govet) (push) Waiting to run
Web App CI / check-lint (push) Waiting to run
Web App CI / check-i18n (push) Blocked by required conditions
Web App CI / check-external-links (push) Blocked by required conditions
Web App CI / check-types (push) Blocked by required conditions
Web App CI / test (platform) (push) Blocked by required conditions
Web App CI / test (mattermost-redux) (push) Blocked by required conditions
Web App CI / test (channels shard 1/4) (push) Blocked by required conditions
Web App CI / test (channels shard 2/4) (push) Blocked by required conditions
Web App CI / test (channels shard 3/4) (push) Blocked by required conditions
Web App CI / test (channels shard 4/4) (push) Blocked by required conditions
Web App CI / upload-coverage (push) Blocked by required conditions
Web App CI / build (push) Blocked by required conditions
[MM-67859] Update license renewal and expiry email branding (#35701)
2026-04-04 20:30:36 -03:00

201 lines
7.9 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package email
import (
"io"
"net/url"
"path"
"strings"
"github.com/pkg/errors"
"github.com/throttled/throttled/v2"
"github.com/throttled/throttled/v2/store/memstore"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/i18n"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/app/users"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/platform/shared/templates"
)
const (
emailRateLimitingMemstoreSize = 65536
emailRateLimitingPerMinute = 60
emailRateLimitingPerHour = 20
emailRateLimitingMaxBurst = 20
TokenTypePasswordRecovery = "password_recovery"
TokenTypeVerifyEmail = "verify_email"
TokenTypeTeamInvitation = "team_invitation"
TokenTypeGuestInvitation = "guest_invitation"
TokenTypeCWSAccess = "cws_access_token"
TokenTypeGuestMagicLinkInvitation = "guest_magic_link_invitation"
TokenTypeGuestMagicLink = "guest_magic_link"
)
func condenseSiteURL(siteURL string) string {
parsedSiteURL, _ := url.Parse(siteURL)
if parsedSiteURL.Path == "" || parsedSiteURL.Path == "/" {
return parsedSiteURL.Host
}
return path.Join(parsedSiteURL.Host, parsedSiteURL.Path)
}
type Service struct {
config func() *model.Config
license func() *model.License
userService *users.UserService
store store.Store
templatesContainer *templates.Container
perMinuteEmailRateLimiter *throttled.GCRARateLimiterCtx
perHourEmailRateLimiter *throttled.GCRARateLimiterCtx
EmailBatching *EmailBatchingJob
}
type ServiceConfig struct {
ConfigFn func() *model.Config
LicenseFn func() *model.License
TemplatesContainer *templates.Container
UserService *users.UserService
Store store.Store
}
func NewService(config ServiceConfig) (*Service, error) {
if err := config.validate(); err != nil {
return nil, err
}
service := &Service{
config: config.ConfigFn,
templatesContainer: config.TemplatesContainer,
license: config.LicenseFn,
store: config.Store,
userService: config.UserService,
}
if err := service.setUpRateLimiters(); err != nil {
return nil, err
}
service.InitEmailBatching()
return service, nil
}
func (es *Service) Stop() {
mlog.Info("Shutting down Email batching service...")
if es.EmailBatching != nil {
es.EmailBatching.Stop()
}
}
func (c *ServiceConfig) validate() error {
if c.ConfigFn == nil || c.Store == nil || c.LicenseFn == nil || c.TemplatesContainer == nil {
return errors.New("invalid service config")
}
return nil
}
func (es *Service) setUpRateLimiters() error {
store, err := memstore.NewCtx(emailRateLimitingMemstoreSize)
if err != nil {
return errors.Wrap(err, "Unable to setup email rate limiting memstore.")
}
perMinuteQuota := throttled.RateQuota{
MaxRate: throttled.PerMin(emailRateLimitingPerMinute),
MaxBurst: emailRateLimitingMaxBurst,
}
perHourQuota := throttled.RateQuota{
MaxRate: throttled.PerHour(emailRateLimitingPerHour),
MaxBurst: emailRateLimitingMaxBurst,
}
perMinuteRateLimiter, err := throttled.NewGCRARateLimiterCtx(store, perMinuteQuota)
if err != nil {
return errors.Wrap(err, "Unable to setup email rate limiting GCRA rate limiter.")
}
perHourRateLimiter, err := throttled.NewGCRARateLimiterCtx(store, perHourQuota)
if err != nil {
return errors.Wrap(err, "Unable to setup email rate limiting GCRA rate limiter.")
}
es.perMinuteEmailRateLimiter = perMinuteRateLimiter
es.perHourEmailRateLimiter = perHourRateLimiter
return nil
}
type ServiceInterface interface {
NewEmailTemplateData(locale string) templates.Data
SendEmailChangeVerifyEmail(newUserEmail, locale, siteURL, token string) error
SendEmailChangeEmail(oldEmail, newEmail, locale, siteURL string) error
SendVerifyEmail(userEmail, locale, siteURL, token, redirect string) error
SendSignInChangeEmail(email, method, locale, siteURL string) error
SendWelcomeEmail(userID string, email string, verified bool, disableWelcomeEmail bool, locale, siteURL, redirect string) error
SendCloudWelcomeEmail(userEmail, locale, teamInviteID, workSpaceName, dns, siteURL string) error
SendPasswordChangeEmail(email, method, locale, siteURL string) error
SendUserAccessTokenAddedEmail(email, locale, siteURL string) error
SendPasswordResetEmail(email string, token *model.Token, locale, siteURL string) (bool, error)
SendMfaChangeEmail(email string, activated bool, locale, siteURL string) error
SendInviteEmails(rctx request.CTX, team *model.Team, senderName string, senderUserId string, invites []string, siteURL string, reminderData *model.TeamInviteReminderData, errorWhenNotSent bool, isSystemAdmin bool, isFirstAdmin bool) error
SendGuestInviteEmails(rctx request.CTX, team *model.Team, channels []*model.Channel, senderName string, senderUserId string, senderProfileImage []byte, invites []string, siteURL string, message string, errorWhenNotSent bool, isSystemAdmin bool, isFirstAdmin bool, isGuestMagicLink bool) error
SendMagicLinkEmailSelfService(rctx request.CTX, invite string, siteURL string) error
SendInviteEmailsToTeamAndChannels(rctx request.CTX, team *model.Team, channels []*model.Channel, senderName string, senderUserId string, senderProfileImage []byte, invites []string, siteURL string, reminderData *model.TeamInviteReminderData, message string, errorWhenNotSent bool, isSystemAdmin bool, isFirstAdmin bool) ([]*model.EmailInviteWithError, error)
SendDeactivateAccountEmail(email string, locale, siteURL string) error
SendNotificationMail(to, subject, htmlBody string) error
SendMailWithEmbeddedFiles(to, subject, htmlBody string, embeddedFiles map[string]io.Reader, messageID string, inReplyTo string, references string, category string) error
SendLicenseUpForRenewalEmail(email, locale string, daysToExpiration int) error
SendRemoveExpiredLicenseEmail(email, locale string) error
AddNotificationEmailToBatch(user *model.User, post *model.Post, team *model.Team) *model.AppError
GetMessageForNotification(post *model.Post, teamName, siteUrl string, translateFunc i18n.TranslateFunc) string
GenerateHyperlinkForChannels(postMessage, teamName, teamURL string) (string, error)
InitEmailBatching()
SendChangeUsernameEmail(newUsername, email, locale, siteURL string) error
CreateVerifyEmailToken(userID string, newEmail string) (*model.Token, error)
SendIPFiltersChangedEmail(email string, userWhoChangedFilter *model.User, siteURL, portalURL, locale string, isWorkspaceOwner bool) error
SetStore(st store.Store)
Stop()
}
// getLicenseSkuName returns the license tier descriptor (e.g. "Professional",
// "Entry", "E20"), stripping the "Mattermost " prefix if the license server
// included it. Falls back to "Mattermost" when the SKU name is absent so
// that body text like "Your {{.SkuName}} license..." still reads naturally.
func (es *Service) getLicenseSkuName() string {
if license := es.license(); license != nil && license.SkuName != "" {
return strings.TrimPrefix(license.SkuName, "Mattermost ")
}
return "Mattermost"
}
// getPrefixedLicenseSkuName returns the full product name including "Mattermost"
// (e.g. "Mattermost Professional"), suitable for email subjects. Falls back
// to "Mattermost" when no license exists or the SKU name is empty.
func (es *Service) getPrefixedLicenseSkuName() string {
skuName := es.getLicenseSkuName()
if skuName == "Mattermost" {
return "Mattermost"
}
return "Mattermost " + skuName
}
func (es *Service) getConfigSiteName() string {
if siteName := *es.config().TeamSettings.SiteName; siteName != "" {
return siteName
}
return "Mattermost"
}
func (es *Service) Store() store.Store {
return es.store
}
func (es *Service) SetStore(st store.Store) {
es.store = st
}