feat: add http as smtp provider (#8545)

# Which Problems Are Solved

Send Email messages as a HTTP call to a relay, for own logic on handling
different Email providers

# How the Problems Are Solved

Create endpoints under Email provider to manage SMTP and HTTP in the
notification handlers.

# Additional Changes

Clean up old logic in command and query side to handle the general Email
providers with deactivate, activate and remove.

# Additional Context

Partially closes #8270

---------

Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
Stefan Benz
2024-09-12 06:27:29 +02:00
committed by GitHub
parent d8a71d217c
commit 21c38b061d
28 changed files with 3575 additions and 1152 deletions

View File

@@ -5,8 +5,8 @@ import (
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/notification/channels/email"
"github.com/zitadel/zitadel/internal/notification/channels/sms"
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
"github.com/zitadel/zitadel/internal/notification/channels/webhook"
"github.com/zitadel/zitadel/internal/notification/handlers"
"github.com/zitadel/zitadel/internal/notification/senders"
@@ -62,20 +62,20 @@ func registerCounter(counter, desc string) {
logging.WithFields("metric", counter).OnError(err).Panic("unable to register counter")
}
func (c *channels) Email(ctx context.Context) (*senders.Chain, *smtp.Config, error) {
smtpCfg, err := c.q.GetSMTPConfig(ctx)
func (c *channels) Email(ctx context.Context) (*senders.Chain, *email.Config, error) {
emailCfg, err := c.q.GetActiveEmailConfig(ctx)
if err != nil {
return nil, nil, err
}
chain, err := senders.EmailChannels(
ctx,
smtpCfg,
emailCfg,
c.q.GetFileSystemProvider,
c.q.GetLogProvider,
c.counters.success.email,
c.counters.failed.email,
)
return chain, smtpCfg, err
return chain, emailCfg, err
}
func (c *channels) SMS(ctx context.Context) (*senders.Chain, *sms.Config, error) {

View File

@@ -0,0 +1,17 @@
package email
import (
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
"github.com/zitadel/zitadel/internal/notification/channels/webhook"
)
type Config struct {
ProviderConfig *Provider
SMTPConfig *smtp.Config
WebhookConfig *webhook.Config
}
type Provider struct {
ID string `json:"id,omitempty"`
Description string `json:"description,omitempty"`
}

View File

@@ -1,7 +1,6 @@
package smtp
type Config struct {
Description string
SMTP SMTP
Tls bool
From string
@@ -18,3 +17,7 @@ type SMTP struct {
func (smtp *SMTP) HasAuth() bool {
return smtp.User != "" && smtp.Password != ""
}
type ConfigHTTP struct {
Endpoint string
}

View File

@@ -0,0 +1,56 @@
package handlers
import (
"context"
"net/http"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/notification/channels/email"
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
"github.com/zitadel/zitadel/internal/notification/channels/webhook"
"github.com/zitadel/zitadel/internal/zerrors"
)
// GetSMTPConfig reads the iam SMTP provider config
func (n *NotificationQueries) GetActiveEmailConfig(ctx context.Context) (*email.Config, error) {
config, err := n.SMTPConfigActive(ctx, authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
provider := &email.Provider{
ID: config.ID,
Description: config.Description,
}
if config.SMTPConfig != nil {
password, err := crypto.DecryptString(config.SMTPConfig.Password, n.SMTPPasswordCrypto)
if err != nil {
return nil, err
}
return &email.Config{
ProviderConfig: provider,
SMTPConfig: &smtp.Config{
From: config.SMTPConfig.SenderAddress,
FromName: config.SMTPConfig.SenderName,
ReplyToAddress: config.SMTPConfig.ReplyToAddress,
Tls: config.SMTPConfig.TLS,
SMTP: smtp.SMTP{
Host: config.SMTPConfig.Host,
User: config.SMTPConfig.User,
Password: password,
},
},
}, nil
}
if config.HTTPConfig != nil {
return &email.Config{
ProviderConfig: provider,
WebhookConfig: &webhook.Config{
CallURL: config.HTTPConfig.Endpoint,
Method: http.MethodPost,
Headers: nil,
},
}, nil
}
return nil, zerrors.ThrowNotFound(err, "QUERY-KPQleOckOV", "Errors.SMTPConfig.NotFound")
}

View File

@@ -1,33 +0,0 @@
package handlers
import (
"context"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
)
// GetSMTPConfig reads the iam SMTP provider config
func (n *NotificationQueries) GetSMTPConfig(ctx context.Context) (*smtp.Config, error) {
config, err := n.SMTPConfigActive(ctx, authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
password, err := crypto.DecryptString(config.Password, n.SMTPPasswordCrypto)
if err != nil {
return nil, err
}
return &smtp.Config{
Description: config.Description,
From: config.SenderAddress,
FromName: config.SenderName,
ReplyToAddress: config.ReplyToAddress,
Tls: config.TLS,
SMTP: smtp.SMTP{
Host: config.Host,
User: config.User,
Password: password,
},
}, nil
}

View File

@@ -15,6 +15,7 @@ import (
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/repository"
es_repo_mock "github.com/zitadel/zitadel/internal/eventstore/repository/mock"
"github.com/zitadel/zitadel/internal/notification/channels/email"
channel_mock "github.com/zitadel/zitadel/internal/notification/channels/mock"
"github.com/zitadel/zitadel/internal/notification/channels/sms"
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
@@ -1449,7 +1450,27 @@ func newUserNotifier(t *testing.T, ctrl *gomock.Controller, queries *mock.MockQu
f.SMSTokenCrypto,
),
otpEmailTmpl: defaultOTPEmailTemplate,
channels: &channels{Chain: *senders.ChainChannels(channel)},
channels: &channels{
Chain: *senders.ChainChannels(channel),
EmailConfig: &email.Config{
ProviderConfig: &email.Provider{
ID: "ID",
Description: "Description",
},
SMTPConfig: &smtp.Config{
SMTP: smtp.SMTP{
Host: "host",
User: "user",
Password: "password",
},
Tls: true,
From: "from",
FromName: "fromName",
ReplyToAddress: "replyToAddress",
},
WebhookConfig: nil,
},
},
}
}
@@ -1457,10 +1478,11 @@ var _ types.ChannelChains = (*channels)(nil)
type channels struct {
senders.Chain
EmailConfig *email.Config
}
func (c *channels) Email(context.Context) (*senders.Chain, *smtp.Config, error) {
return &c.Chain, nil, nil
func (c *channels) Email(context.Context) (*senders.Chain, *email.Config, error) {
return &c.Chain, c.EmailConfig, nil
}
func (c *channels) SMS(context.Context) (*senders.Chain, *sms.Config, error) {

View File

@@ -7,38 +7,61 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/notification/channels"
"github.com/zitadel/zitadel/internal/notification/channels/email"
"github.com/zitadel/zitadel/internal/notification/channels/fs"
"github.com/zitadel/zitadel/internal/notification/channels/instrumenting"
"github.com/zitadel/zitadel/internal/notification/channels/log"
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
"github.com/zitadel/zitadel/internal/notification/channels/webhook"
)
const smtpSpanName = "smtp.NotificationChannel"
func EmailChannels(
ctx context.Context,
emailConfig *smtp.Config,
emailConfig *email.Config,
getFileSystemProvider func(ctx context.Context) (*fs.Config, error),
getLogProvider func(ctx context.Context) (*log.Config, error),
successMetricName,
failureMetricName string,
) (chain *Chain, err error) {
channels := make([]channels.NotificationChannel, 0, 3)
p, err := smtp.InitChannel(emailConfig)
logging.WithFields(
"instance", authz.GetInstance(ctx).InstanceID(),
).OnError(err).Debug("initializing SMTP channel failed")
if err == nil {
channels = append(
channels,
instrumenting.Wrap(
ctx,
p,
smtpSpanName,
successMetricName,
failureMetricName,
),
)
if emailConfig.SMTPConfig != nil {
p, err := smtp.InitChannel(emailConfig.SMTPConfig)
logging.WithFields(
"instance", authz.GetInstance(ctx).InstanceID(),
).OnError(err).Debug("initializing SMTP channel failed")
if err == nil {
channels = append(
channels,
instrumenting.Wrap(
ctx,
p,
smtpSpanName,
successMetricName,
failureMetricName,
),
)
}
}
if emailConfig.WebhookConfig != nil {
webhookChannel, err := webhook.InitChannel(ctx, *emailConfig.WebhookConfig)
logging.WithFields(
"instance", authz.GetInstance(ctx).InstanceID(),
"callurl", emailConfig.WebhookConfig.CallURL,
).OnError(err).Debug("initializing JSON channel failed")
if err == nil {
channels = append(
channels,
instrumenting.Wrap(
ctx,
webhookChannel,
webhookSpanName,
successMetricName,
failureMetricName,
),
)
}
}
channels = append(channels, debugChannels(ctx, getFileSystemProvider, getLogProvider)...)
return ChainChannels(channels...), nil

View File

@@ -7,8 +7,8 @@ import (
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/i18n"
"github.com/zitadel/zitadel/internal/notification/channels/email"
"github.com/zitadel/zitadel/internal/notification/channels/sms"
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
"github.com/zitadel/zitadel/internal/notification/channels/webhook"
"github.com/zitadel/zitadel/internal/notification/senders"
"github.com/zitadel/zitadel/internal/notification/templates"
@@ -23,7 +23,7 @@ type Notify func(
) error
type ChannelChains interface {
Email(context.Context) (*senders.Chain, *smtp.Config, error)
Email(context.Context) (*senders.Chain, *email.Config, error)
SMS(context.Context) (*senders.Chain, *sms.Config, error)
Webhook(context.Context, webhook.Config) (*senders.Chain, error)
}
@@ -54,8 +54,9 @@ func SendEmail(
ctx,
channels,
user,
data.Subject,
template,
data,
args,
allowUnverifiedNotificationChannel,
triggeringEvent,
)

View File

@@ -3,9 +3,13 @@ package types
import (
"context"
"html"
"strings"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/notification/messages"
"github.com/zitadel/zitadel/internal/notification/templates"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
)
@@ -14,29 +18,56 @@ func generateEmail(
ctx context.Context,
channels ChannelChains,
user *query.NotifyUser,
subject,
content string,
template string,
data templates.TemplateData,
args map[string]interface{},
lastEmail bool,
triggeringEvent eventstore.Event,
) error {
content = html.UnescapeString(content)
message := &messages.Email{
Recipients: []string{user.VerifiedEmail},
Subject: subject,
Content: content,
TriggeringEvent: triggeringEvent,
}
if lastEmail {
message.Recipients = []string{user.LastEmail}
}
emailChannels, _, err := channels.Email(ctx)
if err != nil {
return err
}
emailChannels, config, err := channels.Email(ctx)
logging.OnError(err).Error("could not create email channel")
if emailChannels == nil || emailChannels.Len() == 0 {
return zerrors.ThrowPreconditionFailed(nil, "MAIL-83nof", "Errors.Notification.Channels.NotPresent")
return zerrors.ThrowPreconditionFailed(nil, "PHONE-w8nfow", "Errors.Notification.Channels.NotPresent")
}
return emailChannels.HandleMessage(message)
recipient := user.VerifiedEmail
if lastEmail {
recipient = user.LastEmail
}
if config.SMTPConfig != nil {
message := &messages.Email{
Recipients: []string{recipient},
Subject: data.Subject,
Content: html.UnescapeString(template),
TriggeringEvent: triggeringEvent,
}
return emailChannels.HandleMessage(message)
}
if config.WebhookConfig != nil {
caseArgs := make(map[string]interface{}, len(args))
for k, v := range args {
caseArgs[strings.ToLower(string(k[0]))+k[1:]] = v
}
contextInfo := map[string]interface{}{
"recipientEmailAddress": recipient,
"eventType": triggeringEvent.Type(),
"provider": config.ProviderConfig,
}
message := &messages.JSON{
Serializable: &serializableData{
ContextInfo: contextInfo,
TemplateData: data,
Args: caseArgs,
},
TriggeringEvent: triggeringEvent,
}
webhookChannels, err := channels.Webhook(ctx, *config.WebhookConfig)
if err != nil {
return err
}
return webhookChannels.HandleMessage(message)
}
return zerrors.ThrowPreconditionFailed(nil, "MAIL-83nof", "Errors.Notification.Channels.NotPresent")
}
func mapNotifyUserToArgs(user *query.NotifyUser, args map[string]interface{}) map[string]interface{} {