fix guard rails for lowercasing emails

Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
Ronni Skansing
2026-06-22 22:50:06 +02:00
parent 96f2f7cf8d
commit bd00db2a6e
2 changed files with 40 additions and 30 deletions
+32
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"slices"
"strconv"
"strings"
"time"
"github.com/google/uuid"
@@ -754,6 +755,31 @@ func (r *Recipient) GetByEmail(
return ToRecipient(&dbRecipient)
}
// GetByEmailLower looks up a recipient by a case insensitive email match.
// the supplied email is compared with LOWER() on both sides so that the same
// address in a different casing (e.g. Foo@bar.tld vs foo@bar.tld) is treated as
// the same recipient. used to reject a new recipient that duplicates an
// existing one apart from casing. for resolving a specific recipient use the
// exact match GetByEmail instead.
func (r *Recipient) GetByEmailLower(
ctx context.Context,
email *vo.Email,
fields ...string,
) (*model.Recipient, error) {
var dbRecipient database.Recipient
fields = assignTableToColumns(database.RECIPIENT_TABLE, fields)
res := useSelect(r.DB, fields).
Where(
fmt.Sprintf("LOWER(%s) = LOWER(?)", TableColumn(database.RECIPIENT_TABLE, "email")),
email.String(),
).
First(&dbRecipient)
if res.Error != nil {
return nil, res.Error
}
return ToRecipient(&dbRecipient)
}
// GetByEmailLowerAndCompanyID looks up a recipient by a case-insensitive email
// match within a company. the supplied email is compared with LOWER() on both
// sides so that differently cased addresses (e.g. John@X.com vs john@x.com) are
@@ -832,6 +858,12 @@ func (r *Recipient) Insert(
) (*uuid.UUID, error) {
id := uuid.New()
row := recp.ToDBMap()
// the email is the recipient identity and is stored lowercased so the same
// address in a different casing is one recipient. only applied on create so
// an existing stored casing is never rewritten on update
if email, ok := row["email"].(string); ok {
row["email"] = strings.ToLower(email)
}
row["id"] = id
AddTimestamps(row)
res := r.DB.Model(&database.Recipient{}).Create(row)
+8 -30
View File
@@ -2,7 +2,6 @@ package service
import (
"context"
"fmt"
"github.com/go-errors/errors"
"github.com/oapi-codegen/nullable"
@@ -51,7 +50,8 @@ func (r *Recipient) Create(
email := recipient.Email.MustGet()
// check if recipient already exists to avoid a unique constraint error
// and gorm does not return a unique constraint error but a string error depending on DB
_, err = r.RecipientRepository.GetByEmail(
// the match is case insensitive so the same address in a different casing is rejected
_, err = r.RecipientRepository.GetByEmailLower(
ctx,
&email,
)
@@ -112,32 +112,8 @@ func (r *Recipient) UpdateByID(
}
// update config - if a field is present and not null, update it
// if the email is changed, check that another recipient is not using this email already
if v, err := incoming.Email.Get(); err != nil {
if v.String() != current.Email.MustGet().String() {
var companyID *uuid.UUID
if current.CompanyID != nil {
if cid, err := current.CompanyID.Get(); err != nil {
companyID = &cid
}
}
_, err := r.RecipientRepository.GetByEmailAndCompanyID(
ctx,
&v,
companyID,
)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
r.Logger.Errorw("failed check existing recipient email", "error", err)
return err
}
if !errors.Is(err, gorm.ErrRecordNotFound) {
r.Logger.Debugw("email is already taken", "email", v.String())
s := fmt.Sprintf("email '%s' is already used by another recipient", v.String())
return validate.WrapErrorWithField(errors.New("not unique"), s)
}
}
current.Email.Set(v)
}
// the email is the recipient identity and is immutable, so any incoming
// email is ignored and the stored one is preserved
if v, err := incoming.Phone.Get(); err == nil {
current.Phone.Set(v)
}
@@ -589,9 +565,11 @@ func (r *Recipient) Import(
continue
}
// check if the recipient exists
// check if the recipient exists, case insensitively so the same address
// in a different casing updates the existing recipient instead of creating
// a duplicate
email := incoming.Email.MustGet()
current, err := r.RecipientRepository.GetByEmail(
current, err := r.RecipientRepository.GetByEmailLower(
ctx,
&email,
"id", "email", "company_id",