mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 03:57:32 +00:00
chore: move the go code into a subfolder
This commit is contained in:
20
apps/api/internal/notification/channels/channel.go
Normal file
20
apps/api/internal/notification/channels/channel.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package channels
|
||||
|
||||
import "github.com/zitadel/zitadel/internal/eventstore"
|
||||
|
||||
type Message interface {
|
||||
GetTriggeringEventType() eventstore.EventType
|
||||
GetContent() (string, error)
|
||||
}
|
||||
|
||||
type NotificationChannel interface {
|
||||
HandleMessage(message Message) error
|
||||
}
|
||||
|
||||
var _ NotificationChannel = (HandleMessageFunc)(nil)
|
||||
|
||||
type HandleMessageFunc func(message Message) error
|
||||
|
||||
func (h HandleMessageFunc) HandleMessage(message Message) error {
|
||||
return h(message)
|
||||
}
|
17
apps/api/internal/notification/channels/email/config.go
Normal file
17
apps/api/internal/notification/channels/email/config.go
Normal 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"`
|
||||
}
|
25
apps/api/internal/notification/channels/error.go
Normal file
25
apps/api/internal/notification/channels/error.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package channels
|
||||
|
||||
import "errors"
|
||||
|
||||
type CancelError struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *CancelError) Error() string {
|
||||
return e.Err.Error()
|
||||
}
|
||||
|
||||
func NewCancelError(err error) error {
|
||||
return &CancelError{
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *CancelError) Is(target error) bool {
|
||||
return errors.As(target, &e)
|
||||
}
|
||||
|
||||
func (e *CancelError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
52
apps/api/internal/notification/channels/fs/channel.go
Normal file
52
apps/api/internal/notification/channels/fs/channel.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package fs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/k3a/html2text"
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/notification/channels"
|
||||
"github.com/zitadel/zitadel/internal/notification/messages"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func InitFSChannel(config Config) (channels.NotificationChannel, error) {
|
||||
if err := os.MkdirAll(config.Path, os.ModePerm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logging.Debug("successfully initialized filesystem email and sms channel")
|
||||
|
||||
return channels.HandleMessageFunc(func(message channels.Message) error {
|
||||
|
||||
fileName := fmt.Sprintf("%d_", time.Now().Unix())
|
||||
content, err := message.GetContent()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch msg := message.(type) {
|
||||
case *messages.Email:
|
||||
recipients := make([]string, len(msg.Recipients))
|
||||
copy(recipients, msg.Recipients)
|
||||
sort.Strings(recipients)
|
||||
fileName = fileName + "mail_to_" + strings.Join(recipients, "_") + ".html"
|
||||
if config.Compact {
|
||||
content = html2text.HTML2Text(content)
|
||||
}
|
||||
case *messages.SMS:
|
||||
fileName = fileName + "sms_to_" + msg.RecipientPhoneNumber + ".txt"
|
||||
case *messages.JSON:
|
||||
fileName = "message.json"
|
||||
default:
|
||||
return zerrors.ThrowUnimplementedf(nil, "NOTIF-6f9a1", "filesystem provider doesn't support message type %T", message)
|
||||
}
|
||||
|
||||
return os.WriteFile(filepath.Join(config.Path, fileName), []byte(content), 0666)
|
||||
}), nil
|
||||
}
|
7
apps/api/internal/notification/channels/fs/config.go
Normal file
7
apps/api/internal/notification/channels/fs/config.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package fs
|
||||
|
||||
type Config struct {
|
||||
Enabled bool
|
||||
Compact bool
|
||||
Path string
|
||||
}
|
4
apps/api/internal/notification/channels/gen_mock.go
Normal file
4
apps/api/internal/notification/channels/gen_mock.go
Normal file
@@ -0,0 +1,4 @@
|
||||
package channels
|
||||
|
||||
//go:generate mockgen -package mock -destination ./mock/channel.mock.go github.com/zitadel/zitadel/internal/notification/channels NotificationChannel
|
||||
//go:generate mockgen -package mock -destination ./mock/message.mock.go github.com/zitadel/zitadel/internal/notification/channels Message
|
@@ -0,0 +1,26 @@
|
||||
package instrumenting
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/notification/channels"
|
||||
)
|
||||
|
||||
func Wrap(
|
||||
ctx context.Context,
|
||||
channel channels.NotificationChannel,
|
||||
traceSpanName,
|
||||
successMetricName,
|
||||
failureMetricName string,
|
||||
) channels.NotificationChannel {
|
||||
return traceMessages(
|
||||
ctx,
|
||||
countMessages(
|
||||
ctx,
|
||||
logMessages(ctx, channel),
|
||||
successMetricName,
|
||||
failureMetricName,
|
||||
),
|
||||
traceSpanName,
|
||||
)
|
||||
}
|
@@ -0,0 +1,24 @@
|
||||
package instrumenting
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels"
|
||||
)
|
||||
|
||||
func logMessages(ctx context.Context, channel channels.NotificationChannel) channels.NotificationChannel {
|
||||
return channels.HandleMessageFunc(func(message channels.Message) error {
|
||||
logEntry := logging.WithFields(
|
||||
"instance", authz.GetInstance(ctx).InstanceID(),
|
||||
"triggering_event_type", message.GetTriggeringEventType(),
|
||||
)
|
||||
logEntry.Debug("sending notification")
|
||||
err := channel.HandleMessage(message)
|
||||
logEntry.OnError(err).Warn("sending notification failed")
|
||||
logEntry.Debug("notification sent")
|
||||
return err
|
||||
})
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
package instrumenting
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/notification/channels"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/metrics"
|
||||
)
|
||||
|
||||
func countMessages(ctx context.Context, channel channels.NotificationChannel, successMetricName, errorMetricName string) channels.NotificationChannel {
|
||||
return channels.HandleMessageFunc(func(message channels.Message) error {
|
||||
err := channel.HandleMessage(message)
|
||||
metricName := successMetricName
|
||||
if err != nil {
|
||||
metricName = errorMetricName
|
||||
}
|
||||
addCount(ctx, metricName, message)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func addCount(ctx context.Context, metricName string, message channels.Message) {
|
||||
labels := map[string]attribute.Value{
|
||||
"triggering_event_type": attribute.StringValue(string(message.GetTriggeringEventType())),
|
||||
}
|
||||
addCountErr := metrics.AddCount(ctx, metricName, 1, labels)
|
||||
logging.WithFields("name", metricName, "labels", labels).OnError(addCountErr).Error("incrementing counter metric failed")
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
package instrumenting
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/notification/channels"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
)
|
||||
|
||||
func traceMessages(ctx context.Context, channel channels.NotificationChannel, spanName string) channels.NotificationChannel {
|
||||
return channels.HandleMessageFunc(func(message channels.Message) (err error) {
|
||||
_, span := tracing.NewNamedSpan(ctx, spanName)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
return channel.HandleMessage(message)
|
||||
})
|
||||
}
|
32
apps/api/internal/notification/channels/log/channel.go
Normal file
32
apps/api/internal/notification/channels/log/channel.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/k3a/html2text"
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/notification/channels"
|
||||
)
|
||||
|
||||
func InitStdoutChannel(config Config) channels.NotificationChannel {
|
||||
|
||||
logging.WithFields("logID", "NOTIF-D0164").Debug("successfully initialized stdout email and sms channel")
|
||||
|
||||
return channels.HandleMessageFunc(func(message channels.Message) error {
|
||||
|
||||
content, err := message.GetContent()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if config.Compact {
|
||||
content = html2text.HTML2Text(content)
|
||||
}
|
||||
|
||||
logging.WithFields("logID", "NOTIF-c73ba").WithFields(map[string]interface{}{
|
||||
"type": fmt.Sprintf("%T", message),
|
||||
"content": content,
|
||||
}).Info("handling notification message")
|
||||
return nil
|
||||
})
|
||||
}
|
6
apps/api/internal/notification/channels/log/config.go
Normal file
6
apps/api/internal/notification/channels/log/config.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package log
|
||||
|
||||
type Config struct {
|
||||
Enabled bool
|
||||
Compact bool
|
||||
}
|
54
apps/api/internal/notification/channels/mock/channel.mock.go
Normal file
54
apps/api/internal/notification/channels/mock/channel.mock.go
Normal file
@@ -0,0 +1,54 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/zitadel/zitadel/internal/notification/channels (interfaces: NotificationChannel)
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen -package mock -destination ./mock/channel.mock.go github.com/zitadel/zitadel/internal/notification/channels NotificationChannel
|
||||
//
|
||||
|
||||
// Package mock is a generated GoMock package.
|
||||
package mock
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
channels "github.com/zitadel/zitadel/internal/notification/channels"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockNotificationChannel is a mock of NotificationChannel interface.
|
||||
type MockNotificationChannel struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockNotificationChannelMockRecorder
|
||||
}
|
||||
|
||||
// MockNotificationChannelMockRecorder is the mock recorder for MockNotificationChannel.
|
||||
type MockNotificationChannelMockRecorder struct {
|
||||
mock *MockNotificationChannel
|
||||
}
|
||||
|
||||
// NewMockNotificationChannel creates a new mock instance.
|
||||
func NewMockNotificationChannel(ctrl *gomock.Controller) *MockNotificationChannel {
|
||||
mock := &MockNotificationChannel{ctrl: ctrl}
|
||||
mock.recorder = &MockNotificationChannelMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockNotificationChannel) EXPECT() *MockNotificationChannelMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// HandleMessage mocks base method.
|
||||
func (m *MockNotificationChannel) HandleMessage(arg0 channels.Message) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "HandleMessage", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// HandleMessage indicates an expected call of HandleMessage.
|
||||
func (mr *MockNotificationChannelMockRecorder) HandleMessage(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandleMessage", reflect.TypeOf((*MockNotificationChannel)(nil).HandleMessage), arg0)
|
||||
}
|
69
apps/api/internal/notification/channels/mock/message.mock.go
Normal file
69
apps/api/internal/notification/channels/mock/message.mock.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/zitadel/zitadel/internal/notification/channels (interfaces: Message)
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen -package mock -destination ./mock/message.mock.go github.com/zitadel/zitadel/internal/notification/channels Message
|
||||
//
|
||||
|
||||
// Package mock is a generated GoMock package.
|
||||
package mock
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
eventstore "github.com/zitadel/zitadel/internal/eventstore"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockMessage is a mock of Message interface.
|
||||
type MockMessage struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockMessageMockRecorder
|
||||
}
|
||||
|
||||
// MockMessageMockRecorder is the mock recorder for MockMessage.
|
||||
type MockMessageMockRecorder struct {
|
||||
mock *MockMessage
|
||||
}
|
||||
|
||||
// NewMockMessage creates a new mock instance.
|
||||
func NewMockMessage(ctrl *gomock.Controller) *MockMessage {
|
||||
mock := &MockMessage{ctrl: ctrl}
|
||||
mock.recorder = &MockMessageMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockMessage) EXPECT() *MockMessageMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// GetContent mocks base method.
|
||||
func (m *MockMessage) GetContent() (string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetContent")
|
||||
ret0, _ := ret[0].(string)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetContent indicates an expected call of GetContent.
|
||||
func (mr *MockMessageMockRecorder) GetContent() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetContent", reflect.TypeOf((*MockMessage)(nil).GetContent))
|
||||
}
|
||||
|
||||
// GetTriggeringEvent mocks base method.
|
||||
func (m *MockMessage) GetTriggeringEvent() eventstore.Event {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetTriggeringEvent")
|
||||
ret0, _ := ret[0].(eventstore.Event)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetTriggeringEvent indicates an expected call of GetTriggeringEvent.
|
||||
func (mr *MockMessageMockRecorder) GetTriggeringEvent() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTriggeringEvent", reflect.TypeOf((*MockMessage)(nil).GetTriggeringEvent))
|
||||
}
|
75
apps/api/internal/notification/channels/set/channel.go
Normal file
75
apps/api/internal/notification/channels/set/channel.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package set
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels"
|
||||
"github.com/zitadel/zitadel/internal/notification/messages"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func InitChannel(ctx context.Context, cfg Config) (channels.NotificationChannel, error) {
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logging.Debug("successfully initialized security event token json channel")
|
||||
return channels.HandleMessageFunc(func(message channels.Message) error {
|
||||
requestCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
msg, ok := message.(*messages.Form)
|
||||
if !ok {
|
||||
return zerrors.ThrowInternal(nil, "SET-K686U", "message is not SET")
|
||||
}
|
||||
payload, err := msg.GetContent()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(requestCtx, http.MethodPost, cfg.CallURL, strings.NewReader(payload))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logging.WithFields("instanceID", authz.GetInstance(ctx).InstanceID(), "calling_url", cfg.CallURL).Debug("security event token called")
|
||||
if resp.StatusCode == http.StatusOK ||
|
||||
resp.StatusCode == http.StatusAccepted ||
|
||||
resp.StatusCode == http.StatusNoContent {
|
||||
return nil
|
||||
}
|
||||
body, err := mapResponse(resp)
|
||||
logging.WithFields("instanceID", authz.GetInstance(ctx).InstanceID(), "callURL", cfg.CallURL).
|
||||
OnError(err).Debug("error mapping response")
|
||||
if resp.StatusCode == http.StatusBadRequest {
|
||||
logging.WithFields("instanceID", authz.GetInstance(ctx).InstanceID(), "callURL", cfg.CallURL, "status", resp.Status, "body", body).
|
||||
Error("security event token didn't return a success status")
|
||||
return nil
|
||||
}
|
||||
return zerrors.ThrowInternalf(err, "SET-DF3dq", "security event token to %s didn't return a success status: %s (%v)", cfg.CallURL, resp.Status, body)
|
||||
}), nil
|
||||
}
|
||||
|
||||
func mapResponse(resp *http.Response) (map[string]any, error) {
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
requestError := make(map[string]any)
|
||||
err = json.Unmarshal(body, &requestError)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return requestError, nil
|
||||
}
|
14
apps/api/internal/notification/channels/set/config.go
Normal file
14
apps/api/internal/notification/channels/set/config.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package set
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
CallURL string
|
||||
}
|
||||
|
||||
func (w *Config) Validate() error {
|
||||
_, err := url.Parse(w.CallURL)
|
||||
return err
|
||||
}
|
17
apps/api/internal/notification/channels/sms/config.go
Normal file
17
apps/api/internal/notification/channels/sms/config.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package sms
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/twilio"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/webhook"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
ProviderConfig *Provider
|
||||
TwilioConfig *twilio.Config
|
||||
WebhookConfig *webhook.Config
|
||||
}
|
||||
|
||||
type Provider struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
209
apps/api/internal/notification/channels/smtp/channel.go
Normal file
209
apps/api/internal/notification/channels/smtp/channel.go
Normal file
@@ -0,0 +1,209 @@
|
||||
package smtp
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"net"
|
||||
"net/smtp"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/notification/channels"
|
||||
"github.com/zitadel/zitadel/internal/notification/messages"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
var _ channels.NotificationChannel = (*Email)(nil)
|
||||
|
||||
type Email struct {
|
||||
smtpClient *smtp.Client
|
||||
senderAddress string
|
||||
senderName string
|
||||
replyToAddress string
|
||||
}
|
||||
|
||||
func InitChannel(cfg *Config) (*Email, error) {
|
||||
client, err := cfg.SMTP.connectToSMTP(cfg.Tls)
|
||||
if err != nil {
|
||||
logging.New().WithError(err).Error("could not connect to smtp")
|
||||
return nil, err
|
||||
}
|
||||
logging.New().Debug("successfully initialized smtp email channel")
|
||||
return &Email{
|
||||
smtpClient: client,
|
||||
senderName: cfg.FromName,
|
||||
senderAddress: cfg.From,
|
||||
replyToAddress: cfg.ReplyToAddress,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (email *Email) HandleMessage(message channels.Message) error {
|
||||
defer email.smtpClient.Close()
|
||||
emailMsg, ok := message.(*messages.Email)
|
||||
if !ok {
|
||||
return zerrors.ThrowInternal(nil, "EMAIL-s8JLs", "Errors.SMTP.NotEmailMessage")
|
||||
}
|
||||
|
||||
if emailMsg.Content == "" || emailMsg.Subject == "" || len(emailMsg.Recipients) == 0 {
|
||||
return zerrors.ThrowInternal(nil, "EMAIL-zGemZ", "Errors.SMTP.RequiredAttributes")
|
||||
}
|
||||
emailMsg.SenderEmail = email.senderAddress
|
||||
emailMsg.SenderName = email.senderName
|
||||
emailMsg.ReplyToAddress = email.replyToAddress
|
||||
// To && From
|
||||
if err := email.smtpClient.Mail(emailMsg.SenderEmail); err != nil {
|
||||
return zerrors.ThrowInternal(err, "EMAIL-s3is3", "Errors.SMTP.CouldNotSetSender")
|
||||
}
|
||||
for _, recp := range append(append(emailMsg.Recipients, emailMsg.CC...), emailMsg.BCC...) {
|
||||
if err := email.smtpClient.Rcpt(recp); err != nil {
|
||||
return zerrors.ThrowInternal(err, "EMAIL-s4is4", "Errors.SMTP.CouldNotSetRecipient")
|
||||
}
|
||||
}
|
||||
|
||||
// Data
|
||||
w, err := email.smtpClient.Data()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
content, err := emailMsg.GetContent()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = w.Write([]byte(content))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return email.smtpClient.Quit()
|
||||
}
|
||||
|
||||
func (smtpConfig SMTP) connectToSMTP(tlsRequired bool) (client *smtp.Client, err error) {
|
||||
host, _, err := net.SplitHostPort(smtpConfig.Host)
|
||||
if err != nil {
|
||||
return nil, zerrors.ThrowInternal(err, "EMAIL-spR56", "Errors.SMTP.CouldNotSplit")
|
||||
}
|
||||
|
||||
if !tlsRequired {
|
||||
client, err = smtpConfig.getSMTPClient()
|
||||
} else {
|
||||
client, err = smtpConfig.getSMTPClientWithTls(host)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = smtpConfig.smtpAuth(client, host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (smtpConfig SMTP) getSMTPClient() (*smtp.Client, error) {
|
||||
client, err := smtp.Dial(smtpConfig.Host)
|
||||
if err != nil {
|
||||
return nil, zerrors.ThrowInternal(err, "EMAIL-skwos", "Errors.SMTP.CouldNotDial")
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (smtpConfig SMTP) getSMTPClientWithTls(host string) (*smtp.Client, error) {
|
||||
conn, err := tls.Dial("tcp", smtpConfig.Host, &tls.Config{})
|
||||
|
||||
if errors.As(err, &tls.RecordHeaderError{}) {
|
||||
logging.OnError(err).Warn("could not connect using normal tls. trying starttls instead...")
|
||||
return smtpConfig.getSMTPClientWithStartTls(host)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, zerrors.ThrowInternal(err, "EMAIL-sl39s", "Errors.SMTP.CouldNotDialTLS")
|
||||
}
|
||||
|
||||
client, err := smtp.NewClient(conn, host)
|
||||
if err != nil {
|
||||
return nil, zerrors.ThrowInternal(err, "EMAIL-skwi4", "Errors.SMTP.CouldNotCreateClient")
|
||||
}
|
||||
return client, err
|
||||
}
|
||||
|
||||
func (smtpConfig SMTP) getSMTPClientWithStartTls(host string) (*smtp.Client, error) {
|
||||
client, err := smtpConfig.getSMTPClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := client.StartTLS(&tls.Config{
|
||||
ServerName: host,
|
||||
}); err != nil {
|
||||
return nil, zerrors.ThrowInternal(err, "EMAIL-guvsQ", "Errors.SMTP.CouldNotStartTLS")
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (smtpConfig SMTP) smtpAuth(client *smtp.Client, host string) error {
|
||||
if !smtpConfig.HasAuth() {
|
||||
return nil
|
||||
}
|
||||
// Auth
|
||||
err := client.Auth(PlainOrLoginAuth(smtpConfig.User, smtpConfig.Password, host))
|
||||
if err != nil {
|
||||
return zerrors.ThrowInternal(err, "EMAIL-s9kfs", "Errors.SMTP.CouldNotAuth")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestConfiguration(cfg *Config, testEmail string) error {
|
||||
client, err := cfg.SMTP.connectToSMTP(cfg.Tls)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer client.Close()
|
||||
|
||||
message := &messages.Email{
|
||||
Recipients: []string{testEmail},
|
||||
Subject: "Test email",
|
||||
Content: "This is a test email to check if your SMTP provider works fine",
|
||||
SenderEmail: cfg.From,
|
||||
SenderName: cfg.FromName,
|
||||
}
|
||||
|
||||
if err := client.Mail(cfg.From); err != nil {
|
||||
return zerrors.ThrowInternal(err, "EMAIL-s3is3", "Errors.SMTP.CouldNotSetSender")
|
||||
}
|
||||
|
||||
if err := client.Rcpt(testEmail); err != nil {
|
||||
return zerrors.ThrowInternal(err, "EMAIL-s4is4", "Errors.SMTP.CouldNotSetRecipient")
|
||||
}
|
||||
|
||||
// Open data connection
|
||||
w, err := client.Data()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Send content
|
||||
content, err := message.GetContent()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write([]byte(content))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Close IO and quit smtp connection
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return client.Quit()
|
||||
}
|
23
apps/api/internal/notification/channels/smtp/config.go
Normal file
23
apps/api/internal/notification/channels/smtp/config.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package smtp
|
||||
|
||||
type Config struct {
|
||||
SMTP SMTP
|
||||
Tls bool
|
||||
From string
|
||||
FromName string
|
||||
ReplyToAddress string
|
||||
}
|
||||
|
||||
type SMTP struct {
|
||||
Host string
|
||||
User string
|
||||
Password string
|
||||
}
|
||||
|
||||
func (smtp *SMTP) HasAuth() bool {
|
||||
return smtp.User != "" && smtp.Password != ""
|
||||
}
|
||||
|
||||
type ConfigHTTP struct {
|
||||
Endpoint string
|
||||
}
|
@@ -0,0 +1,57 @@
|
||||
package smtp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/smtp"
|
||||
"slices"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
// golang net/smtp SMTP AUTH LOGIN or PLAIN Auth Handler
|
||||
// Reference: https://gist.github.com/andelf/5118732?permalink_comment_id=4825669#gistcomment-4825669
|
||||
|
||||
func PlainOrLoginAuth(username, password, host string) smtp.Auth {
|
||||
return &plainOrLoginAuth{username: username, password: password, host: host}
|
||||
}
|
||||
|
||||
type plainOrLoginAuth struct {
|
||||
username string
|
||||
password string
|
||||
host string
|
||||
authMethod string
|
||||
}
|
||||
|
||||
func (a *plainOrLoginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
|
||||
if server.Name != a.host {
|
||||
return "", nil, zerrors.ThrowInternal(nil, "SMTP-RRi75", "wrong host name")
|
||||
}
|
||||
if !slices.Contains(server.Auth, "PLAIN") {
|
||||
a.authMethod = "LOGIN"
|
||||
return a.authMethod, nil, nil
|
||||
} else {
|
||||
a.authMethod = "PLAIN"
|
||||
resp := []byte("\x00" + a.username + "\x00" + a.password)
|
||||
return a.authMethod, resp, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (a *plainOrLoginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
|
||||
if !more {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if a.authMethod == "PLAIN" {
|
||||
// We've already sent everything.
|
||||
return nil, zerrors.ThrowInternal(nil, "SMTP-AAf43", "unexpected server challenge for PLAIN auth method")
|
||||
}
|
||||
|
||||
switch {
|
||||
case bytes.Equal(fromServer, []byte("Username:")):
|
||||
return []byte(a.username), nil
|
||||
case bytes.Equal(fromServer, []byte("Password:")):
|
||||
return []byte(a.password), nil
|
||||
default:
|
||||
return nil, zerrors.ThrowInternal(nil, "SMTP-HjW21", "unexpected server challenge")
|
||||
}
|
||||
}
|
76
apps/api/internal/notification/channels/twilio/channel.go
Normal file
76
apps/api/internal/notification/channels/twilio/channel.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package twilio
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/twilio/twilio-go"
|
||||
twilioClient "github.com/twilio/twilio-go/client"
|
||||
openapi "github.com/twilio/twilio-go/rest/api/v2010"
|
||||
verify "github.com/twilio/twilio-go/rest/verify/v2"
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/notification/channels"
|
||||
"github.com/zitadel/zitadel/internal/notification/messages"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
const (
|
||||
aggregateTypeNotification = "notification"
|
||||
)
|
||||
|
||||
func InitChannel(config Config) channels.NotificationChannel {
|
||||
client := twilio.NewRestClientWithParams(twilio.ClientParams{Username: config.SID, Password: config.Token})
|
||||
logging.Debug("successfully initialized twilio sms channel")
|
||||
|
||||
return channels.HandleMessageFunc(func(message channels.Message) error {
|
||||
twilioMsg, ok := message.(*messages.SMS)
|
||||
if !ok {
|
||||
return zerrors.ThrowInternal(nil, "TWILI-s0pLc", "message is not SMS")
|
||||
}
|
||||
if config.VerifyServiceSID != "" {
|
||||
params := &verify.CreateVerificationParams{}
|
||||
params.SetTo(twilioMsg.RecipientPhoneNumber)
|
||||
params.SetChannel("sms")
|
||||
|
||||
resp, err := client.VerifyV2.CreateVerification(config.VerifyServiceSID, params)
|
||||
|
||||
// In case of any client error (4xx), we should not retry sending the verification code
|
||||
// as it would be a waste of resources and could potentially result in a rate limit.
|
||||
var twilioErr *twilioClient.TwilioRestError
|
||||
if errors.As(err, &twilioErr) && twilioErr.Status >= 400 && twilioErr.Status < 500 {
|
||||
logging.WithFields(
|
||||
"error", twilioErr.Message,
|
||||
"status", twilioErr.Status,
|
||||
"code", twilioErr.Code,
|
||||
"instanceID", twilioMsg.InstanceID,
|
||||
"jobID", twilioMsg.JobID,
|
||||
"userID", twilioMsg.UserID,
|
||||
).Warn("twilio create verification error")
|
||||
return channels.NewCancelError(twilioErr)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return zerrors.ThrowInternal(err, "TWILI-0s9f2", "could not send verification")
|
||||
}
|
||||
logging.WithFields("sid", resp.Sid, "status", resp.Status).Debug("verification sent")
|
||||
|
||||
twilioMsg.VerificationID = resp.Sid
|
||||
return nil
|
||||
}
|
||||
|
||||
content, err := twilioMsg.GetContent()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
params := &openapi.CreateMessageParams{}
|
||||
params.SetTo(twilioMsg.RecipientPhoneNumber)
|
||||
params.SetFrom(twilioMsg.SenderPhoneNumber)
|
||||
params.SetBody(content)
|
||||
m, err := client.Api.CreateMessage(params)
|
||||
if err != nil {
|
||||
return zerrors.ThrowInternal(err, "TWILI-osk3S", "could not send message")
|
||||
}
|
||||
logging.WithFields("message_sid", m.Sid, "status", m.Status).Debug("sms sent")
|
||||
return nil
|
||||
})
|
||||
}
|
40
apps/api/internal/notification/channels/twilio/config.go
Normal file
40
apps/api/internal/notification/channels/twilio/config.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package twilio
|
||||
|
||||
import (
|
||||
newTwilio "github.com/twilio/twilio-go"
|
||||
verify "github.com/twilio/twilio-go/rest/verify/v2"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
SID string
|
||||
Token string
|
||||
SenderNumber string
|
||||
VerifyServiceSID string
|
||||
}
|
||||
|
||||
func (t *Config) IsValid() bool {
|
||||
return t.SID != "" && t.Token != "" && t.SenderNumber != ""
|
||||
}
|
||||
|
||||
func (t *Config) VerifyCode(verificationID, code string) error {
|
||||
client := newTwilio.NewRestClientWithParams(newTwilio.ClientParams{Username: t.SID, Password: t.Token})
|
||||
checkParams := &verify.CreateVerificationCheckParams{}
|
||||
checkParams.SetVerificationSid(verificationID)
|
||||
checkParams.SetCode(code)
|
||||
resp, err := client.VerifyV2.CreateVerificationCheck(t.VerifyServiceSID, checkParams)
|
||||
if err != nil || resp.Status == nil {
|
||||
return zerrors.ThrowInvalidArgument(err, "TWILI-JK3ta", "Errors.User.Code.NotFound")
|
||||
}
|
||||
switch *resp.Status {
|
||||
case "approved":
|
||||
return nil
|
||||
case "expired":
|
||||
return zerrors.ThrowInvalidArgument(nil, "TWILI-SF3ba", "Errors.User.Code.Expired")
|
||||
case "max_attempts_reached":
|
||||
return zerrors.ThrowInvalidArgument(nil, "TWILI-Ok39a", "Errors.User.Code.NotFound")
|
||||
default:
|
||||
return zerrors.ThrowInvalidArgument(nil, "TWILI-Skwe4", "Errors.User.Code.Invalid")
|
||||
}
|
||||
}
|
55
apps/api/internal/notification/channels/webhook/channel.go
Normal file
55
apps/api/internal/notification/channels/webhook/channel.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/notification/channels"
|
||||
"github.com/zitadel/zitadel/internal/notification/messages"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func InitChannel(ctx context.Context, cfg Config) (channels.NotificationChannel, error) {
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logging.Debug("successfully initialized webhook json channel")
|
||||
return channels.HandleMessageFunc(func(message channels.Message) error {
|
||||
requestCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
msg, ok := message.(*messages.JSON)
|
||||
if !ok {
|
||||
return zerrors.ThrowInternal(nil, "WEBH-K686U", "message is not JSON")
|
||||
}
|
||||
payload, err := msg.GetContent()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(requestCtx, cfg.Method, cfg.CallURL, strings.NewReader(payload))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cfg.Headers != nil {
|
||||
req.Header = cfg.Headers
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = resp.Body.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return zerrors.ThrowUnknown(fmt.Errorf("calling url %s returned %s", cfg.CallURL, resp.Status), "WEBH-LBxU0", "webhook didn't return a success status")
|
||||
}
|
||||
logging.WithFields("calling_url", cfg.CallURL, "method", cfg.Method).Debug("webhook called")
|
||||
return nil
|
||||
}), nil
|
||||
}
|
17
apps/api/internal/notification/channels/webhook/config.go
Normal file
17
apps/api/internal/notification/channels/webhook/config.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
CallURL string
|
||||
Method string
|
||||
Headers http.Header
|
||||
}
|
||||
|
||||
func (w *Config) Validate() error {
|
||||
_, err := url.Parse(w.CallURL)
|
||||
return err
|
||||
}
|
Reference in New Issue
Block a user