mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 04:49:01 +08:00
feat(emailing): add smtp and emailing (#7993)
* feat(emailing): initial commit for emailing * feat(emailing): implement emailing * test(integration): fix tests * fix(emailing): fix directory path * fix(emailing): fix email template path * fix(emailing): copy from go-gomail * fix(emailing): copy from go-gomail * fix(emailing): fix smtp bugs * test(integration): fix tests * feat(emailing): let missing templates passthrough * feat(emailing): let missing templates passthrough * feat(smtp): refactor and beautify * test(integration): fix tests * docs(smtp): fix incorrect grammer * feat(smtp): add to header * feat(smtp): remove comments * chore(smtp): address comments --------- Co-authored-by: Vikrant Gupta <vikrant@signoz.io>
This commit is contained in:
parent
a1c7a948fa
commit
9e13245d1b
@ -170,3 +170,40 @@ alertmanager:
|
|||||||
analytics:
|
analytics:
|
||||||
# Whether to enable analytics.
|
# Whether to enable analytics.
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
||||||
|
##################### Emailing #####################
|
||||||
|
emailing:
|
||||||
|
# Whether to enable emailing.
|
||||||
|
enabled: false
|
||||||
|
templates:
|
||||||
|
# The directory containing the email templates. This directory should contain a list of files defined at pkg/types/emailtypes/template.go.
|
||||||
|
directory: /opt/signoz/conf/templates/email
|
||||||
|
smtp:
|
||||||
|
# The SMTP server address.
|
||||||
|
address: localhost:25
|
||||||
|
# The email address to use for the SMTP server.
|
||||||
|
from:
|
||||||
|
# The hello message to use for the SMTP server.
|
||||||
|
hello:
|
||||||
|
# The static headers to send with the email.
|
||||||
|
headers: {}
|
||||||
|
auth:
|
||||||
|
# The username to use for the SMTP server.
|
||||||
|
username:
|
||||||
|
# The password to use for the SMTP server.
|
||||||
|
password:
|
||||||
|
# The secret to use for the SMTP server.
|
||||||
|
secret:
|
||||||
|
# The identity to use for the SMTP server.
|
||||||
|
identity:
|
||||||
|
tls:
|
||||||
|
# Whether to enable TLS. It should be false in most cases since the authentication mechanism should use the STARTTLS extension instead.
|
||||||
|
enabled: false
|
||||||
|
# Whether to skip TLS verification.
|
||||||
|
insecure_skip_verify: false
|
||||||
|
# The path to the CA file.
|
||||||
|
ca_file_path:
|
||||||
|
# The path to the key file.
|
||||||
|
key_file_path:
|
||||||
|
# The path to the certificate file.
|
||||||
|
cert_file_path:
|
||||||
|
@ -7,7 +7,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/ee/query-service/constants"
|
"github.com/SigNoz/signoz/ee/query-service/constants"
|
||||||
|
"github.com/SigNoz/signoz/pkg/emailing"
|
||||||
"github.com/SigNoz/signoz/pkg/errors"
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||||
baseimpl "github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
baseimpl "github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
"github.com/SigNoz/signoz/pkg/types"
|
||||||
@ -22,8 +24,8 @@ type Module struct {
|
|||||||
store types.UserStore
|
store types.UserStore
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewModule(store types.UserStore) user.Module {
|
func NewModule(store types.UserStore, jwt *authtypes.JWT, emailing emailing.Emailing, providerSettings factory.ProviderSettings) user.Module {
|
||||||
baseModule := baseimpl.NewModule(store)
|
baseModule := baseimpl.NewModule(store, jwt, emailing, providerSettings)
|
||||||
return &Module{
|
return &Module{
|
||||||
Module: baseModule,
|
Module: baseModule,
|
||||||
store: store,
|
store: store,
|
||||||
|
@ -12,7 +12,6 @@ import (
|
|||||||
"github.com/SigNoz/signoz/ee/query-service/model"
|
"github.com/SigNoz/signoz/ee/query-service/model"
|
||||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
"github.com/SigNoz/signoz/pkg/types"
|
||||||
ossTypes "github.com/SigNoz/signoz/pkg/types"
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
@ -164,7 +163,7 @@ func (m *modelDao) CreateDomain(ctx context.Context, domain *types.GettableOrgDo
|
|||||||
Name: domain.Name,
|
Name: domain.Name,
|
||||||
OrgID: domain.OrgID,
|
OrgID: domain.OrgID,
|
||||||
Data: string(configJson),
|
Data: string(configJson),
|
||||||
TimeAuditable: ossTypes.TimeAuditable{CreatedAt: time.Now(), UpdatedAt: time.Now()},
|
TimeAuditable: types.TimeAuditable{CreatedAt: time.Now(), UpdatedAt: time.Now()},
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = m.sqlStore.BunDB().NewInsert().
|
_, err = m.sqlStore.BunDB().NewInsert().
|
||||||
@ -198,7 +197,7 @@ func (m *modelDao) UpdateDomain(ctx context.Context, domain *types.GettableOrgDo
|
|||||||
Name: domain.Name,
|
Name: domain.Name,
|
||||||
OrgID: domain.OrgID,
|
OrgID: domain.OrgID,
|
||||||
Data: string(configJson),
|
Data: string(configJson),
|
||||||
TimeAuditable: ossTypes.TimeAuditable{UpdatedAt: time.Now()},
|
TimeAuditable: types.TimeAuditable{UpdatedAt: time.Now()},
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = m.sqlStore.BunDB().NewUpdate().
|
_, err = m.sqlStore.BunDB().NewUpdate().
|
||||||
|
@ -1,18 +1,14 @@
|
|||||||
package sqlite
|
package sqlite
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
|
||||||
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
)
|
)
|
||||||
|
|
||||||
type modelDao struct {
|
type modelDao struct {
|
||||||
userModule user.Module
|
|
||||||
sqlStore sqlstore.SQLStore
|
sqlStore sqlstore.SQLStore
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitDB creates and extends base model DB repository
|
// InitDB creates and extends base model DB repository
|
||||||
func NewModelDao(sqlStore sqlstore.SQLStore) *modelDao {
|
func NewModelDao(sqlStore sqlstore.SQLStore) *modelDao {
|
||||||
userModule := impluser.NewModule(impluser.NewStore(sqlStore))
|
return &modelDao{sqlStore: sqlStore}
|
||||||
return &modelDao{userModule: userModule, sqlStore: sqlStore}
|
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,8 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/config"
|
"github.com/SigNoz/signoz/pkg/config"
|
||||||
"github.com/SigNoz/signoz/pkg/config/envprovider"
|
"github.com/SigNoz/signoz/pkg/config/envprovider"
|
||||||
"github.com/SigNoz/signoz/pkg/config/fileprovider"
|
"github.com/SigNoz/signoz/pkg/config/fileprovider"
|
||||||
|
"github.com/SigNoz/signoz/pkg/emailing"
|
||||||
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||||
baseconst "github.com/SigNoz/signoz/pkg/query-service/constants"
|
baseconst "github.com/SigNoz/signoz/pkg/query-service/constants"
|
||||||
"github.com/SigNoz/signoz/pkg/signoz"
|
"github.com/SigNoz/signoz/pkg/signoz"
|
||||||
@ -112,26 +114,6 @@ func main() {
|
|||||||
zap.L().Fatal("Failed to add postgressqlstore factory", zap.Error(err))
|
zap.L().Fatal("Failed to add postgressqlstore factory", zap.Error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
signoz, err := signoz.New(
|
|
||||||
context.Background(),
|
|
||||||
config,
|
|
||||||
zeus.Config(),
|
|
||||||
httpzeus.NewProviderFactory(),
|
|
||||||
signoz.NewCacheProviderFactories(),
|
|
||||||
signoz.NewWebProviderFactories(),
|
|
||||||
sqlStoreFactories,
|
|
||||||
signoz.NewTelemetryStoreProviderFactories(),
|
|
||||||
func(sqlstore sqlstore.SQLStore) user.Module {
|
|
||||||
return eeuserimpl.NewModule(eeuserimpl.NewStore(sqlstore))
|
|
||||||
},
|
|
||||||
func(userModule user.Module) user.Handler {
|
|
||||||
return eeuserimpl.NewHandler(userModule)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Fatal("Failed to create signoz", zap.Error(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
jwtSecret := os.Getenv("SIGNOZ_JWT_SECRET")
|
jwtSecret := os.Getenv("SIGNOZ_JWT_SECRET")
|
||||||
|
|
||||||
if len(jwtSecret) == 0 {
|
if len(jwtSecret) == 0 {
|
||||||
@ -142,6 +124,27 @@ func main() {
|
|||||||
|
|
||||||
jwt := authtypes.NewJWT(jwtSecret, 30*time.Minute, 30*24*time.Hour)
|
jwt := authtypes.NewJWT(jwtSecret, 30*time.Minute, 30*24*time.Hour)
|
||||||
|
|
||||||
|
signoz, err := signoz.New(
|
||||||
|
context.Background(),
|
||||||
|
config,
|
||||||
|
zeus.Config(),
|
||||||
|
httpzeus.NewProviderFactory(),
|
||||||
|
signoz.NewEmailingProviderFactories(),
|
||||||
|
signoz.NewCacheProviderFactories(),
|
||||||
|
signoz.NewWebProviderFactories(),
|
||||||
|
sqlStoreFactories,
|
||||||
|
signoz.NewTelemetryStoreProviderFactories(),
|
||||||
|
func(sqlstore sqlstore.SQLStore, emailing emailing.Emailing, providerSettings factory.ProviderSettings) user.Module {
|
||||||
|
return eeuserimpl.NewModule(eeuserimpl.NewStore(sqlstore), jwt, emailing, providerSettings)
|
||||||
|
},
|
||||||
|
func(userModule user.Module) user.Handler {
|
||||||
|
return eeuserimpl.NewHandler(userModule)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
zap.L().Fatal("Failed to create signoz", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
serverOptions := &app.ServerOptions{
|
serverOptions := &app.ServerOptions{
|
||||||
Config: config,
|
Config: config,
|
||||||
SigNoz: signoz,
|
SigNoz: signoz,
|
||||||
|
81
pkg/emailing/config.go
Normal file
81
pkg/emailing/config.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package emailing
|
||||||
|
|
||||||
|
import "github.com/SigNoz/signoz/pkg/factory"
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Enabled bool `mapstructure:"enabled"`
|
||||||
|
Templates Templates `mapstructure:"templates"`
|
||||||
|
SMTP SMTP `mapstructure:"smtp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Templates struct {
|
||||||
|
Directory string `mapstructure:"directory"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SMTP struct {
|
||||||
|
Address string `mapstructure:"address"`
|
||||||
|
From string `mapstructure:"from"`
|
||||||
|
Hello string `mapstructure:"hello"`
|
||||||
|
Headers map[string]string `mapstructure:"headers"`
|
||||||
|
Auth SMTPAuth `mapstructure:"auth"`
|
||||||
|
TLS SMTPTLS `mapstructure:"tls"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SMTPAuth struct {
|
||||||
|
Username string `mapstructure:"username"`
|
||||||
|
Password string `mapstructure:"password"`
|
||||||
|
Secret string `mapstructure:"secret"`
|
||||||
|
Identity string `mapstructure:"identity"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SMTPTLS struct {
|
||||||
|
Enabled bool `mapstructure:"enabled"`
|
||||||
|
InsecureSkipVerify bool `mapstructure:"insecure_skip_verify"`
|
||||||
|
CAFilePath string `mapstructure:"ca_file_path"`
|
||||||
|
KeyFilePath string `mapstructure:"key_file_path"`
|
||||||
|
CertFilePath string `mapstructure:"cert_file_path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfigFactory() factory.ConfigFactory {
|
||||||
|
return factory.NewConfigFactory(factory.MustNewName("emailing"), newConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newConfig() factory.Config {
|
||||||
|
return &Config{
|
||||||
|
Enabled: false,
|
||||||
|
Templates: Templates{
|
||||||
|
Directory: "/root/templates",
|
||||||
|
},
|
||||||
|
SMTP: SMTP{
|
||||||
|
Address: "localhost:25",
|
||||||
|
From: "",
|
||||||
|
Hello: "",
|
||||||
|
Headers: map[string]string{},
|
||||||
|
Auth: SMTPAuth{
|
||||||
|
Username: "",
|
||||||
|
Password: "",
|
||||||
|
Secret: "",
|
||||||
|
Identity: "",
|
||||||
|
},
|
||||||
|
TLS: SMTPTLS{
|
||||||
|
Enabled: false,
|
||||||
|
InsecureSkipVerify: false,
|
||||||
|
CAFilePath: "",
|
||||||
|
KeyFilePath: "",
|
||||||
|
CertFilePath: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Config) Validate() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Config) Provider() string {
|
||||||
|
if c.Enabled {
|
||||||
|
return "smtp"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "noop"
|
||||||
|
}
|
12
pkg/emailing/emailing.go
Normal file
12
pkg/emailing/emailing.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package emailing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/emailtypes"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Emailing interface {
|
||||||
|
// Sends an HTML email to the given address with the given subject and template name and data.
|
||||||
|
SendHTML(context.Context, string, string, emailtypes.TemplateName, map[string]any) error
|
||||||
|
}
|
29
pkg/emailing/noopemailing/provider.go
Normal file
29
pkg/emailing/noopemailing/provider.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package noopemailing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/emailing"
|
||||||
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/emailtypes"
|
||||||
|
)
|
||||||
|
|
||||||
|
type provider struct {
|
||||||
|
settings factory.ScopedProviderSettings
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFactory() factory.ProviderFactory[emailing.Emailing, emailing.Config] {
|
||||||
|
return factory.NewProviderFactory(factory.MustNewName("noop"), New)
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(ctx context.Context, providerSettings factory.ProviderSettings, config emailing.Config) (emailing.Emailing, error) {
|
||||||
|
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/emailing/noopemailing")
|
||||||
|
return &provider{
|
||||||
|
settings: settings,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *provider) SendHTML(ctx context.Context, to string, subject string, templateName emailtypes.TemplateName, data map[string]any) error {
|
||||||
|
provider.settings.Logger().WarnContext(ctx, "using noop provider, no email will be sent", "to", to, "subject", subject)
|
||||||
|
return nil
|
||||||
|
}
|
78
pkg/emailing/smtpemailing/provider.go
Normal file
78
pkg/emailing/smtpemailing/provider.go
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
package smtpemailing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/mail"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/emailing"
|
||||||
|
"github.com/SigNoz/signoz/pkg/emailing/templatestore/filetemplatestore"
|
||||||
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
|
"github.com/SigNoz/signoz/pkg/smtp/client"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/emailtypes"
|
||||||
|
)
|
||||||
|
|
||||||
|
type provider struct {
|
||||||
|
settings factory.ScopedProviderSettings
|
||||||
|
store emailtypes.TemplateStore
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFactory() factory.ProviderFactory[emailing.Emailing, emailing.Config] {
|
||||||
|
return factory.NewProviderFactory(factory.MustNewName("smtp"), New)
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(ctx context.Context, providerSettings factory.ProviderSettings, config emailing.Config) (emailing.Emailing, error) {
|
||||||
|
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/emailing/smtpemailing")
|
||||||
|
|
||||||
|
// Try to create a template store. If it fails, use an empty store.
|
||||||
|
store, err := filetemplatestore.NewStore(config.Templates.Directory, emailtypes.Templates, settings.Logger())
|
||||||
|
if err != nil {
|
||||||
|
settings.Logger().ErrorContext(ctx, "failed to create template store, using empty store", "error", err)
|
||||||
|
store = filetemplatestore.NewEmptyStore()
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := client.New(
|
||||||
|
config.SMTP.Address,
|
||||||
|
settings.Logger(),
|
||||||
|
client.WithFrom(config.SMTP.From),
|
||||||
|
client.WithHello(config.SMTP.Hello),
|
||||||
|
client.WithHeaders(config.SMTP.Headers),
|
||||||
|
client.WithTLS(client.TLS{
|
||||||
|
Enabled: config.SMTP.TLS.Enabled,
|
||||||
|
InsecureSkipVerify: config.SMTP.TLS.InsecureSkipVerify,
|
||||||
|
CAFilePath: config.SMTP.TLS.CAFilePath,
|
||||||
|
KeyFilePath: config.SMTP.TLS.KeyFilePath,
|
||||||
|
CertFilePath: config.SMTP.TLS.CertFilePath,
|
||||||
|
}),
|
||||||
|
client.WithAuth(client.Auth{
|
||||||
|
Username: config.SMTP.Auth.Username,
|
||||||
|
Password: config.SMTP.Auth.Password,
|
||||||
|
Secret: config.SMTP.Auth.Secret,
|
||||||
|
Identity: config.SMTP.Auth.Identity,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &provider{settings: settings, store: store, client: client}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *provider) SendHTML(ctx context.Context, to string, subject string, templateName emailtypes.TemplateName, data map[string]any) error {
|
||||||
|
toAddress, err := mail.ParseAddressList(to)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
template, err := provider.store.Get(ctx, templateName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := emailtypes.NewContent(template, data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return provider.client.Do(ctx, toAddress, subject, client.ContentTypeHTML, content)
|
||||||
|
}
|
103
pkg/emailing/templatestore/filetemplatestore/store.go
Normal file
103
pkg/emailing/templatestore/filetemplatestore/store.go
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
package filetemplatestore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"html/template"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/emailtypes"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
emailTemplateExt = ".gotmpl"
|
||||||
|
)
|
||||||
|
|
||||||
|
type store struct {
|
||||||
|
fs map[emailtypes.TemplateName]*template.Template
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStore(baseDir string, templates []emailtypes.TemplateName, logger *slog.Logger) (emailtypes.TemplateStore, error) {
|
||||||
|
fs := make(map[emailtypes.TemplateName]*template.Template)
|
||||||
|
fis, err := os.ReadDir(filepath.Clean(baseDir))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
foundTemplates := make(map[emailtypes.TemplateName]bool)
|
||||||
|
for _, fi := range fis {
|
||||||
|
if fi.IsDir() || filepath.Ext(fi.Name()) != emailTemplateExt {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
templateName, err := parseTemplateName(fi.Name())
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !slices.Contains(templates, templateName) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := parseTemplateFile(filepath.Join(baseDir, fi.Name()), templateName)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("failed to parse template file", "template", templateName, "path", filepath.Join(baseDir, fi.Name()), "error", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fs[templateName] = t
|
||||||
|
foundTemplates[templateName] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := checkMissingTemplates(templates, foundTemplates); err != nil {
|
||||||
|
logger.Error("some templates are missing", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &store{fs: fs}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEmptyStore() emailtypes.TemplateStore {
|
||||||
|
return &store{fs: make(map[emailtypes.TemplateName]*template.Template)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repository *store) Get(ctx context.Context, name emailtypes.TemplateName) (*template.Template, error) {
|
||||||
|
template, ok := repository.fs[name]
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.Newf(errors.TypeNotFound, errors.CodeNotFound, "cannot find template with name %q", name.StringValue())
|
||||||
|
}
|
||||||
|
|
||||||
|
return template.Clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTemplateName(fileName string) (emailtypes.TemplateName, error) {
|
||||||
|
name := strings.TrimSuffix(fileName, emailTemplateExt)
|
||||||
|
return emailtypes.NewTemplateName(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTemplateFile(filePath string, templateName emailtypes.TemplateName) (*template.Template, error) {
|
||||||
|
contents, err := os.ReadFile(filepath.Clean(filePath))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return template.New(templateName.StringValue()).Parse(string(contents))
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkMissingTemplates(supportedTemplates []emailtypes.TemplateName, foundTemplates map[emailtypes.TemplateName]bool) error {
|
||||||
|
var missingTemplates []string
|
||||||
|
for _, template := range supportedTemplates {
|
||||||
|
if !foundTemplates[template] {
|
||||||
|
missingTemplates = append(missingTemplates, template.StringValue())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(missingTemplates) > 0 {
|
||||||
|
return errors.Newf(errors.TypeNotFound, errors.CodeNotFound, "missing email templates: %s", strings.Join(missingTemplates, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@ -1,35 +1,38 @@
|
|||||||
package impluser
|
package impluser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"slices"
|
"slices"
|
||||||
"text/template"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/emailing"
|
||||||
"github.com/SigNoz/signoz/pkg/errors"
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||||
"github.com/SigNoz/signoz/pkg/query-service/constants"
|
|
||||||
"github.com/SigNoz/signoz/pkg/query-service/telemetry"
|
"github.com/SigNoz/signoz/pkg/query-service/telemetry"
|
||||||
smtpservice "github.com/SigNoz/signoz/pkg/query-service/utils/smtpService"
|
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
"github.com/SigNoz/signoz/pkg/types"
|
||||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/emailtypes"
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Module struct {
|
type Module struct {
|
||||||
store types.UserStore
|
store types.UserStore
|
||||||
JWT *authtypes.JWT
|
jwt *authtypes.JWT
|
||||||
|
emailing emailing.Emailing
|
||||||
|
settings factory.ScopedProviderSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
// This module is a WIP, don't take inspiration from this.
|
// This module is a WIP, don't take inspiration from this.
|
||||||
func NewModule(store types.UserStore) user.Module {
|
func NewModule(store types.UserStore, jwt *authtypes.JWT, emailing emailing.Emailing, providerSettings factory.ProviderSettings) user.Module {
|
||||||
jwtSecret := os.Getenv("SIGNOZ_JWT_SECRET")
|
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/modules/user/impluser")
|
||||||
jwt := authtypes.NewJWT(jwtSecret, 30*time.Minute, 30*24*time.Hour)
|
return &Module{
|
||||||
return &Module{store: store, JWT: jwt}
|
store: store,
|
||||||
|
jwt: jwt,
|
||||||
|
emailing: emailing,
|
||||||
|
settings: settings,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateBulk implements invite.Module.
|
// CreateBulk implements invite.Module.
|
||||||
@ -84,47 +87,20 @@ func (m *Module) CreateBulkInvite(ctx context.Context, orgID, userID string, bul
|
|||||||
"invited user email": invites[i].Email,
|
"invited user email": invites[i].Email,
|
||||||
}, creator.Email, true, false)
|
}, creator.Email, true, false)
|
||||||
|
|
||||||
// send email if SMTP is enabled
|
if err := m.emailing.SendHTML(ctx, invites[i].Email, "You are invited to join a team in SigNoz", emailtypes.TemplateNameInvitationEmail, map[string]any{
|
||||||
if os.Getenv("SMTP_ENABLED") == "true" && bulkInvites.Invites[i].FrontendBaseUrl != "" {
|
"CustomerName": invites[i].Name,
|
||||||
m.inviteEmail(&bulkInvites.Invites[i], creator.Email, creator.DisplayName, invites[i].Token)
|
"InviterName": creator.DisplayName,
|
||||||
|
"InviterEmail": creator.Email,
|
||||||
|
"Link": fmt.Sprintf("%s/signup?token=%s", bulkInvites.Invites[i].FrontendBaseUrl, invites[i].Token),
|
||||||
|
}); err != nil {
|
||||||
|
m.settings.Logger().ErrorContext(ctx, "failed to send email", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return invites, nil
|
return invites, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Module) inviteEmail(req *types.PostableInvite, creatorEmail, creatorName, token string) {
|
|
||||||
smtp := smtpservice.GetInstance()
|
|
||||||
data := types.InviteEmailData{
|
|
||||||
CustomerName: req.Name,
|
|
||||||
InviterName: creatorName,
|
|
||||||
InviterEmail: creatorEmail,
|
|
||||||
Link: fmt.Sprintf("%s/signup?token=%s", req.FrontendBaseUrl, token),
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpl, err := template.ParseFiles(constants.InviteEmailTemplate)
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Error("failed to send email", zap.Error(err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var body bytes.Buffer
|
|
||||||
if err := tmpl.Execute(&body, data); err != nil {
|
|
||||||
zap.L().Error("failed to send email", zap.Error(err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = smtp.SendEmail(
|
|
||||||
req.Email,
|
|
||||||
creatorName+" has invited you to their team in SigNoz",
|
|
||||||
body.String(),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Error("failed to send email", zap.Error(err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Module) ListInvite(ctx context.Context, orgID string) ([]*types.Invite, error) {
|
func (m *Module) ListInvite(ctx context.Context, orgID string) ([]*types.Invite, error) {
|
||||||
return m.store.ListInvite(ctx, orgID)
|
return m.store.ListInvite(ctx, orgID)
|
||||||
}
|
}
|
||||||
@ -282,7 +258,7 @@ func (m *Module) UpdatePassword(ctx context.Context, userID string, password str
|
|||||||
func (m *Module) GetAuthenticatedUser(ctx context.Context, orgID, email, password, refreshToken string) (*types.User, error) {
|
func (m *Module) GetAuthenticatedUser(ctx context.Context, orgID, email, password, refreshToken string) (*types.User, error) {
|
||||||
if refreshToken != "" {
|
if refreshToken != "" {
|
||||||
// parse the refresh token
|
// parse the refresh token
|
||||||
claims, err := m.JWT.Claims(refreshToken)
|
claims, err := m.jwt.Claims(refreshToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -361,12 +337,12 @@ func (m *Module) GetJWTForUser(ctx context.Context, user *types.User) (types.Get
|
|||||||
return types.GettableUserJwt{}, err
|
return types.GettableUserJwt{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
accessJwt, accessClaims, err := m.JWT.AccessToken(user.OrgID, user.ID.String(), user.Email, role)
|
accessJwt, accessClaims, err := m.jwt.AccessToken(user.OrgID, user.ID.String(), user.Email, role)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return types.GettableUserJwt{}, err
|
return types.GettableUserJwt{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshJwt, refreshClaims, err := m.JWT.RefreshToken(user.OrgID, user.ID.String(), user.Email, role)
|
refreshJwt, refreshClaims, err := m.jwt.RefreshToken(user.OrgID, user.ID.String(), user.Email, role)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return types.GettableUserJwt{}, err
|
return types.GettableUserJwt{}, err
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/emailing"
|
||||||
|
"github.com/SigNoz/signoz/pkg/emailing/noopemailing"
|
||||||
|
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||||
@ -22,7 +25,9 @@ func TestRegenerateConnectionUrlWithUpdatedConfig(t *testing.T) {
|
|||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
|
|
||||||
organizationModule := implorganization.NewModule(implorganization.NewStore(sqlStore))
|
organizationModule := implorganization.NewModule(implorganization.NewStore(sqlStore))
|
||||||
userModule := impluser.NewModule(impluser.NewStore(sqlStore))
|
providerSettings := instrumentationtest.New().ToProviderSettings()
|
||||||
|
emailing, _ := noopemailing.New(context.Background(), providerSettings, emailing.Config{})
|
||||||
|
userModule := impluser.NewModule(impluser.NewStore(sqlStore), nil, emailing, providerSettings)
|
||||||
user, apiErr := createTestUser(organizationModule, userModule)
|
user, apiErr := createTestUser(organizationModule, userModule)
|
||||||
require.Nil(apiErr)
|
require.Nil(apiErr)
|
||||||
|
|
||||||
@ -70,7 +75,9 @@ func TestAgentCheckIns(t *testing.T) {
|
|||||||
controller, err := NewController(sqlStore)
|
controller, err := NewController(sqlStore)
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
organizationModule := implorganization.NewModule(implorganization.NewStore(sqlStore))
|
organizationModule := implorganization.NewModule(implorganization.NewStore(sqlStore))
|
||||||
userModule := impluser.NewModule(impluser.NewStore(sqlStore))
|
providerSettings := instrumentationtest.New().ToProviderSettings()
|
||||||
|
emailing, _ := noopemailing.New(context.Background(), providerSettings, emailing.Config{})
|
||||||
|
userModule := impluser.NewModule(impluser.NewStore(sqlStore), nil, emailing, providerSettings)
|
||||||
user, apiErr := createTestUser(organizationModule, userModule)
|
user, apiErr := createTestUser(organizationModule, userModule)
|
||||||
require.Nil(apiErr)
|
require.Nil(apiErr)
|
||||||
|
|
||||||
@ -158,7 +165,9 @@ func TestCantDisconnectNonExistentAccount(t *testing.T) {
|
|||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
|
|
||||||
organizationModule := implorganization.NewModule(implorganization.NewStore(sqlStore))
|
organizationModule := implorganization.NewModule(implorganization.NewStore(sqlStore))
|
||||||
userModule := impluser.NewModule(impluser.NewStore(sqlStore))
|
providerSettings := instrumentationtest.New().ToProviderSettings()
|
||||||
|
emailing, _ := noopemailing.New(context.Background(), providerSettings, emailing.Config{})
|
||||||
|
userModule := impluser.NewModule(impluser.NewStore(sqlStore), nil, emailing, providerSettings)
|
||||||
user, apiErr := createTestUser(organizationModule, userModule)
|
user, apiErr := createTestUser(organizationModule, userModule)
|
||||||
require.Nil(apiErr)
|
require.Nil(apiErr)
|
||||||
|
|
||||||
@ -178,7 +187,9 @@ func TestConfigureService(t *testing.T) {
|
|||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
|
|
||||||
organizationModule := implorganization.NewModule(implorganization.NewStore(sqlStore))
|
organizationModule := implorganization.NewModule(implorganization.NewStore(sqlStore))
|
||||||
userModule := impluser.NewModule(impluser.NewStore(sqlStore))
|
providerSettings := instrumentationtest.New().ToProviderSettings()
|
||||||
|
emailing, _ := noopemailing.New(context.Background(), providerSettings, emailing.Config{})
|
||||||
|
userModule := impluser.NewModule(impluser.NewStore(sqlStore), nil, emailing, providerSettings)
|
||||||
user, apiErr := createTestUser(organizationModule, userModule)
|
user, apiErr := createTestUser(organizationModule, userModule)
|
||||||
require.Nil(apiErr)
|
require.Nil(apiErr)
|
||||||
|
|
||||||
|
@ -4,6 +4,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/emailing"
|
||||||
|
"github.com/SigNoz/signoz/pkg/emailing/noopemailing"
|
||||||
|
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
@ -17,7 +20,9 @@ func TestIntegrationLifecycle(t *testing.T) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
organizationModule := implorganization.NewModule(implorganization.NewStore(store))
|
organizationModule := implorganization.NewModule(implorganization.NewStore(store))
|
||||||
userModule := impluser.NewModule(impluser.NewStore(store))
|
providerSettings := instrumentationtest.New().ToProviderSettings()
|
||||||
|
emailing, _ := noopemailing.New(context.Background(), providerSettings, emailing.Config{})
|
||||||
|
userModule := impluser.NewModule(impluser.NewStore(store), nil, emailing, providerSettings)
|
||||||
user, apiErr := createTestUser(organizationModule, userModule)
|
user, apiErr := createTestUser(organizationModule, userModule)
|
||||||
if apiErr != nil {
|
if apiErr != nil {
|
||||||
t.Fatalf("could not create test user: %v", apiErr)
|
t.Fatalf("could not create test user: %v", apiErr)
|
||||||
|
@ -52,7 +52,8 @@ var TELEMETRY_HEART_BEAT_DURATION_MINUTES = GetOrDefaultEnvInt("TELEMETRY_HEART_
|
|||||||
|
|
||||||
var TELEMETRY_ACTIVE_USER_DURATION_MINUTES = GetOrDefaultEnvInt("TELEMETRY_ACTIVE_USER_DURATION_MINUTES", 360)
|
var TELEMETRY_ACTIVE_USER_DURATION_MINUTES = GetOrDefaultEnvInt("TELEMETRY_ACTIVE_USER_DURATION_MINUTES", 360)
|
||||||
|
|
||||||
var InviteEmailTemplate = GetOrDefaultEnv("INVITE_EMAIL_TEMPLATE", "/root/templates/invitation_email_template.html")
|
// Deprecated: Use the new emailing service instead
|
||||||
|
var InviteEmailTemplate = GetOrDefaultEnv("INVITE_EMAIL_TEMPLATE", "/root/templates/invitation_email.gotmpl")
|
||||||
|
|
||||||
var MetricsExplorerClickhouseThreads = GetOrDefaultEnvInt("METRICS_EXPLORER_CLICKHOUSE_THREADS", 8)
|
var MetricsExplorerClickhouseThreads = GetOrDefaultEnvInt("METRICS_EXPLORER_CLICKHOUSE_THREADS", 8)
|
||||||
var UpdatedMetricsMetadataCachePrefix = GetOrDefaultEnv("METRICS_UPDATED_METADATA_CACHE_KEY", "UPDATED_METRICS_METADATA")
|
var UpdatedMetricsMetadataCachePrefix = GetOrDefaultEnv("METRICS_UPDATED_METADATA_CACHE_KEY", "UPDATED_METRICS_METADATA")
|
||||||
|
@ -9,6 +9,8 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/config"
|
"github.com/SigNoz/signoz/pkg/config"
|
||||||
"github.com/SigNoz/signoz/pkg/config/envprovider"
|
"github.com/SigNoz/signoz/pkg/config/envprovider"
|
||||||
"github.com/SigNoz/signoz/pkg/config/fileprovider"
|
"github.com/SigNoz/signoz/pkg/config/fileprovider"
|
||||||
|
"github.com/SigNoz/signoz/pkg/emailing"
|
||||||
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
||||||
"github.com/SigNoz/signoz/pkg/query-service/app"
|
"github.com/SigNoz/signoz/pkg/query-service/app"
|
||||||
@ -102,26 +104,6 @@ func main() {
|
|||||||
|
|
||||||
version.Info.PrettyPrint(config.Version)
|
version.Info.PrettyPrint(config.Version)
|
||||||
|
|
||||||
signoz, err := signoz.New(
|
|
||||||
context.Background(),
|
|
||||||
config,
|
|
||||||
zeus.Config{},
|
|
||||||
noopzeus.NewProviderFactory(),
|
|
||||||
signoz.NewCacheProviderFactories(),
|
|
||||||
signoz.NewWebProviderFactories(),
|
|
||||||
signoz.NewSQLStoreProviderFactories(),
|
|
||||||
signoz.NewTelemetryStoreProviderFactories(),
|
|
||||||
func(sqlstore sqlstore.SQLStore) user.Module {
|
|
||||||
return impluser.NewModule(impluser.NewStore(sqlstore))
|
|
||||||
},
|
|
||||||
func(userModule user.Module) user.Handler {
|
|
||||||
return impluser.NewHandler(userModule)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Fatal("Failed to create signoz", zap.Error(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the jwt secret key
|
// Read the jwt secret key
|
||||||
jwtSecret := os.Getenv("SIGNOZ_JWT_SECRET")
|
jwtSecret := os.Getenv("SIGNOZ_JWT_SECRET")
|
||||||
|
|
||||||
@ -133,6 +115,27 @@ func main() {
|
|||||||
|
|
||||||
jwt := authtypes.NewJWT(jwtSecret, 30*time.Minute, 30*24*time.Hour)
|
jwt := authtypes.NewJWT(jwtSecret, 30*time.Minute, 30*24*time.Hour)
|
||||||
|
|
||||||
|
signoz, err := signoz.New(
|
||||||
|
context.Background(),
|
||||||
|
config,
|
||||||
|
zeus.Config{},
|
||||||
|
noopzeus.NewProviderFactory(),
|
||||||
|
signoz.NewEmailingProviderFactories(),
|
||||||
|
signoz.NewCacheProviderFactories(),
|
||||||
|
signoz.NewWebProviderFactories(),
|
||||||
|
signoz.NewSQLStoreProviderFactories(),
|
||||||
|
signoz.NewTelemetryStoreProviderFactories(),
|
||||||
|
func(sqlstore sqlstore.SQLStore, emailing emailing.Emailing, providerSettings factory.ProviderSettings) user.Module {
|
||||||
|
return impluser.NewModule(impluser.NewStore(sqlstore), jwt, emailing, providerSettings)
|
||||||
|
},
|
||||||
|
func(userModule user.Module) user.Handler {
|
||||||
|
return impluser.NewHandler(userModule)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
zap.L().Fatal("Failed to create signoz", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
serverOptions := &app.ServerOptions{
|
serverOptions := &app.ServerOptions{
|
||||||
Config: config,
|
Config: config,
|
||||||
HTTPHostPort: constants.HTTPHostPort,
|
HTTPHostPort: constants.HTTPHostPort,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package tests
|
package tests
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -8,9 +9,13 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/emailing"
|
||||||
|
"github.com/SigNoz/signoz/pkg/emailing/noopemailing"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
|
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
|
||||||
quickfilterscore "github.com/SigNoz/signoz/pkg/modules/quickfilter/core"
|
quickfilterscore "github.com/SigNoz/signoz/pkg/modules/quickfilter/core"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/http/middleware"
|
"github.com/SigNoz/signoz/pkg/http/middleware"
|
||||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||||
@ -303,7 +308,10 @@ func NewFilterSuggestionsTestBed(t *testing.T) *FilterSuggestionsTestBed {
|
|||||||
reader, mockClickhouse := NewMockClickhouseReader(t, testDB)
|
reader, mockClickhouse := NewMockClickhouseReader(t, testDB)
|
||||||
mockClickhouse.MatchExpectationsInOrder(false)
|
mockClickhouse.MatchExpectationsInOrder(false)
|
||||||
|
|
||||||
userModule := impluser.NewModule(impluser.NewStore(testDB))
|
providerSettings := instrumentationtest.New().ToProviderSettings()
|
||||||
|
emailing, _ := noopemailing.New(context.Background(), providerSettings, emailing.Config{})
|
||||||
|
jwt := authtypes.NewJWT("", 1*time.Hour, 1*time.Hour)
|
||||||
|
userModule := impluser.NewModule(impluser.NewStore(testDB), jwt, emailing, providerSettings)
|
||||||
userHandler := impluser.NewHandler(userModule)
|
userHandler := impluser.NewHandler(userModule)
|
||||||
modules := signoz.NewModules(testDB, userModule)
|
modules := signoz.NewModules(testDB, userModule)
|
||||||
quickFilterModule := quickfilter.NewAPI(quickfilterscore.NewQuickFilters(quickfilterscore.NewStore(testDB)))
|
quickFilterModule := quickfilter.NewAPI(quickfilterscore.NewQuickFilters(quickfilterscore.NewStore(testDB)))
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package tests
|
package tests
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@ -8,7 +9,11 @@ import (
|
|||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/emailing"
|
||||||
|
"github.com/SigNoz/signoz/pkg/emailing/noopemailing"
|
||||||
|
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
|
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
|
||||||
quickfilterscore "github.com/SigNoz/signoz/pkg/modules/quickfilter/core"
|
quickfilterscore "github.com/SigNoz/signoz/pkg/modules/quickfilter/core"
|
||||||
@ -27,6 +32,7 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/signoz"
|
"github.com/SigNoz/signoz/pkg/signoz"
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
"github.com/SigNoz/signoz/pkg/types"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||||
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
|
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
@ -476,7 +482,10 @@ func NewTestbedWithoutOpamp(t *testing.T, sqlStore sqlstore.SQLStore) *LogPipeli
|
|||||||
t.Fatalf("could not create a logparsingpipelines controller: %v", err)
|
t.Fatalf("could not create a logparsingpipelines controller: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
userModule := impluser.NewModule(impluser.NewStore(sqlStore))
|
providerSettings := instrumentationtest.New().ToProviderSettings()
|
||||||
|
emailing, _ := noopemailing.New(context.Background(), providerSettings, emailing.Config{})
|
||||||
|
jwt := authtypes.NewJWT("", 10*time.Minute, 30*time.Minute)
|
||||||
|
userModule := impluser.NewModule(impluser.NewStore(sqlStore), jwt, emailing, providerSettings)
|
||||||
userHandler := impluser.NewHandler(userModule)
|
userHandler := impluser.NewHandler(userModule)
|
||||||
modules := signoz.NewModules(sqlStore, userModule)
|
modules := signoz.NewModules(sqlStore, userModule)
|
||||||
handlers := signoz.NewHandlers(modules, userHandler)
|
handlers := signoz.NewHandlers(modules, userHandler)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package tests
|
package tests
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -8,8 +9,11 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/emailing"
|
||||||
|
"github.com/SigNoz/signoz/pkg/emailing/noopemailing"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
|
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
|
||||||
quickfilterscore "github.com/SigNoz/signoz/pkg/modules/quickfilter/core"
|
quickfilterscore "github.com/SigNoz/signoz/pkg/modules/quickfilter/core"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/http/middleware"
|
"github.com/SigNoz/signoz/pkg/http/middleware"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
||||||
@ -366,7 +370,10 @@ func NewCloudIntegrationsTestBed(t *testing.T, testDB sqlstore.SQLStore) *CloudI
|
|||||||
reader, mockClickhouse := NewMockClickhouseReader(t, testDB)
|
reader, mockClickhouse := NewMockClickhouseReader(t, testDB)
|
||||||
mockClickhouse.MatchExpectationsInOrder(false)
|
mockClickhouse.MatchExpectationsInOrder(false)
|
||||||
|
|
||||||
userModule := impluser.NewModule(impluser.NewStore(testDB))
|
providerSettings := instrumentationtest.New().ToProviderSettings()
|
||||||
|
emailing, _ := noopemailing.New(context.Background(), providerSettings, emailing.Config{})
|
||||||
|
jwt := authtypes.NewJWT("", 10*time.Minute, 30*time.Minute)
|
||||||
|
userModule := impluser.NewModule(impluser.NewStore(testDB), jwt, emailing, providerSettings)
|
||||||
userHandler := impluser.NewHandler(userModule)
|
userHandler := impluser.NewHandler(userModule)
|
||||||
modules := signoz.NewModules(testDB, userModule)
|
modules := signoz.NewModules(testDB, userModule)
|
||||||
handlers := signoz.NewHandlers(modules, userHandler)
|
handlers := signoz.NewHandlers(modules, userHandler)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package tests
|
package tests
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -8,6 +9,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/emailing"
|
||||||
|
"github.com/SigNoz/signoz/pkg/emailing/noopemailing"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
|
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
|
||||||
quickfilterscore "github.com/SigNoz/signoz/pkg/modules/quickfilter/core"
|
quickfilterscore "github.com/SigNoz/signoz/pkg/modules/quickfilter/core"
|
||||||
|
|
||||||
@ -26,6 +29,7 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/signoz"
|
"github.com/SigNoz/signoz/pkg/signoz"
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
"github.com/SigNoz/signoz/pkg/types"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||||
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
|
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
|
||||||
mockhouse "github.com/srikanthccv/ClickHouse-go-mock"
|
mockhouse "github.com/srikanthccv/ClickHouse-go-mock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@ -572,7 +576,10 @@ func NewIntegrationsTestBed(t *testing.T, testDB sqlstore.SQLStore) *Integration
|
|||||||
t.Fatalf("could not create cloud integrations controller: %v", err)
|
t.Fatalf("could not create cloud integrations controller: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
userModule := impluser.NewModule(impluser.NewStore(testDB))
|
providerSettings := instrumentationtest.New().ToProviderSettings()
|
||||||
|
emailing, _ := noopemailing.New(context.Background(), providerSettings, emailing.Config{})
|
||||||
|
jwt := authtypes.NewJWT("", 10*time.Minute, 30*time.Minute)
|
||||||
|
userModule := impluser.NewModule(impluser.NewStore(testDB), jwt, emailing, providerSettings)
|
||||||
userHandler := impluser.NewHandler(userModule)
|
userHandler := impluser.NewHandler(userModule)
|
||||||
modules := signoz.NewModules(testDB, userModule)
|
modules := signoz.NewModules(testDB, userModule)
|
||||||
handlers := signoz.NewHandlers(modules, userHandler)
|
handlers := signoz.NewHandlers(modules, userHandler)
|
||||||
|
@ -1,57 +0,0 @@
|
|||||||
package smtpservice
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/smtp"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SMTP struct {
|
|
||||||
Host string
|
|
||||||
Port string
|
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
From string
|
|
||||||
}
|
|
||||||
|
|
||||||
var smtpInstance *SMTP
|
|
||||||
var once sync.Once
|
|
||||||
|
|
||||||
func New() *SMTP {
|
|
||||||
return &SMTP{
|
|
||||||
Host: os.Getenv("SMTP_HOST"),
|
|
||||||
Port: os.Getenv("SMTP_PORT"),
|
|
||||||
Username: os.Getenv("SMTP_USERNAME"),
|
|
||||||
Password: os.Getenv("SMTP_PASSWORD"),
|
|
||||||
From: os.Getenv("SMTP_FROM"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetInstance() *SMTP {
|
|
||||||
once.Do(func() {
|
|
||||||
smtpInstance = New()
|
|
||||||
})
|
|
||||||
return smtpInstance
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SMTP) SendEmail(to, subject, body string) error {
|
|
||||||
|
|
||||||
msgString := "From: " + s.From + "\r\n" +
|
|
||||||
"To: " + to + "\r\n" +
|
|
||||||
"Subject: " + subject + "\r\n" +
|
|
||||||
"MIME-Version: 1.0\r\n" +
|
|
||||||
"Content-Type: text/html; charset=UTF-8\r\n" +
|
|
||||||
"\r\n" +
|
|
||||||
body
|
|
||||||
|
|
||||||
msg := []byte(msgString)
|
|
||||||
|
|
||||||
addr := s.Host + ":" + s.Port
|
|
||||||
if s.Password == "" || s.Username == "" {
|
|
||||||
return smtp.SendMail(addr, nil, s.From, strings.Split(to, ","), msg)
|
|
||||||
} else {
|
|
||||||
auth := smtp.PlainAuth("", s.Username, s.Password, s.Host)
|
|
||||||
return smtp.SendMail(addr, auth, s.From, strings.Split(to, ","), msg)
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"reflect"
|
"reflect"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -12,6 +13,7 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/apiserver"
|
"github.com/SigNoz/signoz/pkg/apiserver"
|
||||||
"github.com/SigNoz/signoz/pkg/cache"
|
"github.com/SigNoz/signoz/pkg/cache"
|
||||||
"github.com/SigNoz/signoz/pkg/config"
|
"github.com/SigNoz/signoz/pkg/config"
|
||||||
|
"github.com/SigNoz/signoz/pkg/emailing"
|
||||||
"github.com/SigNoz/signoz/pkg/factory"
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
"github.com/SigNoz/signoz/pkg/instrumentation"
|
"github.com/SigNoz/signoz/pkg/instrumentation"
|
||||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||||
@ -57,6 +59,9 @@ type Config struct {
|
|||||||
|
|
||||||
// Alertmanager config
|
// Alertmanager config
|
||||||
Alertmanager alertmanager.Config `mapstructure:"alertmanager" yaml:"alertmanager"`
|
Alertmanager alertmanager.Config `mapstructure:"alertmanager" yaml:"alertmanager"`
|
||||||
|
|
||||||
|
// Emailing config
|
||||||
|
Emailing emailing.Config `mapstructure:"emailing" yaml:"emailing"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeprecatedFlags are the flags that are deprecated and scheduled for removal.
|
// DeprecatedFlags are the flags that are deprecated and scheduled for removal.
|
||||||
@ -80,6 +85,7 @@ func NewConfig(ctx context.Context, resolverConfig config.ResolverConfig, deprec
|
|||||||
telemetrystore.NewConfigFactory(),
|
telemetrystore.NewConfigFactory(),
|
||||||
prometheus.NewConfigFactory(),
|
prometheus.NewConfigFactory(),
|
||||||
alertmanager.NewConfigFactory(),
|
alertmanager.NewConfigFactory(),
|
||||||
|
emailing.NewConfigFactory(),
|
||||||
}
|
}
|
||||||
|
|
||||||
conf, err := config.New(ctx, resolverConfig, configFactories)
|
conf, err := config.New(ctx, resolverConfig, configFactories)
|
||||||
@ -186,4 +192,42 @@ func mergeAndEnsureBackwardCompatibility(config *Config, deprecatedFlags Depreca
|
|||||||
if deprecatedFlags.Config != "" {
|
if deprecatedFlags.Config != "" {
|
||||||
fmt.Println("[Deprecated] flag --config is deprecated for passing prometheus config. The flag will be used for passing the entire SigNoz config. More details can be found at https://github.com/SigNoz/signoz/issues/6805.")
|
fmt.Println("[Deprecated] flag --config is deprecated for passing prometheus config. The flag will be used for passing the entire SigNoz config. More details can be found at https://github.com/SigNoz/signoz/issues/6805.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if os.Getenv("INVITE_EMAIL_TEMPLATE") != "" {
|
||||||
|
fmt.Println("[Deprecated] env INVITE_EMAIL_TEMPLATE is deprecated and scheduled for removal. Please use SIGNOZ_EMAILING_TEMPLATES_DIRECTORY instead.")
|
||||||
|
config.Emailing.Templates.Directory = path.Dir(os.Getenv("INVITE_EMAIL_TEMPLATE"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.Getenv("SMTP_ENABLED") != "" {
|
||||||
|
fmt.Println("[Deprecated] env SMTP_ENABLED is deprecated and scheduled for removal. Please use SIGNOZ_EMAILING_ENABLED instead.")
|
||||||
|
config.Emailing.Enabled = os.Getenv("SMTP_ENABLED") == "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.Getenv("SMTP_HOST") != "" {
|
||||||
|
fmt.Println("[Deprecated] env SMTP_HOST is deprecated and scheduled for removal. Please use SIGNOZ_EMAILING_ADDRESS instead.")
|
||||||
|
if os.Getenv("SMTP_PORT") != "" {
|
||||||
|
config.Emailing.SMTP.Address = os.Getenv("SMTP_HOST") + ":" + os.Getenv("SMTP_PORT")
|
||||||
|
} else {
|
||||||
|
config.Emailing.SMTP.Address = os.Getenv("SMTP_HOST")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.Getenv("SMTP_PORT") != "" {
|
||||||
|
fmt.Println("[Deprecated] env SMTP_PORT is deprecated and scheduled for removal. Please use SIGNOZ_EMAILING_ADDRESS instead.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.Getenv("SMTP_USERNAME") != "" {
|
||||||
|
fmt.Println("[Deprecated] env SMTP_USERNAME is deprecated and scheduled for removal. Please use SIGNOZ_EMAILING_AUTH_USERNAME instead.")
|
||||||
|
config.Emailing.SMTP.Auth.Username = os.Getenv("SMTP_USERNAME")
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.Getenv("SMTP_PASSWORD") != "" {
|
||||||
|
fmt.Println("[Deprecated] env SMTP_PASSWORD is deprecated and scheduled for removal. Please use SIGNOZ_EMAILING_AUTH_PASSWORD instead.")
|
||||||
|
config.Emailing.SMTP.Auth.Password = os.Getenv("SMTP_PASSWORD")
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.Getenv("SMTP_FROM") != "" {
|
||||||
|
fmt.Println("[Deprecated] env SMTP_FROM is deprecated and scheduled for removal. Please use SIGNOZ_EMAILING_FROM instead.")
|
||||||
|
config.Emailing.SMTP.From = os.Getenv("SMTP_FROM")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,9 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/cache"
|
"github.com/SigNoz/signoz/pkg/cache"
|
||||||
"github.com/SigNoz/signoz/pkg/cache/memorycache"
|
"github.com/SigNoz/signoz/pkg/cache/memorycache"
|
||||||
"github.com/SigNoz/signoz/pkg/cache/rediscache"
|
"github.com/SigNoz/signoz/pkg/cache/rediscache"
|
||||||
|
"github.com/SigNoz/signoz/pkg/emailing"
|
||||||
|
"github.com/SigNoz/signoz/pkg/emailing/noopemailing"
|
||||||
|
"github.com/SigNoz/signoz/pkg/emailing/smtpemailing"
|
||||||
"github.com/SigNoz/signoz/pkg/factory"
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||||
"github.com/SigNoz/signoz/pkg/prometheus/clickhouseprometheus"
|
"github.com/SigNoz/signoz/pkg/prometheus/clickhouseprometheus"
|
||||||
@ -99,3 +102,10 @@ func NewAlertmanagerProviderFactories(sqlstore sqlstore.SQLStore) factory.NamedM
|
|||||||
signozalertmanager.NewFactory(sqlstore),
|
signozalertmanager.NewFactory(sqlstore),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewEmailingProviderFactories() factory.NamedMap[factory.ProviderFactory[emailing.Emailing, emailing.Config]] {
|
||||||
|
return factory.MustNewNamedMap(
|
||||||
|
noopemailing.NewFactory(),
|
||||||
|
smtpemailing.NewFactory(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -42,4 +42,8 @@ func TestNewProviderFactories(t *testing.T) {
|
|||||||
assert.NotPanics(t, func() {
|
assert.NotPanics(t, func() {
|
||||||
NewAlertmanagerProviderFactories(sqlstoretest.New(sqlstore.Config{Provider: "sqlite"}, sqlmock.QueryMatcherEqual))
|
NewAlertmanagerProviderFactories(sqlstoretest.New(sqlstore.Config{Provider: "sqlite"}, sqlmock.QueryMatcherEqual))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
assert.NotPanics(t, func() {
|
||||||
|
NewEmailingProviderFactories()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/alertmanager"
|
"github.com/SigNoz/signoz/pkg/alertmanager"
|
||||||
"github.com/SigNoz/signoz/pkg/cache"
|
"github.com/SigNoz/signoz/pkg/cache"
|
||||||
|
"github.com/SigNoz/signoz/pkg/emailing"
|
||||||
"github.com/SigNoz/signoz/pkg/factory"
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
"github.com/SigNoz/signoz/pkg/instrumentation"
|
"github.com/SigNoz/signoz/pkg/instrumentation"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||||
@ -29,6 +30,7 @@ type SigNoz struct {
|
|||||||
Prometheus prometheus.Prometheus
|
Prometheus prometheus.Prometheus
|
||||||
Alertmanager alertmanager.Alertmanager
|
Alertmanager alertmanager.Alertmanager
|
||||||
Zeus zeus.Zeus
|
Zeus zeus.Zeus
|
||||||
|
Emailing emailing.Emailing
|
||||||
Modules Modules
|
Modules Modules
|
||||||
Handlers Handlers
|
Handlers Handlers
|
||||||
}
|
}
|
||||||
@ -38,12 +40,13 @@ func New(
|
|||||||
config Config,
|
config Config,
|
||||||
zeusConfig zeus.Config,
|
zeusConfig zeus.Config,
|
||||||
zeusProviderFactory factory.ProviderFactory[zeus.Zeus, zeus.Config],
|
zeusProviderFactory factory.ProviderFactory[zeus.Zeus, zeus.Config],
|
||||||
|
emailingProviderFactories factory.NamedMap[factory.ProviderFactory[emailing.Emailing, emailing.Config]],
|
||||||
cacheProviderFactories factory.NamedMap[factory.ProviderFactory[cache.Cache, cache.Config]],
|
cacheProviderFactories factory.NamedMap[factory.ProviderFactory[cache.Cache, cache.Config]],
|
||||||
webProviderFactories factory.NamedMap[factory.ProviderFactory[web.Web, web.Config]],
|
webProviderFactories factory.NamedMap[factory.ProviderFactory[web.Web, web.Config]],
|
||||||
sqlstoreProviderFactories factory.NamedMap[factory.ProviderFactory[sqlstore.SQLStore, sqlstore.Config]],
|
sqlstoreProviderFactories factory.NamedMap[factory.ProviderFactory[sqlstore.SQLStore, sqlstore.Config]],
|
||||||
telemetrystoreProviderFactories factory.NamedMap[factory.ProviderFactory[telemetrystore.TelemetryStore, telemetrystore.Config]],
|
telemetrystoreProviderFactories factory.NamedMap[factory.ProviderFactory[telemetrystore.TelemetryStore, telemetrystore.Config]],
|
||||||
diModules func(sqlstore.SQLStore) user.Module,
|
userModuleFactory func(sqlstore sqlstore.SQLStore, emailing emailing.Emailing, providerSettings factory.ProviderSettings) user.Module,
|
||||||
diHandlers func(user.Module) user.Handler,
|
userHandlerFactory func(user.Module) user.Handler,
|
||||||
) (*SigNoz, error) {
|
) (*SigNoz, error) {
|
||||||
// Initialize instrumentation
|
// Initialize instrumentation
|
||||||
instrumentation, err := instrumentation.New(ctx, config.Instrumentation, version.Info, "signoz")
|
instrumentation, err := instrumentation.New(ctx, config.Instrumentation, version.Info, "signoz")
|
||||||
@ -68,6 +71,18 @@ func New(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize emailing from the available emailing provider factories
|
||||||
|
emailing, err := factory.NewProviderFromNamedMap(
|
||||||
|
ctx,
|
||||||
|
providerSettings,
|
||||||
|
config.Emailing,
|
||||||
|
emailingProviderFactories,
|
||||||
|
config.Emailing.Provider(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize cache from the available cache provider factories
|
// Initialize cache from the available cache provider factories
|
||||||
cache, err := factory.NewProviderFromNamedMap(
|
cache, err := factory.NewProviderFromNamedMap(
|
||||||
ctx,
|
ctx,
|
||||||
@ -156,8 +171,8 @@ func New(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
userModule := diModules(sqlstore)
|
userModule := userModuleFactory(sqlstore, emailing, providerSettings)
|
||||||
userHandler := diHandlers(userModule)
|
userHandler := userHandlerFactory(userModule)
|
||||||
|
|
||||||
// Initialize all modules
|
// Initialize all modules
|
||||||
modules := NewModules(sqlstore, userModule)
|
modules := NewModules(sqlstore, userModule)
|
||||||
@ -184,6 +199,7 @@ func New(
|
|||||||
Prometheus: prometheus,
|
Prometheus: prometheus,
|
||||||
Alertmanager: alertmanager,
|
Alertmanager: alertmanager,
|
||||||
Zeus: zeus,
|
Zeus: zeus,
|
||||||
|
Emailing: emailing,
|
||||||
Modules: modules,
|
Modules: modules,
|
||||||
Handlers: handlers,
|
Handlers: handlers,
|
||||||
}, nil
|
}, nil
|
||||||
|
34
pkg/smtp/client/auth.go
Normal file
34
pkg/smtp/client/auth.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/smtp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type loginAuth struct {
|
||||||
|
username string
|
||||||
|
password string
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoginAuth(username, password string) smtp.Auth {
|
||||||
|
return &loginAuth{username, password}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
|
||||||
|
return "LOGIN", []byte{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
|
||||||
|
if more {
|
||||||
|
switch strings.ToLower(string(fromServer)) {
|
||||||
|
case "username:":
|
||||||
|
return []byte(auth.username), nil
|
||||||
|
case "password:":
|
||||||
|
return []byte(auth.password), nil
|
||||||
|
default:
|
||||||
|
return nil, errors.New("unexpected server challenge")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
10
pkg/smtp/client/content.go
Normal file
10
pkg/smtp/client/content.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import "github.com/SigNoz/signoz/pkg/valuer"
|
||||||
|
|
||||||
|
var (
|
||||||
|
ContentTypeText = ContentType{valuer.NewString("text/plain")}
|
||||||
|
ContentTypeHTML = ContentType{valuer.NewString("text/html")}
|
||||||
|
)
|
||||||
|
|
||||||
|
type ContentType struct{ valuer.String }
|
56
pkg/smtp/client/option.go
Normal file
56
pkg/smtp/client/option.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
type Auth struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
Identity string
|
||||||
|
Secret string
|
||||||
|
}
|
||||||
|
|
||||||
|
type TLS struct {
|
||||||
|
Enabled bool
|
||||||
|
InsecureSkipVerify bool
|
||||||
|
CAFilePath string
|
||||||
|
KeyFilePath string
|
||||||
|
CertFilePath string
|
||||||
|
}
|
||||||
|
|
||||||
|
type options struct {
|
||||||
|
from string
|
||||||
|
headers map[string]string
|
||||||
|
hello string
|
||||||
|
auth Auth
|
||||||
|
tls TLS
|
||||||
|
}
|
||||||
|
|
||||||
|
type Option func(*options)
|
||||||
|
|
||||||
|
func WithFrom(s string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.from = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithHeaders(m map[string]string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.headers = m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithHello(s string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.hello = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithAuth(auth Auth) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.auth = auth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithTLS(tls TLS) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.tls = tls
|
||||||
|
}
|
||||||
|
}
|
360
pkg/smtp/client/smtp.go
Normal file
360
pkg/smtp/client/smtp.go
Normal file
@ -0,0 +1,360 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"math/rand"
|
||||||
|
"mime"
|
||||||
|
"mime/multipart"
|
||||||
|
"mime/quotedprintable"
|
||||||
|
"net"
|
||||||
|
"net/mail"
|
||||||
|
"net/smtp"
|
||||||
|
"net/textproto"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
logger *slog.Logger
|
||||||
|
address string
|
||||||
|
host string
|
||||||
|
port string
|
||||||
|
from *mail.Address
|
||||||
|
headers map[string]string
|
||||||
|
hello string
|
||||||
|
auth Auth
|
||||||
|
tls TLS
|
||||||
|
tlsConfig *tls.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(address string, logger *slog.Logger, opts ...Option) (*Client, error) {
|
||||||
|
clientOpts := options{
|
||||||
|
from: "signoz@signoz.localhost",
|
||||||
|
headers: make(map[string]string),
|
||||||
|
auth: Auth{},
|
||||||
|
tls: TLS{
|
||||||
|
Enabled: false,
|
||||||
|
},
|
||||||
|
hello: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(&clientOpts)
|
||||||
|
}
|
||||||
|
|
||||||
|
from, err := mail.ParseAddress(clientOpts.from)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse 'from' address: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
host, port, err := net.SplitHostPort(address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse 'address': %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if clientOpts.headers == nil {
|
||||||
|
clientOpts.headers = make(map[string]string)
|
||||||
|
}
|
||||||
|
clientOpts.headers["From"] = from.String()
|
||||||
|
|
||||||
|
tls, err := newTLSConfig(clientOpts.tls, host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create TLS config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Client{
|
||||||
|
logger: logger,
|
||||||
|
address: address,
|
||||||
|
host: host,
|
||||||
|
port: port,
|
||||||
|
from: from,
|
||||||
|
headers: clientOpts.headers,
|
||||||
|
hello: clientOpts.hello,
|
||||||
|
auth: clientOpts.auth,
|
||||||
|
tls: clientOpts.tls,
|
||||||
|
tlsConfig: tls,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Do(ctx context.Context, tos []*mail.Address, subject string, contentType ContentType, body []byte) error {
|
||||||
|
var (
|
||||||
|
smtpClient *smtp.Client
|
||||||
|
conn net.Conn
|
||||||
|
err error
|
||||||
|
success = false
|
||||||
|
)
|
||||||
|
|
||||||
|
// Dial the SMTP server.
|
||||||
|
conn, err = c.dial(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new SMTP client.
|
||||||
|
smtpClient, err = smtp.NewClient(conn, c.host)
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return fmt.Errorf("failed to create SMTP client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to clean up after ourselves but don't log anything if something has failed.
|
||||||
|
defer func() {
|
||||||
|
if err := smtpClient.Quit(); success && err != nil {
|
||||||
|
c.logger.Warn("failed to close SMTP connection", "error", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Send the EHLO command.
|
||||||
|
if c.hello != "" {
|
||||||
|
err = smtpClient.Hello(c.hello)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to send EHLO command: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If TLS is not enabled, check if the server supports STARTTLS.
|
||||||
|
if !c.tls.Enabled {
|
||||||
|
if ok, _ := smtpClient.Extension("STARTTLS"); ok {
|
||||||
|
if err := smtpClient.StartTLS(c.tlsConfig); err != nil {
|
||||||
|
return fmt.Errorf("failed to send STARTTLS command: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the server supports the AUTH command,
|
||||||
|
if ok, mech := smtpClient.Extension("AUTH"); ok {
|
||||||
|
// If the username is set, find the appropriate authentication mechanism.
|
||||||
|
if c.auth.Username != "" {
|
||||||
|
auth, err := c.smtpAuth(ctx, mech)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to find auth mechanism: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the AUTH command.
|
||||||
|
if err := smtpClient.Auth(auth); err != nil {
|
||||||
|
return fmt.Errorf("failed to auth: %T: %w", auth, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the MAIL command.
|
||||||
|
if err = smtpClient.Mail(c.from.Address); err != nil {
|
||||||
|
return fmt.Errorf("failed to send MAIL command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the RCPT command for each recipient.
|
||||||
|
for _, addr := range tos {
|
||||||
|
if err = smtpClient.Rcpt(addr.Address); err != nil {
|
||||||
|
return fmt.Errorf("failed to send RCPT command: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the email headers and body.
|
||||||
|
message, err := smtpClient.Data()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to send DATA command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
closeOnce := sync.OnceValue(func() error {
|
||||||
|
return message.Close()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Close the message when this method exits in order to not leak resources. Even though we're calling this explicitly
|
||||||
|
// further down, the method may exit before then.
|
||||||
|
defer func() {
|
||||||
|
// If we try close an already-closed writer, it'll send a subsequent request to the server which is invalid.
|
||||||
|
_ = closeOnce()
|
||||||
|
}()
|
||||||
|
|
||||||
|
buffer := &bytes.Buffer{}
|
||||||
|
for header, value := range c.headers {
|
||||||
|
fmt.Fprintf(buffer, "%s: %s\r\n", header, mime.QEncoding.Encode("utf-8", value))
|
||||||
|
}
|
||||||
|
|
||||||
|
at := strings.LastIndex(c.from.Address, "@")
|
||||||
|
if at >= 0 {
|
||||||
|
fmt.Fprintf(buffer, "Message-Id: %s\r\n", fmt.Sprintf("<%d.%d@%s>", time.Now().UnixNano(), rand.Uint64(), c.from.Address[at+1:]))
|
||||||
|
}
|
||||||
|
|
||||||
|
multipartBuffer := &bytes.Buffer{}
|
||||||
|
multipartWriter := multipart.NewWriter(multipartBuffer)
|
||||||
|
|
||||||
|
tosAsStrings := make([]string, len(tos))
|
||||||
|
for i, to := range tos {
|
||||||
|
tosAsStrings[i] = to.String()
|
||||||
|
}
|
||||||
|
fmt.Fprintf(buffer, "To: %s\r\n", strings.Join(tosAsStrings, ","))
|
||||||
|
fmt.Fprintf(buffer, "Subject: %s\r\n", subject)
|
||||||
|
fmt.Fprintf(buffer, "Date: %s\r\n", time.Now().Format(time.RFC1123Z))
|
||||||
|
fmt.Fprintf(buffer, "Content-Type: multipart/alternative; boundary=%s\r\n", multipartWriter.Boundary())
|
||||||
|
fmt.Fprintf(buffer, "MIME-Version: 1.0\r\n\r\n")
|
||||||
|
|
||||||
|
_, err = message.Write(buffer.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write headers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text template
|
||||||
|
if contentType == ContentTypeText {
|
||||||
|
w, err := multipartWriter.CreatePart(textproto.MIMEHeader{
|
||||||
|
"Content-Transfer-Encoding": {"quoted-printable"},
|
||||||
|
"Content-Type": {"text/plain; charset=UTF-8"},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create part for text template: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
qw := quotedprintable.NewWriter(w)
|
||||||
|
_, err = qw.Write([]byte(body))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write text part: %w", err)
|
||||||
|
}
|
||||||
|
err = qw.Close()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to close text part: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Html template
|
||||||
|
// Preferred alternative placed last per section 5.1.4 of RFC 2046
|
||||||
|
// https://www.ietf.org/rfc/rfc2046.txt
|
||||||
|
if contentType == ContentTypeHTML {
|
||||||
|
w, err := multipartWriter.CreatePart(textproto.MIMEHeader{
|
||||||
|
"Content-Transfer-Encoding": {"quoted-printable"},
|
||||||
|
"Content-Type": {"text/html; charset=UTF-8"},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create part for html template: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
qw := quotedprintable.NewWriter(w)
|
||||||
|
_, err = qw.Write([]byte(body))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write HTML part: %w", err)
|
||||||
|
}
|
||||||
|
err = qw.Close()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to close HTML part: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = multipartWriter.Close()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to close multipartWriter: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = message.Write(multipartBuffer.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write body buffer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete the message and await response.
|
||||||
|
if err = closeOnce(); err != nil {
|
||||||
|
return fmt.Errorf("failed to deliver: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
success = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// auth resolves a string of authentication mechanisms.
|
||||||
|
func (c *Client) smtpAuth(_ context.Context, mechs string) (smtp.Auth, error) {
|
||||||
|
username := c.auth.Username
|
||||||
|
|
||||||
|
var errs []error
|
||||||
|
for _, mech := range strings.Split(mechs, " ") {
|
||||||
|
switch mech {
|
||||||
|
case "CRAM-MD5":
|
||||||
|
secret := c.auth.Secret
|
||||||
|
if secret == "" {
|
||||||
|
errs = append(errs, errors.New("missing secret for CRAM-MD5 auth mechanism"))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return smtp.CRAMMD5Auth(username, secret), nil
|
||||||
|
|
||||||
|
case "PLAIN":
|
||||||
|
password := c.auth.Password
|
||||||
|
if password == "" {
|
||||||
|
errs = append(errs, errors.New("missing password for PLAIN auth mechanism"))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
identity := c.auth.Identity
|
||||||
|
|
||||||
|
return smtp.PlainAuth(identity, username, password, c.host), nil
|
||||||
|
case "LOGIN":
|
||||||
|
password := c.auth.Password
|
||||||
|
if password == "" {
|
||||||
|
errs = append(errs, errors.New("missing password for LOGIN auth mechanism"))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return LoginAuth(username, password), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) == 0 {
|
||||||
|
errs = append(errs, errors.New("unknown auth mechanism: "+mechs))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.Join(errs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) dial(ctx context.Context) (net.Conn, error) {
|
||||||
|
var (
|
||||||
|
conn net.Conn
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if c.tls.Enabled || c.port == "465" {
|
||||||
|
conn, err = tls.Dial("tcp", c.address, c.tlsConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to establish TLS connection to server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var d net.Dialer
|
||||||
|
conn, err = d.DialContext(ctx, "tcp", c.address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to establish connection to server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTLSConfig(config TLS, serverName string) (*tls.Config, error) {
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
InsecureSkipVerify: config.InsecureSkipVerify,
|
||||||
|
ServerName: serverName,
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.CertFilePath != "" {
|
||||||
|
cert, err := tls.LoadX509KeyPair(config.CertFilePath, config.KeyFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load cert or key file: %w", err)
|
||||||
|
}
|
||||||
|
tlsConfig.Certificates = []tls.Certificate{cert}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.CAFilePath != "" {
|
||||||
|
ca, err := os.ReadFile(config.CAFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load CA file: %w", err)
|
||||||
|
}
|
||||||
|
tlsConfig.RootCAs = x509.NewCertPool()
|
||||||
|
if !tlsConfig.RootCAs.AppendCertsFromPEM(ca) {
|
||||||
|
return nil, fmt.Errorf("failed to append CA file: %s", config.CAFilePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tlsConfig, nil
|
||||||
|
}
|
45
pkg/types/emailtypes/template.go
Normal file
45
pkg/types/emailtypes/template.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package emailtypes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"html/template"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Templates is a list of all the templates that are supported by the emailing service.
|
||||||
|
// This list should be updated whenever a new template is added.
|
||||||
|
Templates = []TemplateName{TemplateNameInvitationEmail}
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
TemplateNameInvitationEmail = TemplateName{valuer.NewString("invitation_email")}
|
||||||
|
)
|
||||||
|
|
||||||
|
type TemplateName struct{ valuer.String }
|
||||||
|
|
||||||
|
func NewTemplateName(name string) (TemplateName, error) {
|
||||||
|
switch name {
|
||||||
|
case TemplateNameInvitationEmail.StringValue():
|
||||||
|
return TemplateNameInvitationEmail, nil
|
||||||
|
default:
|
||||||
|
return TemplateName{}, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid template name: %s", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewContent(template *template.Template, data map[string]any) ([]byte, error) {
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
err := template.Execute(buf, data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to execute template")
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type TemplateStore interface {
|
||||||
|
Get(context.Context, TemplateName) (*template.Template, error)
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user