feat: add reply-to header in email notification (#6393)

* feat: add reply-to header to smtp messages

* fix: grpc reply_to_address min 0 and js var name

* fix: add missing translations

* fix merge and linting

---------

Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
Miguel Cabrerizo 2023-08-29 09:08:24 +02:00 committed by GitHub
parent 9b43e28c23
commit fd00ac533a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 307 additions and 120 deletions

View File

@ -371,10 +371,10 @@ SystemDefaults:
MachineKeySize: 2048 # ZITADEL_SYSTEMDEFAULTS_SECRETGENERATORS_MACHINEKEYSIZE
ApplicationKeySize: 2048 # ZITADEL_SYSTEMDEFAULTS_SECRETGENERATORS_APPLICATIONKEYSIZE
PasswordHasher:
# Set hasher configuration for user passwords.
# Passwords previously hashed with a different algorithm
# or cost are automatically re-hashed using this config,
# upon password validation or update.
# Set hasher configuration for user passwords.
# Passwords previously hashed with a different algorithm
# or cost are automatically re-hashed using this config,
# upon password validation or update.
Hasher:
Algorithm: "bcrypt" # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_ALGORITHM
Cost: 14 # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_COST
@ -688,6 +688,7 @@ DefaultInstance:
# If the host of the sender is different from ExternalDomain set DefaultInstance.DomainPolicy.SMTPSenderAddressMatchesInstanceDomain to false
From: # ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_SMTP_FROM
FromName: # ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_SMTP_FROMNAME
ReplyToAddress: # ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_SMTP_REPLYTOADDRESS
MessageTexts:
- MessageTextType: InitCode
Language: de

View File

@ -15,12 +15,17 @@
<form (ngSubmit)="savePolicy()" [formGroup]="form" autocomplete="off">
<cnsl-form-field class="smtp-form-field" label="Sender Address" required="true">
<cnsl-label>{{ 'SETTING.SMTP.SENDERADDRESS' | translate }}</cnsl-label>
<input cnslInput name="senderAddress" formControlName="senderAddress" />
<input cnslInput name="senderAddress" formControlName="senderAddress" required />
</cnsl-form-field>
<cnsl-form-field class="smtp-form-field" label="Sender Name" required="true">
<cnsl-label>{{ 'SETTING.SMTP.SENDERNAME' | translate }}</cnsl-label>
<input cnslInput name="senderName" formControlName="senderName" />
<input cnslInput name="senderName" formControlName="senderName" required />
</cnsl-form-field>
<cnsl-form-field class="smtp-form-field" label="Reply-To Address">
<cnsl-label>{{ 'SETTING.SMTP.REPLYTOADDRESS' | translate }}</cnsl-label>
<input cnslInput name="senderReplyToAddress" formControlName="replyToAddress" />
</cnsl-form-field>
<mat-checkbox class="smtp-checkbox" formControlName="tls">
@ -29,12 +34,12 @@
<cnsl-form-field class="smtp-form-field" label="Host And Port" required="true">
<cnsl-label>{{ 'SETTING.SMTP.HOSTANDPORT' | translate }}</cnsl-label>
<input cnslInput name="hostAndPort" formControlName="hostAndPort" placeholder="smtp.mailtrap.io:2525" />
<input cnslInput name="hostAndPort" formControlName="hostAndPort" placeholder="smtp.mailtrap.io:2525" required />
</cnsl-form-field>
<cnsl-form-field class="smtp-form-field" label="User" required="true">
<cnsl-label>{{ 'SETTING.SMTP.USER' | translate }}</cnsl-label>
<input id="smtp-user" cnslInput name="smtp-user" autocomplete="smtp-user" formControlName="user" />
<input id="smtp-user" cnslInput name="smtp-user" autocomplete="smtp-user" formControlName="user" required />
</cnsl-form-field>
<button

View File

@ -58,6 +58,7 @@ export class NotificationSettingsComponent implements OnInit {
this.form = this.fb.group({
senderAddress: [{ disabled: true, value: '' }, [requiredValidator]],
senderName: [{ disabled: true, value: '' }, [requiredValidator]],
replyToAddress: [{ disabled: true, value: '' }],
tls: [{ disabled: true, value: true }, [requiredValidator]],
hostAndPort: [{ disabled: true, value: '' }, [requiredValidator]],
user: [{ disabled: true, value: '' }, [requiredValidator]],
@ -143,6 +144,7 @@ export class NotificationSettingsComponent implements OnInit {
req.setHost(this.hostAndPort?.value ?? '');
req.setSenderAddress(this.senderAddress?.value ?? '');
req.setSenderName(this.senderName?.value ?? '');
req.setReplyToAddress(this.replyToAddress?.value ?? '');
req.setTls(this.tls?.value ?? false);
req.setUser(this.user?.value ?? '');
@ -152,6 +154,7 @@ export class NotificationSettingsComponent implements OnInit {
req.setHost(this.hostAndPort?.value ?? '');
req.setSenderAddress(this.senderAddress?.value ?? '');
req.setSenderName(this.senderName?.value ?? '');
req.setReplyToAddress(this.replyToAddress?.value ?? '');
req.setTls(this.tls?.value ?? false);
req.setUser(this.user?.value ?? '');
@ -298,6 +301,10 @@ export class NotificationSettingsComponent implements OnInit {
return this.form.get('senderName');
}
public get replyToAddress(): AbstractControl | null {
return this.form.get('replyToAddress');
}
public get tls(): AbstractControl | null {
return this.form.get('tls');
}

View File

@ -1049,6 +1049,7 @@
"TITLE": "SMTP настройки",
"SENDERADDRESS": "Имейл адрес на изпращача",
"SENDERNAME": "Име на изпращача",
"REPLYTOADDRESS": "Reply-to адрес",
"HOSTANDPORT": "Хост и порт",
"USER": "Потребител",
"PASSWORD": "Парола",

View File

@ -1055,6 +1055,7 @@
"TITLE": "SMTP Einstellungen",
"SENDERADDRESS": "Sender Email-Adresse",
"SENDERNAME": "Sender Name",
"REPLYTOADDRESS": "Reply-to-Adresse",
"HOSTANDPORT": "Host und Port",
"USER": "Benutzer",
"PASSWORD": "Passwort",

View File

@ -1056,6 +1056,7 @@
"TITLE": "SMTP Settings",
"SENDERADDRESS": "Sender Email Address",
"SENDERNAME": "Sender Name",
"REPLYTOADDRESS": "Reply-to Address",
"HOSTANDPORT": "Host And Port",
"USER": "User",
"PASSWORD": "Password",

View File

@ -1056,6 +1056,7 @@
"TITLE": "Ajustes SMTP",
"SENDERADDRESS": "Dirección email del emisor",
"SENDERNAME": "Nombre del emisor",
"REPLYTOADDRESS": "Dirección Reply-To",
"HOSTANDPORT": "Servidor y puerto",
"USER": "Usuario",
"PASSWORD": "Contraseña",

View File

@ -1055,6 +1055,7 @@
"TITLE": "Paramètres SMTP",
"SENDERADDRESS": "Adresse e-mail de l'expéditeur",
"SENDERNAME": "Nom de l'expéditeur",
"REPLYTOADDRESS": "Adresse Reply-to",
"HOSTANDPORT": "Hôte et port",
"USER": "Utilisateur",
"PASSWORD": "Mot de passe",

View File

@ -1055,6 +1055,7 @@
"TITLE": "Impostazioni SMTP",
"SENDERADDRESS": "Indirizzo email del mittente",
"SENDERNAME": "Nome del mittente",
"REPLYTOADDRESS": "Indirizzo Reply-to",
"HOSTANDPORT": "Host e porta",
"USER": "Utente",
"PASSWORD": "Password",

View File

@ -1056,6 +1056,7 @@
"TITLE": "SMTP設定",
"SENDERADDRESS": "送信者のメールアドレス",
"SENDERNAME": "送信者名",
"REPLYTOADDRESS": "返信先アドレス",
"HOSTANDPORT": "ホストとポート",
"USER": "ユーザー",
"PASSWORD": "パスワード",

View File

@ -1056,6 +1056,7 @@
"TITLE": "SMTP подесувања",
"SENDERADDRESS": "Адреса на испраќачот",
"SENDERNAME": "Име на испраќачот",
"REPLYTOADDRESS": "Reply-to адреса",
"HOSTANDPORT": "Host и Port",
"USER": "Корисник",
"PASSWORD": "Лозинка",

View File

@ -1055,6 +1055,7 @@
"TITLE": "Ustawienia SMTP",
"SENDERADDRESS": "Adres e-mail nadawcy",
"SENDERNAME": "Nazwa nadawcy",
"REPLYTOADDRESS": "Adres Reply-to",
"HOSTANDPORT": "Host i port",
"USER": "Użytkownik",
"PASSWORD": "Hasło",

View File

@ -1056,6 +1056,7 @@
"TITLE": "Configurações SMTP",
"SENDERADDRESS": "Endereço de e-mail do remetente",
"SENDERNAME": "Nome do remetente",
"REPLYTOADDRESS": "Endereço Reply-To",
"HOSTANDPORT": "Host e porta",
"USER": "Usuário",
"PASSWORD": "Senha",

View File

@ -1055,6 +1055,7 @@
"TITLE": "SMTP 设置",
"SENDERADDRESS": "发件人地址",
"SENDERNAME": "发件人名称",
"REPLYTOADDRESS": "Reply-to 地址",
"HOSTANDPORT": "主机和端口",
"USER": "用户名",
"PASSWORD": "密码",

View File

@ -200,6 +200,7 @@ DefaultInstance:
# if the host of the sender is different from ExternalDomain set DefaultInstance.DomainPolicy.SMTPSenderAddressMatchesInstanceDomain to false
From:
FromName:
ReplyToAddress:
```
- If you don't want to use the DefaultInstance configuration for the first instance that ZITADEL automatically creates for you during the [setup phase](/self-hosting/manage/configure#database-initialization), you can provide a FirstInstance YAML section using the --steps argument.

View File

@ -132,9 +132,10 @@ func SecretGeneratorTypeToDomain(generatorType settings_pb.SecretGeneratorType)
func AddSMTPToConfig(req *admin_pb.AddSMTPConfigRequest) *smtp.Config {
return &smtp.Config{
Tls: req.Tls,
From: req.SenderAddress,
FromName: req.SenderName,
Tls: req.Tls,
From: req.SenderAddress,
FromName: req.SenderName,
ReplyToAddress: req.ReplyToAddress,
SMTP: smtp.SMTP{
Host: req.Host,
User: req.User,
@ -145,9 +146,10 @@ func AddSMTPToConfig(req *admin_pb.AddSMTPConfigRequest) *smtp.Config {
func UpdateSMTPToConfig(req *admin_pb.UpdateSMTPConfigRequest) *smtp.Config {
return &smtp.Config{
Tls: req.Tls,
From: req.SenderAddress,
FromName: req.SenderName,
Tls: req.Tls,
From: req.SenderAddress,
FromName: req.SenderName,
ReplyToAddress: req.ReplyToAddress,
SMTP: smtp.SMTP{
Host: req.Host,
User: req.User,
@ -157,12 +159,13 @@ func UpdateSMTPToConfig(req *admin_pb.UpdateSMTPConfigRequest) *smtp.Config {
func SMTPConfigToPb(smtp *query.SMTPConfig) *settings_pb.SMTPConfig {
mapped := &settings_pb.SMTPConfig{
Tls: smtp.TLS,
SenderAddress: smtp.SenderAddress,
SenderName: smtp.SenderName,
Host: smtp.Host,
User: smtp.User,
Details: obj_grpc.ToViewDetailsPb(smtp.Sequence, smtp.CreationDate, smtp.ChangeDate, smtp.AggregateID),
Tls: smtp.TLS,
SenderAddress: smtp.SenderAddress,
SenderName: smtp.SenderName,
ReplyToAddress: smtp.ReplyToAddress,
Host: smtp.Host,
User: smtp.User,
Details: obj_grpc.ToViewDetailsPb(smtp.Sequence, smtp.CreationDate, smtp.ChangeDate, smtp.AggregateID),
}
return mapped
}

View File

@ -414,6 +414,7 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str
instanceAgg,
setup.SMTPConfiguration.From,
setup.SMTPConfiguration.FromName,
setup.SMTPConfiguration.ReplyToAddress,
setup.SMTPConfiguration.SMTP.Host,
setup.SMTPConfiguration.SMTP.User,
[]byte(setup.SMTPConfiguration.SMTP.Password),

View File

@ -12,13 +12,14 @@ import (
type InstanceSMTPConfigWriteModel struct {
eventstore.WriteModel
SenderAddress string
SenderName string
TLS bool
Host string
User string
Password *crypto.CryptoValue
State domain.SMTPConfigState
SenderAddress string
SenderName string
ReplyToAddress string
TLS bool
Host string
User string
Password *crypto.CryptoValue
State domain.SMTPConfigState
domain string
domainState domain.InstanceDomainState
@ -62,6 +63,7 @@ func (wm *InstanceSMTPConfigWriteModel) Reduce() error {
wm.TLS = e.TLS
wm.SenderAddress = e.SenderAddress
wm.SenderName = e.SenderName
wm.ReplyToAddress = e.ReplyToAddress
wm.Host = e.Host
wm.User = e.User
wm.Password = e.Password
@ -76,6 +78,9 @@ func (wm *InstanceSMTPConfigWriteModel) Reduce() error {
if e.FromName != nil {
wm.SenderName = *e.FromName
}
if e.ReplyToAddress != nil {
wm.ReplyToAddress = *e.ReplyToAddress
}
if e.Host != nil {
wm.Host = *e.Host
}
@ -87,6 +92,7 @@ func (wm *InstanceSMTPConfigWriteModel) Reduce() error {
wm.TLS = false
wm.SenderName = ""
wm.SenderAddress = ""
wm.ReplyToAddress = ""
wm.Host = ""
wm.User = ""
wm.Password = nil
@ -122,7 +128,7 @@ func (wm *InstanceSMTPConfigWriteModel) Query() *eventstore.SearchQueryBuilder {
Builder()
}
func (wm *InstanceSMTPConfigWriteModel) NewChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, tls bool, fromAddress, fromName, smtpHost, smtpUser string) (*instance.SMTPConfigChangedEvent, bool, error) {
func (wm *InstanceSMTPConfigWriteModel) NewChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, tls bool, fromAddress, fromName, replyToAddress, smtpHost, smtpUser string) (*instance.SMTPConfigChangedEvent, bool, error) {
changes := make([]instance.SMTPConfigChanges, 0)
var err error
@ -135,6 +141,9 @@ func (wm *InstanceSMTPConfigWriteModel) NewChangedEvent(ctx context.Context, agg
if wm.SenderName != fromName {
changes = append(changes, instance.ChangeSMTPConfigFromName(fromName))
}
if wm.ReplyToAddress != replyToAddress {
changes = append(changes, instance.ChangeSMTPConfigReplyToAddress(replyToAddress))
}
if wm.Host != smtpHost {
changes = append(changes, instance.ChangeSMTPConfigSMTPHost(smtpHost))
}

View File

@ -17,7 +17,7 @@ import (
func (c *Commands) AddSMTPConfig(ctx context.Context, config *smtp.Config) (*domain.ObjectDetails, error) {
instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID())
validation := c.prepareAddSMTPConfig(instanceAgg, config.From, config.FromName, config.SMTP.Host, config.SMTP.User, []byte(config.SMTP.Password), config.Tls)
validation := c.prepareAddSMTPConfig(instanceAgg, config.From, config.FromName, config.ReplyToAddress, config.SMTP.Host, config.SMTP.User, []byte(config.SMTP.Password), config.Tls)
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validation)
if err != nil {
return nil, err
@ -35,7 +35,7 @@ func (c *Commands) AddSMTPConfig(ctx context.Context, config *smtp.Config) (*dom
func (c *Commands) ChangeSMTPConfig(ctx context.Context, config *smtp.Config) (*domain.ObjectDetails, error) {
instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID())
validation := c.prepareChangeSMTPConfig(instanceAgg, config.From, config.FromName, config.SMTP.Host, config.SMTP.User, config.Tls)
validation := c.prepareChangeSMTPConfig(instanceAgg, config.From, config.FromName, config.ReplyToAddress, config.SMTP.Host, config.SMTP.User, config.Tls)
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validation)
if err != nil {
return nil, err
@ -99,11 +99,14 @@ func (c *Commands) RemoveSMTPConfig(ctx context.Context) (*domain.ObjectDetails,
}, nil
}
func (c *Commands) prepareAddSMTPConfig(a *instance.Aggregate, from, name, hostAndPort, user string, password []byte, tls bool) preparation.Validation {
func (c *Commands) prepareAddSMTPConfig(a *instance.Aggregate, from, name, replyTo, hostAndPort, user string, password []byte, tls bool) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if from = strings.TrimSpace(from); from == "" {
return nil, errors.ThrowInvalidArgument(nil, "INST-mruNY", "Errors.Invalid.Argument")
}
replyTo = strings.TrimSpace(replyTo)
hostAndPort = strings.TrimSpace(hostAndPort)
if _, _, err := net.SplitHostPort(hostAndPort); err != nil {
return nil, errors.ThrowInvalidArgument(nil, "INST-9JdRe", "Errors.Invalid.Argument")
@ -136,6 +139,7 @@ func (c *Commands) prepareAddSMTPConfig(a *instance.Aggregate, from, name, hostA
tls,
from,
name,
replyTo,
hostAndPort,
user,
smtpPassword,
@ -145,11 +149,13 @@ func (c *Commands) prepareAddSMTPConfig(a *instance.Aggregate, from, name, hostA
}
}
func (c *Commands) prepareChangeSMTPConfig(a *instance.Aggregate, from, name, hostAndPort, user string, tls bool) preparation.Validation {
func (c *Commands) prepareChangeSMTPConfig(a *instance.Aggregate, from, name, replyTo, hostAndPort, user string, tls bool) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if from = strings.TrimSpace(from); from == "" {
return nil, errors.ThrowInvalidArgument(nil, "INST-ASv2d", "Errors.Invalid.Argument")
}
replyTo = strings.TrimSpace(replyTo)
hostAndPort = strings.TrimSpace(hostAndPort)
if _, _, err := net.SplitHostPort(hostAndPort); err != nil {
return nil, errors.ThrowInvalidArgument(nil, "INST-Kv875", "Errors.Invalid.Argument")
@ -174,6 +180,7 @@ func (c *Commands) prepareChangeSMTPConfig(a *instance.Aggregate, from, name, ho
tls,
from,
name,
replyTo,
hostAndPort,
user,
)

View File

@ -95,6 +95,7 @@ func TestCommandSide_AddSMTPConfig(t *testing.T) {
true,
"from@domain.ch",
"name",
"",
"host:587",
"user",
&crypto.CryptoValue{},
@ -106,9 +107,10 @@ func TestCommandSide_AddSMTPConfig(t *testing.T) {
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
smtp: &smtp.Config{
Tls: true,
From: "from@domain.ch",
FromName: "name",
Tls: true,
From: "from@domain.ch",
FromName: "name",
ReplyToAddress: "",
SMTP: smtp.SMTP{
Host: "host:587",
User: "user",
@ -150,6 +152,7 @@ func TestCommandSide_AddSMTPConfig(t *testing.T) {
true,
"from@domain.ch",
"name",
"",
"host:587",
"user",
&crypto.CryptoValue{
@ -184,6 +187,72 @@ func TestCommandSide_AddSMTPConfig(t *testing.T) {
},
},
},
{
name: "add smtp config with reply to address, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
instance.NewDomainAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
"domain.ch",
false,
),
),
eventFromEventPusher(
instance.NewDomainPolicyAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
true, true, false,
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusherWithInstanceID(
"INSTANCE",
instance.NewSMTPConfigAddedEvent(
context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
true,
"from@domain.ch",
"name",
"replyto@domain.ch",
"host:587",
"user",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("password"),
},
),
),
},
),
),
alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
smtp: &smtp.Config{
Tls: true,
From: "from@domain.ch",
FromName: "name",
ReplyToAddress: "replyto@domain.ch",
SMTP: smtp.SMTP{
Host: "host:587",
User: "user",
Password: "password",
},
},
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "INSTANCE",
},
},
},
{
name: "smtp config, port is missing",
fields: fields{
@ -258,6 +327,7 @@ func TestCommandSide_AddSMTPConfig(t *testing.T) {
true,
"from@domain.ch",
"name",
"",
"[2001:db8::1]:2525",
"user",
&crypto.CryptoValue{
@ -396,6 +466,7 @@ func TestCommandSide_ChangeSMTPConfig(t *testing.T) {
true,
"from@domain.ch",
"name",
"",
"host:587",
"user",
&crypto.CryptoValue{},
@ -446,6 +517,7 @@ func TestCommandSide_ChangeSMTPConfig(t *testing.T) {
true,
"from@domain.ch",
"name",
"",
"host:587",
"user",
&crypto.CryptoValue{},
@ -496,6 +568,7 @@ func TestCommandSide_ChangeSMTPConfig(t *testing.T) {
true,
"from@domain.ch",
"name",
"",
"host:587",
"user",
&crypto.CryptoValue{},
@ -511,6 +584,7 @@ func TestCommandSide_ChangeSMTPConfig(t *testing.T) {
false,
"from2@domain.ch",
"name2",
"replyto@domain.ch",
"host2:587",
"user2",
),
@ -522,9 +596,10 @@ func TestCommandSide_ChangeSMTPConfig(t *testing.T) {
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
smtp: &smtp.Config{
Tls: false,
From: "from2@domain.ch",
FromName: "name2",
Tls: false,
From: "from2@domain.ch",
FromName: "name2",
ReplyToAddress: "replyto@domain.ch",
SMTP: smtp.SMTP{
Host: "host2:587",
User: "user2",
@ -607,6 +682,7 @@ func TestCommandSide_ChangeSMTPConfig(t *testing.T) {
true,
"from@domain.ch",
"name",
"",
"host:587",
"user",
&crypto.CryptoValue{},
@ -622,6 +698,7 @@ func TestCommandSide_ChangeSMTPConfig(t *testing.T) {
false,
"from2@domain.ch",
"name2",
"replyto@domain.ch",
"[2001:db8::1]:2525",
"user2",
),
@ -633,9 +710,10 @@ func TestCommandSide_ChangeSMTPConfig(t *testing.T) {
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
smtp: &smtp.Config{
Tls: false,
From: "from2@domain.ch",
FromName: "name2",
Tls: false,
From: "from2@domain.ch",
FromName: "name2",
ReplyToAddress: "replyto@domain.ch",
SMTP: smtp.SMTP{
Host: "[2001:db8::1]:2525",
User: "user2",
@ -716,6 +794,7 @@ func TestCommandSide_ChangeSMTPConfigPassword(t *testing.T) {
true,
"from",
"name",
"",
"host:587",
"user",
&crypto.CryptoValue{},
@ -819,6 +898,7 @@ func TestCommandSide_RemoveSMTPConfig(t *testing.T) {
true,
"from",
"name",
"",
"host:587",
"user",
&crypto.CryptoValue{},
@ -868,11 +948,12 @@ func TestCommandSide_RemoveSMTPConfig(t *testing.T) {
}
}
func newSMTPConfigChangedEvent(ctx context.Context, tls bool, fromAddress, fromName, host, user string) *instance.SMTPConfigChangedEvent {
func newSMTPConfigChangedEvent(ctx context.Context, tls bool, fromAddress, fromName, replyTo, host, user string) *instance.SMTPConfigChangedEvent {
changes := []instance.SMTPConfigChanges{
instance.ChangeSMTPConfigTLS(tls),
instance.ChangeSMTPConfigFromAddress(fromAddress),
instance.ChangeSMTPConfigFromName(fromName),
instance.ChangeSMTPConfigReplyToAddress(replyTo),
instance.ChangeSMTPConfigSMTPHost(host),
instance.ChangeSMTPConfigSMTPUser(user),
}

View File

@ -17,9 +17,10 @@ import (
var _ channels.NotificationChannel = (*Email)(nil)
type Email struct {
smtpClient *smtp.Client
senderAddress string
senderName string
smtpClient *smtp.Client
senderAddress string
senderName string
replyToAddress string
}
func InitChannel(ctx context.Context, getSMTPConfig func(ctx context.Context) (*Config, error)) (*Email, error) {
@ -36,9 +37,10 @@ func InitChannel(ctx context.Context, getSMTPConfig func(ctx context.Context) (*
logging.New().Debug("successfully initialized smtp email channel")
return &Email{
smtpClient: client,
senderName: smtpConfig.FromName,
senderAddress: smtpConfig.From,
smtpClient: client,
senderName: smtpConfig.FromName,
senderAddress: smtpConfig.From,
replyToAddress: smtpConfig.ReplyToAddress,
}, nil
}
@ -54,6 +56,7 @@ func (email *Email) HandleMessage(message channels.Message) error {
}
emailMsg.SenderEmail = email.senderAddress
emailMsg.SenderName = email.senderName
emailMsg.ReplyToAddress = email.replyToAddress
// To && From
if err := email.smtpClient.Mail(emailMsg.SenderEmail); err != nil {
return caos_errs.ThrowInternalf(err, "EMAIL-s3is3", "could not set sender: %v", emailMsg.SenderEmail)

View File

@ -1,10 +1,11 @@
package smtp
type Config struct {
SMTP SMTP
Tls bool
From string
FromName string
SMTP SMTP
Tls bool
From string
FromName string
ReplyToAddress string
}
type SMTP struct {

View File

@ -19,9 +19,10 @@ func (n *NotificationQueries) GetSMTPConfig(ctx context.Context) (*smtp.Config,
return nil, err
}
return &smtp.Config{
From: config.SenderAddress,
FromName: config.SenderName,
Tls: config.TLS,
From: config.SenderAddress,
FromName: config.SenderName,
ReplyToAddress: config.ReplyToAddress,
Tls: config.TLS,
SMTP: smtp.SMTP{
Host: config.Host,
User: config.User,

View File

@ -23,6 +23,7 @@ type Email struct {
CC []string
SenderEmail string
SenderName string
ReplyToAddress string
Subject string
Content string
TriggeringEvent eventstore.Event
@ -35,6 +36,9 @@ func (msg *Email) GetContent() (string, error) {
from = fmt.Sprintf("%s <%s>", msg.SenderName, msg.SenderEmail)
}
headers["From"] = from
if msg.ReplyToAddress != "" {
headers["Reply-to"] = msg.ReplyToAddress
}
headers["Return-Path"] = msg.SenderEmail
headers["To"] = strings.Join(msg.Recipients, ", ")
headers["Cc"] = strings.Join(msg.CC, ", ")

View File

@ -11,20 +11,21 @@ import (
)
const (
SMTPConfigProjectionTable = "projections.smtp_configs"
SMTPConfigProjectionTable = "projections.smtp_configs1"
SMTPConfigColumnAggregateID = "aggregate_id"
SMTPConfigColumnCreationDate = "creation_date"
SMTPConfigColumnChangeDate = "change_date"
SMTPConfigColumnSequence = "sequence"
SMTPConfigColumnResourceOwner = "resource_owner"
SMTPConfigColumnInstanceID = "instance_id"
SMTPConfigColumnTLS = "tls"
SMTPConfigColumnSenderAddress = "sender_address"
SMTPConfigColumnSenderName = "sender_name"
SMTPConfigColumnSMTPHost = "host"
SMTPConfigColumnSMTPUser = "username"
SMTPConfigColumnSMTPPassword = "password"
SMTPConfigColumnAggregateID = "aggregate_id"
SMTPConfigColumnCreationDate = "creation_date"
SMTPConfigColumnChangeDate = "change_date"
SMTPConfigColumnSequence = "sequence"
SMTPConfigColumnResourceOwner = "resource_owner"
SMTPConfigColumnInstanceID = "instance_id"
SMTPConfigColumnTLS = "tls"
SMTPConfigColumnSenderAddress = "sender_address"
SMTPConfigColumnSenderName = "sender_name"
SMTPConfigColumnReplyToAddress = "reply_to_address"
SMTPConfigColumnSMTPHost = "host"
SMTPConfigColumnSMTPUser = "username"
SMTPConfigColumnSMTPPassword = "password"
)
type smtpConfigProjection struct {
@ -46,6 +47,7 @@ func newSMTPConfigProjection(ctx context.Context, config crdb.StatementHandlerCo
crdb.NewColumn(SMTPConfigColumnTLS, crdb.ColumnTypeBool),
crdb.NewColumn(SMTPConfigColumnSenderAddress, crdb.ColumnTypeText),
crdb.NewColumn(SMTPConfigColumnSenderName, crdb.ColumnTypeText),
crdb.NewColumn(SMTPConfigColumnReplyToAddress, crdb.ColumnTypeText),
crdb.NewColumn(SMTPConfigColumnSMTPHost, crdb.ColumnTypeText),
crdb.NewColumn(SMTPConfigColumnSMTPUser, crdb.ColumnTypeText),
crdb.NewColumn(SMTPConfigColumnSMTPPassword, crdb.ColumnTypeJSONB, crdb.Nullable()),
@ -104,6 +106,7 @@ func (p *smtpConfigProjection) reduceSMTPConfigAdded(event eventstore.Event) (*h
handler.NewCol(SMTPConfigColumnTLS, e.TLS),
handler.NewCol(SMTPConfigColumnSenderAddress, e.SenderAddress),
handler.NewCol(SMTPConfigColumnSenderName, e.SenderName),
handler.NewCol(SMTPConfigColumnReplyToAddress, e.ReplyToAddress),
handler.NewCol(SMTPConfigColumnSMTPHost, e.Host),
handler.NewCol(SMTPConfigColumnSMTPUser, e.User),
handler.NewCol(SMTPConfigColumnSMTPPassword, e.Password),
@ -117,7 +120,7 @@ func (p *smtpConfigProjection) reduceSMTPConfigChanged(event eventstore.Event) (
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-wl0wd", "reduce.wrong.event.type %s", instance.SMTPConfigChangedEventType)
}
columns := make([]handler.Column, 0, 7)
columns := make([]handler.Column, 0, 8)
columns = append(columns, handler.NewCol(SMTPConfigColumnChangeDate, e.CreationDate()),
handler.NewCol(SMTPConfigColumnSequence, e.Sequence()))
if e.TLS != nil {
@ -129,6 +132,9 @@ func (p *smtpConfigProjection) reduceSMTPConfigChanged(event eventstore.Event) (
if e.FromName != nil {
columns = append(columns, handler.NewCol(SMTPConfigColumnSenderName, *e.FromName))
}
if e.ReplyToAddress != nil {
columns = append(columns, handler.NewCol(SMTPConfigColumnReplyToAddress, *e.ReplyToAddress))
}
if e.Host != nil {
columns = append(columns, handler.NewCol(SMTPConfigColumnSMTPHost, *e.Host))
}

View File

@ -30,6 +30,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) {
"tls": true,
"senderAddress": "sender",
"senderName": "name",
"replyToAddress": "reply-to",
"host": "host",
"user": "user"
}`,
@ -44,13 +45,14 @@ func TestSMTPConfigProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.smtp_configs SET (change_date, sequence, tls, sender_address, sender_name, host, username) = ($1, $2, $3, $4, $5, $6, $7) WHERE (aggregate_id = $8) AND (instance_id = $9)",
expectedStmt: "UPDATE projections.smtp_configs1 SET (change_date, sequence, tls, sender_address, sender_name, reply_to_address, host, username) = ($1, $2, $3, $4, $5, $6, $7, $8) WHERE (aggregate_id = $9) AND (instance_id = $10)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
true,
"sender",
"name",
"reply-to",
"host",
"user",
"agg-id",
@ -71,6 +73,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) {
"tls": true,
"senderAddress": "sender",
"senderName": "name",
"replyToAddress": "reply-to",
"host": "host",
"user": "user",
"password": {
@ -89,7 +92,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "INSERT INTO projections.smtp_configs (aggregate_id, creation_date, change_date, resource_owner, instance_id, sequence, tls, sender_address, sender_name, host, username, password) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)",
expectedStmt: "INSERT INTO projections.smtp_configs1 (aggregate_id, creation_date, change_date, resource_owner, instance_id, sequence, tls, sender_address, sender_name, reply_to_address, host, username, password) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)",
expectedArgs: []interface{}{
"agg-id",
anyArg{},
@ -100,6 +103,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) {
true,
"sender",
"name",
"reply-to",
"host",
"user",
anyArg{},
@ -132,7 +136,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.smtp_configs SET (change_date, sequence, password) = ($1, $2, $3) WHERE (aggregate_id = $4) AND (instance_id = $5)",
expectedStmt: "UPDATE projections.smtp_configs1 SET (change_date, sequence, password) = ($1, $2, $3) WHERE (aggregate_id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
@ -162,7 +166,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM projections.smtp_configs WHERE (aggregate_id = $1) AND (instance_id = $2)",
expectedStmt: "DELETE FROM projections.smtp_configs1 WHERE (aggregate_id = $1) AND (instance_id = $2)",
expectedArgs: []interface{}{
"agg-id",
"instance-id",
@ -189,7 +193,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM projections.smtp_configs WHERE (instance_id = $1)",
expectedStmt: "DELETE FROM projections.smtp_configs1 WHERE (instance_id = $1)",
expectedArgs: []interface{}{
"agg-id",
},

View File

@ -57,6 +57,10 @@ var (
name: projection.SMTPConfigColumnSenderName,
table: smtpConfigsTable,
}
SMTPConfigColumnReplyToAddress = Column{
name: projection.SMTPConfigColumnReplyToAddress,
table: smtpConfigsTable,
}
SMTPConfigColumnSMTPHost = Column{
name: projection.SMTPConfigColumnSMTPHost,
table: smtpConfigsTable,
@ -83,12 +87,13 @@ type SMTPConfig struct {
ResourceOwner string
Sequence uint64
TLS bool
SenderAddress string
SenderName string
Host string
User string
Password *crypto.CryptoValue
TLS bool
SenderAddress string
SenderName string
ReplyToAddress string
Host string
User string
Password *crypto.CryptoValue
}
func (q *Queries) SMTPConfigByAggregateID(ctx context.Context, aggregateID string) (config *SMTPConfig, err error) {
@ -123,6 +128,7 @@ func prepareSMTPConfigQuery(ctx context.Context, db prepareDatabase) (sq.SelectB
SMTPConfigColumnTLS.identifier(),
SMTPConfigColumnSenderAddress.identifier(),
SMTPConfigColumnSenderName.identifier(),
SMTPConfigColumnReplyToAddress.identifier(),
SMTPConfigColumnSMTPHost.identifier(),
SMTPConfigColumnSMTPUser.identifier(),
SMTPConfigColumnSMTPPassword.identifier()).
@ -139,6 +145,7 @@ func prepareSMTPConfigQuery(ctx context.Context, db prepareDatabase) (sq.SelectB
&config.TLS,
&config.SenderAddress,
&config.SenderName,
&config.ReplyToAddress,
&config.Host,
&config.User,
&password,

View File

@ -13,18 +13,19 @@ import (
)
var (
prepareSMTPConfigStmt = `SELECT projections.smtp_configs.aggregate_id,` +
` projections.smtp_configs.creation_date,` +
` projections.smtp_configs.change_date,` +
` projections.smtp_configs.resource_owner,` +
` projections.smtp_configs.sequence,` +
` projections.smtp_configs.tls,` +
` projections.smtp_configs.sender_address,` +
` projections.smtp_configs.sender_name,` +
` projections.smtp_configs.host,` +
` projections.smtp_configs.username,` +
` projections.smtp_configs.password` +
` FROM projections.smtp_configs` +
prepareSMTPConfigStmt = `SELECT projections.smtp_configs1.aggregate_id,` +
` projections.smtp_configs1.creation_date,` +
` projections.smtp_configs1.change_date,` +
` projections.smtp_configs1.resource_owner,` +
` projections.smtp_configs1.sequence,` +
` projections.smtp_configs1.tls,` +
` projections.smtp_configs1.sender_address,` +
` projections.smtp_configs1.sender_name,` +
` projections.smtp_configs1.reply_to_address,` +
` projections.smtp_configs1.host,` +
` projections.smtp_configs1.username,` +
` projections.smtp_configs1.password` +
` FROM projections.smtp_configs1` +
` AS OF SYSTEM TIME '-1 ms'`
prepareSMTPConfigCols = []string{
"aggregate_id",
@ -35,6 +36,7 @@ var (
"tls",
"sender_address",
"sender_name",
"reply_to_address",
"smtp_host",
"smtp_user",
"smtp_password",
@ -86,6 +88,7 @@ func Test_SMTPConfigsPrepares(t *testing.T) {
true,
"sender",
"name",
"reply-to",
"host",
"user",
&crypto.CryptoValue{},
@ -93,17 +96,18 @@ func Test_SMTPConfigsPrepares(t *testing.T) {
),
},
object: &SMTPConfig{
AggregateID: "agg-id",
CreationDate: testNow,
ChangeDate: testNow,
ResourceOwner: "ro",
Sequence: 20211108,
TLS: true,
SenderAddress: "sender",
SenderName: "name",
Host: "host",
User: "user",
Password: &crypto.CryptoValue{},
AggregateID: "agg-id",
CreationDate: testNow,
ChangeDate: testNow,
ResourceOwner: "ro",
Sequence: 20211108,
TLS: true,
SenderAddress: "sender",
SenderName: "name",
ReplyToAddress: "reply-to",
Host: "host",
User: "user",
Password: &crypto.CryptoValue{},
},
},
{

View File

@ -21,12 +21,13 @@ const (
type SMTPConfigAddedEvent struct {
eventstore.BaseEvent `json:"-"`
SenderAddress string `json:"senderAddress,omitempty"`
SenderName string `json:"senderName,omitempty"`
TLS bool `json:"tls,omitempty"`
Host string `json:"host,omitempty"`
User string `json:"user,omitempty"`
Password *crypto.CryptoValue `json:"password,omitempty"`
SenderAddress string `json:"senderAddress,omitempty"`
SenderName string `json:"senderName,omitempty"`
ReplyToAddress string `json:"replyToAddress,omitempty"`
TLS bool `json:"tls,omitempty"`
Host string `json:"host,omitempty"`
User string `json:"user,omitempty"`
Password *crypto.CryptoValue `json:"password,omitempty"`
}
func NewSMTPConfigAddedEvent(
@ -35,6 +36,7 @@ func NewSMTPConfigAddedEvent(
tls bool,
senderAddress,
senderName,
replyToAddress,
host,
user string,
password *crypto.CryptoValue,
@ -45,12 +47,13 @@ func NewSMTPConfigAddedEvent(
aggregate,
SMTPConfigAddedEventType,
),
TLS: tls,
SenderAddress: senderAddress,
SenderName: senderName,
Host: host,
User: user,
Password: password,
TLS: tls,
SenderAddress: senderAddress,
SenderName: senderName,
ReplyToAddress: replyToAddress,
Host: host,
User: user,
Password: password,
}
}
@ -77,11 +80,12 @@ func SMTPConfigAddedEventMapper(event *repository.Event) (eventstore.Event, erro
type SMTPConfigChangedEvent struct {
eventstore.BaseEvent `json:"-"`
FromAddress *string `json:"senderAddress,omitempty"`
FromName *string `json:"senderName,omitempty"`
TLS *bool `json:"tls,omitempty"`
Host *string `json:"host,omitempty"`
User *string `json:"user,omitempty"`
FromAddress *string `json:"senderAddress,omitempty"`
FromName *string `json:"senderName,omitempty"`
ReplyToAddress *string `json:"replyToAddress,omitempty"`
TLS *bool `json:"tls,omitempty"`
Host *string `json:"host,omitempty"`
User *string `json:"user,omitempty"`
}
func (e *SMTPConfigChangedEvent) Data() interface{} {
@ -133,6 +137,12 @@ func ChangeSMTPConfigFromName(senderName string) func(event *SMTPConfigChangedEv
}
}
func ChangeSMTPConfigReplyToAddress(replyToAddress string) func(event *SMTPConfigChangedEvent) {
return func(e *SMTPConfigChangedEvent) {
e.ReplyToAddress = &replyToAddress
}
}
func ChangeSMTPConfigSMTPHost(smtpHost string) func(event *SMTPConfigChangedEvent) {
return func(e *SMTPConfigChangedEvent) {
e.Host = &smtpHost

View File

@ -3842,6 +3842,14 @@ message AddSMTPConfigRequest {
example: "\"this-is-my-password\"";
}
];
string reply_to_address = 7 [
(validate.rules).string = {min_len: 0, max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"replyto@m.zitadel.cloud\"";
min_length: 1;
max_length: 200;
}
];
}
message AddSMTPConfigResponse {
@ -3883,6 +3891,14 @@ message UpdateSMTPConfigRequest {
example: "\"197f0117-529e-443d-bf6c-0292dd9a02b7\"";
}
];
string reply_to_address = 6 [
(validate.rules).string = {min_len: 0, max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"replyto@m.zitadel.cloud\"";
min_length: 1;
max_length: 200;
}
];
}
message UpdateSMTPConfigResponse {

View File

@ -75,6 +75,11 @@ message SMTPConfig {
example: "\"197f0117-529e-443d-bf6c-0292dd9a02b7\"";
}
];
string reply_to_address = 7 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"replyto@m.zitadel.cloud\"";
}
];
}
message SMSProvider {