forgejo/modules/validation/email.go
2025-08-30 09:45:19 +02:00

132 lines
3 KiB
Go

// Copyright 2016 The Gogs Authors. All rights reserved.
// Copyright 2020 The Gitea Authors. All rights reserved.
// Copyright 2024 The Forgejo Authors. All rights reserved
// SPDX-License-Identifier: MIT
package validation
import (
"fmt"
"net/mail"
"strings"
"forgejo.org/modules/setting"
"forgejo.org/modules/util"
"github.com/gobwas/glob"
)
// ErrEmailNotActivated e-mail address has not been activated error
var ErrEmailNotActivated = util.NewInvalidArgumentErrorf("e-mail address has not been activated")
// ErrEmailInvalid represents an error where the email address does not comply with RFC 5322
// or has a leading '-' character
type ErrEmailInvalid struct {
Email string
}
// IsErrEmailInvalid checks if an error is an ErrEmailInvalid
func IsErrEmailInvalid(err error) bool {
_, ok := err.(ErrEmailInvalid)
return ok
}
func (err ErrEmailInvalid) Error() string {
return fmt.Sprintf("e-mail invalid [email: %s]", err.Email)
}
func (err ErrEmailInvalid) Unwrap() error {
return util.ErrInvalidArgument
}
// check if email is a valid address with allowed domain
func ValidateEmail(email string) error {
if err := validateEmailBasic(email); err != nil {
return err
}
return validateEmailDomain(email)
}
// check if email is a valid address when admins manually add or edit users
func ValidateEmailForAdmin(email string) error {
return validateEmailBasic(email)
// In this case we do not need to check the email domain
}
// validateEmailBasic checks whether the email complies with the rules
func validateEmailBasic(email string) error {
if len(email) == 0 {
return ErrEmailInvalid{email}
}
parsedAddress, err := mail.ParseAddress(email)
if err != nil {
return ErrEmailInvalid{email}
}
if parsedAddress.Name != "" {
return ErrEmailInvalid{email}
}
return nil
}
func validateEmailDomain(email string) error {
if _, ok := IsEmailDomainAllowed(email); !ok {
return ErrEmailInvalid{email}
}
return nil
}
func IsEmailDomainAllowed(email string) (validEmail, ok bool) {
// Normalized the address. This strips for example comments which could be
// used to smuggle a different domain
parsedAddress, err := mail.ParseAddress(email)
if err != nil {
return false, false
}
return true, isEmailDomainAllowedInternal(
parsedAddress.Address,
setting.Service.EmailDomainAllowList,
setting.Service.EmailDomainBlockList)
}
func isEmailDomainAllowedInternal(
email string,
emailDomainAllowList []glob.Glob,
emailDomainBlockList []glob.Glob,
) bool {
var result bool
if len(emailDomainAllowList) == 0 {
result = !isEmailDomainListed(emailDomainBlockList, email)
} else {
result = isEmailDomainListed(emailDomainAllowList, email)
}
return result
}
// isEmailDomainListed checks whether the domain of an email address
// matches a list of domains
func isEmailDomainListed(globs []glob.Glob, email string) bool {
if len(globs) == 0 {
return false
}
n := strings.LastIndex(email, "@")
if n <= 0 {
return false
}
domain := strings.ToLower(email[n+1:])
for _, g := range globs {
if g.Match(domain) {
return true
}
}
return false
}