Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/internal/transport/api/req.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ type AliasReq struct {
Format string `json:"format"`
Domain string `json:"domain" validate:"required"`
WildcardLocalPart string `json:"wildcard_local_part" validate:"omitempty,alphanum,min=6,max=12"`
LocalPart string `json:"local_part" validate:"omitempty,alphanum,min=1,max=64"`
LocalPart string `json:"local_part" validate:"omitempty,emaillocalpart"`
}

type RecipientReq struct {
Expand Down
34 changes: 34 additions & 0 deletions api/internal/utils/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ func NewValidator() Validator {
log.Println("error registering search validation:", err)
}

err = v.RegisterValidation("emaillocalpart", emailLocalPartValidation)
if err != nil {
log.Println("error registering email local part validation:", err)
}

return v
}

Expand Down Expand Up @@ -115,3 +120,32 @@ func searchValidation(fl validator.FieldLevel) bool {
re := regexp.MustCompile(`^[-a-zA-Z0-9 ._+@]+$`)
return re.MatchString(value)
}

func emailLocalPartValidation(fl validator.FieldLevel) bool {
localPart := fl.Field().String()

if len(localPart) < 1 || len(localPart) > 64 {
return false
}

// RFC 5321/5322 allow a broad set of characters in the local part.
// This covers the common, practical subset: letters, digits, and
// the typical unquoted special characters.
var validChars = regexp.MustCompile(`^[a-zA-Z0-9.!#$%&'*+/=?^_` + "`" + `{|}~-]+$`).MatchString

if !validChars(localPart) {
return false
}

// Local part must not start or end with a dot
if strings.HasPrefix(localPart, ".") || strings.HasSuffix(localPart, ".") {
return false
}

// No consecutive dots allowed
if strings.Contains(localPart, "..") {
return false
}

return true
}
54 changes: 54 additions & 0 deletions api/internal/utils/validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,3 +210,57 @@ func TestSearchValidation(t *testing.T) {
}
}
}

func TestEmailLocalPartValidation(t *testing.T) {
v := NewValidator()

err := v.RegisterValidation("emaillocalpart", emailLocalPartValidation)
if err != nil {
t.Fatalf("expected no error when registering emaillocalpart validation, but got: %v", err)
}

tests := []struct {
value string
valid bool
desc string
}{
// Valid
{"user", true, "simple lowercase"},
{"User123", true, "mixed case alphanumeric"},
{"user.name", true, "dot-separated parts"},
{"user+tag", true, "plus sign"},
{"user-name", true, "hyphen"},
{"user_name", true, "underscore"},
{"user!#$%&'*+/=?^_{|}~-", true, "all allowed special chars"},
{"a", true, "single character (minimum length)"},
{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1", true, "64 characters (maximum length)"},
{"123", true, "digits only"},
// Invalid: length
{"", false, "empty string"},
{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1", false, "65 characters (over maximum length)"},
// Invalid: leading/trailing dot
{".user", false, "starts with dot"},
{"user.", false, "ends with dot"},
// Invalid: consecutive dots
{"user..name", false, "consecutive dots"},
// Invalid: disallowed characters
{"user name", false, "space not allowed"},
{"user@name", false, "@ sign not allowed"},
{"user(name)", false, "parentheses not allowed"},
{"user<name>", false, "angle brackets not allowed"},
{"user\\name", false, "backslash not allowed"},
{"user\"name", false, "double quote not allowed"},
// Security
{"<script>alert(1)</script>", false, "XSS attempt"},
{"' OR '1'='1", false, "SQL injection attempt"},
{"../../../etc/passwd", false, "path traversal attempt"},
}

for _, tt := range tests {
err := v.Var(tt.value, "emaillocalpart")
isValid := err == nil
if isValid != tt.valid {
t.Errorf("emailLocalPartValidation(%q): got %v, want %v (%s)", tt.value, isValid, tt.valid, tt.desc)
}
}
}
Loading